Go言語でJSONをYAMLに変換するコマンド json2yaml を作りました!

JSONYAMLに変換するコマンド json2yaml を、Go言語で作ってみました。 他の言語も含めると同じようなコマンドラインツールは無数に作られていますが、 Goのライブラリとして組み込みたかったというのが最初のモチベーションです。 また、JSONをストリーミング的にYAMLに変換できるのかという疑問を以前から抱いていて、ここに答えを得たかったというところもありました。

github.com

go install github.com/itchyny/json2yaml/cmd/json2yaml@latest

Homebrewでもインストールできます。

brew install itchyny/tap/json2yaml

JSONファイルを引数にしてYAMLを出力したり、

 $ cat sample.json
{
  "Sample": "JSON"
}

 $ json2yaml sample.json
Sample: JSON

標準入力を変換したりできます。

 $ curl -sSf "https://httpbin.org/json" | json2yaml
slideshow:
  author: Yours Truly
  date: date of publication
  slides:
    - title: Wake up to WonderWidgets!
      type: all
    - items:
        - Why <em>WonderWidgets</em> are great
        - Who <em>buys</em> WonderWidgets
      title: Overview
      type: all
  title: Sample Slide Show

REST APIの結果をlessで見たり、Jsonnetのビルド結果をKustomizeに持ってきたいときなどに便利そうですね。

ライブラリとしてはio.Writerio.Readerを受け取る関数Convertを提供していて、任意の入力から出力に変換できるようになっています。 もし標準入力から読み込んで変換結果を標準出力に出すだけならば、json2yaml.Convert(os.Stdout, os.Stdin) だけで実装できるようになっています。 引数の順序はio.CopyTransformerに倣っています。 Goの標準パッケージのみに依存しているので、他のツールにも組み込みやすいと思います。

元々はgojoコマンドYAML出力を対応してほしいという要望を受けたのが、このツールを作ったきっかけでした。 gojoコマンドはjoコマンドのGo実装で、JSONを簡単に組み立て出力できるツールです。

 $ gojo -p res[foo][][id]=10 res[foo][][id]=20 res[cnt]=2
{
  "res": {
    "foo": [
      {
        "id": 10
      },
      {
        "id": 20
      }
    ],
    "cnt": 2
  }
}

 $ gojo -y res[foo][][id]=10 res[foo][][id]=20 res[cnt]=2
res:
  foo:
    - id: 10
    - id: 20
  cnt: 2

要望を上げていただいた方は、gojoの出力をgojq --yaml-inputで変換しているとおっしゃっていて、自分のツールを組み合わせて使っている方からこのようにフィードバックが来るのはOSSをやっていてとても嬉しいですね。

itchyny.hatenablog.com itchyny.hatenablog.com \広告です/

さて、json2yamlの実装の話をしましょう。 YAMLJSONのスーパーセット、つまり全てのJSONYAMLでもあるので、入力をそのまま出力してもJSONからYAMLの変換と言えます。 しかしYAMLフォーマットとして我々が想像するのは、マッピングやシーケンスの括弧、文字列のクォートをできる限り使わずに書くフォーマットのことでしょう。

sample:
  string: Hello, world!
  sequence:
    - 0
    - null
    - boolean: true
    - string: This is a string!
  nested:
    mapping:
      and: |-
        #!/bin/sh
        echo "This is"
        echo "a multi-line"
        echo "string!"
    sequence:
      - - - - Deeply nested sequence!
  empty:
    - mapping: {}
    - sequence: []

こういう人間に優しいYAMLの出力を得るには、どのような実装が必要でしょうか。 まず、インデントのレベルの管理が必要ですね。 そして、マッピングやシーケンスの状態管理も必要になります。 これらのコレクションを閉じた次がマッピングのキーなのかシーケンスの要素なのか判別するために、スタックで管理します。 また、空マッピング {} と空シーケンス [] は特殊な処理が必要です

文字列をクォートせずにプレインスタイルで書いて良いかというのはなかなか複雑なルールが必要になります。 YAMLはさまざまな記号に意味がありますので、それらを含む場合はクォートが必要になります。 json2yamlでは、クォートが必要な文字列のパターンを正規表現にして判定するように実装しました。

- ""                   # 空文字はnullになるのでクォートが必要
- "~"                  # ~はnullになるのでクォートが必要
- ~~                   # ~~ならばクォートは不要
- "-"                  # -は[null]になるのでクォートが必要
- --                   # --ならばクォートは不要
- "false"              # booleanになるのでクォートが必要
- "y"                  # booleanになるのでクォートが必要 (YAML 1.1)
- "42.195"             # 数値になるのでクォートが必要
- 42.195.              # 数値にはならないのでクォートは不要
- ".nan"               # NaNになるのでクォートが必要
- "12:59:59"           # 60進数整数になるのでクォートが必要 (YAML 1.1)
- 12:59:60             # 60進数整数にはならないのでクォートは不要
- "2022-08-29"         # 日付になるのでクォートが必要
- "a #b"               # " #"以降はコメントになるのでクォートが必要
- a#b#c                # '#'の前が空白文字ではないのでクォートは不要
- "&test"              # '&'で始まるとアンカーになるのでクォートが必要
- "*test"              # '*'で始まるとエイリアスになるのでクォートが必要
- "* test"             # '*'の直後が空白文字の場合はエイリアスにはなりえないが、
                       # プレインスタイルは'*'で始まってはいけないのでクォートが必要
- x * y                # '*'から始まるわけではないのでクォートは不要
- "!test"              # '!'で始まるとタグになるのでクォートが必要
- "a: 0"               # ": "を含むとマッピングになるのでクォートが必要
- a:0                  # ':'の直後が空白文字ではないのでクォートは不要
- https://example.com/ # ':'の直後が空白文字ではないのでクォートは不要
- "\"quote"            # '"'で始まる場合はクォートが必要
- q"u"o"t"e"           # '"'が文字列の途中や最後の場合はクォートは不要
- "[null]"             # '['で始まるとフロースタイルのシーケンスになるのでクォートが必要
- "-100"               # 数字になるのでクォートが必要
- -100E                # 文字列全体が数字にならないのでクォートは不要
- "- 100E"             # '-'の後に空白文字があるとシーケンスになるのでクォートが必要
- " spaces "           # 前後の空白文字があるとクォートが必要
- "\uFEFFabc"          # BOMを含むのでクォートが必要
- "\uFFFFabc"          # Noncharacterを含むのでクォートが必要
---                    # document separator
"---"                  # document separatorと一致するのでクォートが必要
---                    # document separator
- ---                  # document separatorと一致するがシーケンスの要素なのでクォートは不要

JSONの文字列はYAMLの文字列でもあるので、クォートするときは基本的にJSONと同じエスケープをすれば大丈夫です。 ただし、YAMLパーサーにはクォート内のNoncharacterを解釈できないものがあるので、仕様からするとパーサーのバグですが (パーサーを名乗るなら仕様に沿って実装して欲しい)、json2yamlはNoncharacterもエスケープシーケンスで出力します。 文字列を扱うプログラムはNoncharacterが渡されたときの挙動に気をつけたいですね。

YAMLの複数行文字列は、制御コード・BOM・Noncharacter以外は基本的になんでも使えます。

- |
  This is
  a multi-line
  string.

- |-
  Chomp final
  line breaks.

- |+
  Keep final
  line breaks.

- ? |
    mapping
    key
  : |
    mapping
    value

- |2
      Leading white space
  can be represented
  by indentation indicator.

| |- |+は、文字列最後の改行の扱い方が変わります。 あまり遭遇することはありませんが、マッピングのキーにも複数行文字列を使えます。 文字列がスペースから始まる場合は、インデントの数を指定しないといけません (指定しないと、最初の行がインデントの基準になります)。 ただ、この構文をサポートしていないYAMLパーサーがあるので (パーサーを名乗るなら以下略)、 json2yamlは空白文字から始まる複数行文字列はクォートして出力しています。

json2yamlはJSON全体をメモリーに読み込まず、JSONの字句 (トークン) ごとにYAMLに変換していきます。 この実装により、map[string]any[]anyのメモリー確保を避けられます。 あまり現実的な例ではありませんが、以下のような巨大なシーケンスでも処理できます。

 $ (echo "["; yes "0," | head -n 100000000; echo "0]") | json2yaml
- 0
- 0
- 0
- 0
- 0
- 0
- 0
^C

ストリーミング処理だとマッピングのキー重複の扱いが気になる人もいるでしょう。 JSONではキーの重複は許されていますが (非推奨ではあります)、YAMLでは明確に許されていません。 ただしこれは仕様上の話であって、実際にはYAMLでもキー重複をエラーにしないパーサーはあるようです。 json2yamlはキー重複をエラーにしたり上書きにしたりはせずに、重複のまま出力しています。

ストリーミング処理ならば速そうということでパフォーマンスが気になるところだと思いますが、ファイルサイズに依存してだいぶ結果が変わるので、あくまで一例として見ていただければと思います。 gojq --yaml-outputPython版のjson2yamlNode版のjson2yaml、そしてyqを比較しました。

 $ export file=/tmp/package-lock.json
 $ file "$file"
/tmp/package-lock.json: JSON data
 $ stat -f %z "$file"
135843
 $ hyperfine --warmup 10 'json2yaml "$file"' 'gojq --yaml-output . "$file"' 'py/json2yaml "$file"' 'js/json2yaml "$file"' 'yq "$file"'
Benchmark 1: json2yaml "$file"
  Time (mean ± σ):       8.6 ms ±   0.3 ms    [User: 7.7 ms, System: 0.6 ms]
  Range (min … max):     7.9 ms …   9.4 ms    276 runs

Benchmark 2: gojq --yaml-output . "$file"
  Time (mean ± σ):       9.4 ms ±   0.4 ms    [User: 10.2 ms, System: 2.3 ms]
  Range (min … max):     8.5 ms …  11.1 ms    260 runs

Benchmark 3: py/json2yaml "$file"
  Time (mean ± σ):     103.8 ms ±   0.5 ms    [User: 98.1 ms, System: 4.4 ms]
  Range (min … max):   102.9 ms … 104.9 ms    28 runs

Benchmark 4: js/json2yaml "$file"
  Time (mean ± σ):      43.4 ms ±   0.7 ms    [User: 37.3 ms, System: 4.9 ms]
  Range (min … max):    42.0 ms …  44.7 ms    64 runs

Benchmark 5: yq "$file"
  Time (mean ± σ):     110.1 ms ±   1.7 ms    [User: 365.9 ms, System: 32.4 ms]
  Range (min … max):   106.2 ms … 113.0 ms    26 runs

Summary
  'json2yaml "$file"' ran
    1.09 ± 0.06 times faster than 'gojq --yaml-output . "$file"'
    5.06 ± 0.17 times faster than 'js/json2yaml "$file"'
   12.10 ± 0.37 times faster than 'py/json2yaml "$file"'
   12.83 ± 0.43 times faster than 'yq "$file"'

繰り返しになりますがこれは一例であって、必ずこの順位になるということではありません。 特に、Node版のjson2yamlはパフォーマンスが良くて、JSONファイルが大きくなるほど優秀でした (JSON.parseが圧倒的に速いんだと思います… 500MBを超えると負けました)。 ただし、マッピングのキーが*&から始まるとYAMLとして不正な出力になったり、キーが#から始まると出力行がコメントになったり、60文字を超える文字列の場合に先頭と末尾に改行が入ってしまったりしていて (JSONに戻すと値が変わる)、とても残念な出力でした。 yqはGoで書かれていますが、なぜここまで遅いのかはよく分かりませんでした。

YAMLはフロースタイルとブロックスタイルという文脈に応じて使える文字が変わるため、仕様の構文定義が複雑で読みとくのに骨が折れました。 また、仕様のゆるいYAML 1.1を実装しているパーサーもこの世にはまだあるため、json2yamlでは厳しめに文字列をクォート出力しています。 手元の開発マシンにはわりとたくさんJSONファイルがある (主にnode_modules) ので、これを使って変換処理をテストしました。 BOMがついていたり、C1制御コードが含まれていたり、だいぶ良いテストデータになりました。

Go版 json2yaml コマンドとライブラリ、作ってまだ間もないツールですが、完成度はわりと高いと思うので、ぜひ使ってみてください。 github.com

Go 1.18 で go run -mod=mod の非互換変更に気がついて報告した

Go 1.18 がリリースされて一か月程経ちました。 先日 go run を (go generate経由で) 使っているコードの挙動を見ていたら、モジュール周りの挙動が Go 1.17 と異なることに気が付きました。

Go 1.16 以降、go getgo mod tidy のような一部のコマンドを除くと、go.mod に足りない依存パッケージがあるときはエラーになります。 go rungo testなどでもgo.modを更新したい場合は、-mod=modというフラグを指定します。

go runは、Goのファイルのコンパイルと実行を行うコマンドです。 パッケージを引数として利用されることが多いコマンドですが、Goのファイルを指定して実行することもできます。

❯ cat main.go
package main

func main() {
        println("Hello, world!")
}

❯ cat go.mod
module tmp

go 1.18

❯ go run .
Hello, world!

❯ go run main.go
Hello, world!

Goのファイルを指定して実行する場合はcommand-line-argumentsパッケージという特殊な名前が付けられています。 これはほとんどドキュメントに記述されていませんが、go listのようなコマンドで確認することができます。

❯ go list .
tmp

❯ go list main.go
command-line-arguments

さて、コードが依存しているパッケージがgo.modにない場合はどうなるでしょうか。 go runコマンドはデフォルトではgo.modを更新しないので、依存パッケージが足りない場合はエラーになります。

❯ cat main.go
package main

import "golang.org/x/sys/unix"

func main() {
        println(unix.Getpid())
}

❯ cat go.mod
module tmp

go 1.18

❯ go run .
main.go:3:8: no required module provides package golang.org/x/sys/unix; to add it:
        go get golang.org/x/sys/unix

❯ go run main.go
main.go:3:8: no required module provides package golang.org/x/sys/unix; to add it:
        go get golang.org/x/sys/unix

では -mod=mod をつけるとどうなるでしょうか。 足りない依存をgo.modgo.sumに追加するフラグですので、go.modgolang.org/x/sys/unixパッケージが追加されます。

❯ go run -mod=mod .
go: finding module for package golang.org/x/sys/unix
go: found golang.org/x/sys/unix in golang.org/x/sys v0.0.0-20220412211240-33da011f77ad
10664

❯ cat go.mod
module tmp

go 1.18

require golang.org/x/sys v0.0.0-20220412211240-33da011f77ad

ファイルを指定した時も同じ挙動を期待しますよね。 しかし、Go 1.18.1の挙動は以下のようになります。

❯ cat go.mod
module tmp

go 1.18

❯ go run -mod=mod main.go
go: finding module for package golang.org/x/sys/unix
10820

❯ cat go.mod
module tmp

go 1.18

あれれ、go.modが更新されませんね。 足りない依存パッケージの検索やダウンロードはされ、コードは実行されましたが、go.modは更新されません。 これはgo buildなど、他のコマンドも同じ挙動になります。

実は Go 1.17 ではこの挙動ではありませんでした。 ファイルを指定した場合も-mod=modを指定すればgo.modが更新されていました。 この挙動に依存していたわけではなく、違いに気がついたのは偶然でした (Dockerイメージをpullしておらず、ホストマシンのGoとバージョンがずれていた)。

Go 1.18のリリースノートマニュアルを確認しましたが、いずれにもこの挙動に関する記載はありません。 たしかにcommand-line-argumentsパッケージは特殊な立ち位置で、特にモジュールとの兼ね合いは難しくなっています。 しかし、ユーザーに影響がある挙動の変更ならば、リリースノートに書いて欲しいですよね。 そういう気持ちを込めて、バグトラッカーに報告してみました。 github.com 無事、リグレッションの判断がされたので、Go 1.18へのバックポートもされるようです。 すぐに修正CLが投稿されました。 go runコマンドで-mod=modとGoファイルを指定した時の挙動の変更に気がついて報告したという話でした。

2021年を振り返って

今年は大きな転機が訪れた年でした。六年半勤めたはてなを退職し、11月からサイボウズで働いています。開発体制の規模やプロセス、文化の違いに戸惑いながらも、楽しく働いています。モブプロを中心とする開発は新しく入社した身としてありがたいです。技術的な負債の解消を続けながら、さらに踏み込んで顧客への価値提供を加速するための組織作りに貢献したいと思っています。そこそこ頂いているので、それに見合った働きができるように頑張ります。

住環境も大きく変わりました。六年住んだ1Rを離れ、1LDKに引っ越しました。ゆとりのある部屋の大きさになったことでQOLが上がりました。また引越したタイミングで昇降デスクやベッド、本棚など色々と買い揃えました。三口コンロになったこともあり、料理をする機会が増えました。ただ部屋が広いためか、あるいは24時間換気のせいなのか、エアコンの効きが悪くて寒いです。

OSS活動としては、粛々とgojqの改善を続けていました。末尾再帰呼び出しのパフォーマンスを改善したり、コマンドライン引数のパーサーを自前実装にして、jqとの互換性を高めたりしてました。gojqはライブラリとしてghやArgo CD、fqなど様々なツールに組み込まれていて嬉しい限りです。ただ、転職してから仕事で気を張っているのか、アウトプット量が減ってしまいました。来年は何か新しいものを作りたいです。

今年の見たアニメの中では『不滅のあなたへ』『かげきしょうじょ!!』『白い砂のアクアトープ』が良かったです。ドラマは『最愛』『監察医 朝顔』『漂着者』、映画は『マスカレード・ナイト』あたりでしょうか。引っ越しをしてからアマゾンプライムでよく映画を観ています。

来年がさらにいい一年になりますよう願っています。

パロナ「自分の生き方は与えられるものじゃない!自分で勝ち取るんだ!」

不滅のあなたへ 第5話

株式会社はてなを退職しました

本日2021年9月30日が最終出社日でした。1か月間のお休みをいただき、11月から新しい職場で働きます。2015年4月に新卒入社して、Mackerelチームとブックマークチームに所属しました。約六年半、お世話になりました。どちらのプロダクトも携わることができて楽しかったです。

入社時はウェブサービスの実装について何も知らない状態でした。しかしMackerelチームの優秀な先輩と理解あるプロダクトオーナーとディレクターに支えられて、様々なことを学びながらサービスと共に成長できました。サーバーやフロントエンドを書いたり、機能を作ったり閉じたり、年の瀬の夜中に障害対応したり、ioドメインが壊れたり、Linuxカーネルのコードを読んだり、時系列DBをクラウド上で実装したり、競合との差別化を考えたり、お客様とサービスの強み弱みについて議論したりしながら、SaaSビジネスをやる上での色々なことを経験できました。

ブックマークチームでは、アプリAPIの実装や、データウェアハウスの作成、クラウド移転などをやってました。私がチームに入った頃には新しいシステムへの移行が完了しており、コアな機能の設計を考えたり新機能を開発したりする時間はあまりありませんでした。しかしそういうフェーズのプロダクトに合った知見共有の仕組みを作るなど、開発組織の成長に寄与できたと思います。開発エンジニアながらインフラ周りの知識も発揮できたので、クラウド移転はとても楽しい仕事でした。

developer.hatenastaff.com

この2つのチームで開発を経験できたことで、ソフトウェアエンジニアという仕事の面白さや難しさを学ぶことができました。プロダクトのミッションやビジョンの重要性、データ駆動の意思決定を支えるためのデータウェアハウス、長期プロジェクトの見積もりとマネジメント、技術的負債の源とその可視化、非機能要件タスクの消化の難しさ、顧客別対応の危険性、機能を閉じる判断ポイント、技術選択とフルスクラッチの課題、社内サービスのオーナーシップ問題、投資判断とプロトタイピングの工数感、知見共有とチームの生産性、障害対応訓練、信頼性と開発速度のトレードオフ、リモートワーク下でのオンボーディングやインターンなど。組織やプロダクトのあらゆる課題について考える機会がありました。

趣味プログラミングの幅が広がったのも、仕事の影響が大きかったと感じます。jqをGoで実装してみたり、バイナリエディタを作ってみたり、PDFの仕様を調べてみたり、ptraceの勉強をしてみたり、これらは多かれ少なかれ仕事の内容とリンクしています。仕事からアイディアをもらうこともあれば、趣味で培われたコーディングや設計力によって仕事のパフォーマンスを発揮できることもありました。仕事と趣味が近すぎるとオーバーワークになってしまうので、いい距離にある公園の砂場で遊ぶくらいの趣味がいいですね。新しい職場に入ったらまたコードを書くことになるので、有休消化期間は電子工作でもしてみようと思っています。電気系学科の出身ですが何も覚えていないですね。

この度は、さらなる成長機会、次の組織フェーズ、企業を支える仕事を求めて転職することにしました。依然として根深く残るカウボーイ開発的な文化やそこで生まれる組織構造の歪みに悩みながらも、そういった課題について議論したり解決するためのアクションをとれなかったのが心残りです。しかし最近はデータ駆動の意思決定が浸透し始め、スクラムの改善に取り組む人も増えているので、組織として大きく前進していると思います。技術的に成長し続けたい人にも、組織や開発フローの改善が好きな人にもおすすめです。

hatenacorp.jp

共に働いてくれた皆様、日々の業務が円滑に進むよう支えていただいた皆様、業務に関係なく交流していただいた皆様、こっそり応援していただいた皆様、ありがとうございました。これからもソフトウェアエンジニアとして活動していくのと、京都に住んでいるので、今後ともよろしくお願いします。

Go言語の strconv.ParseFloat のとある挙動変更を見つけた話

弊社のとあるGoプロダクトでGo 1.14から1.16へアップデートしたところ、プログラムの挙動が変わる問題が発生しました *1。 ドキュメントに書かれていない strconv.ParseFloat の挙動の変更を踏んでしまったのです。

package main

import (
    "fmt"
    "strconv"
)

func main() {
    fmt.Println(strconv.ParseFloat("1e100x", 64))
    fmt.Println(strconv.ParseFloat("1e1000x", 64))
}

このコードをGo 1.14で実行すると

0 strconv.ParseFloat: parsing "1e100x": invalid syntax
0 strconv.ParseFloat: parsing "1e1000x": invalid syntax

となりますが、Go 1.15や1.16.4では

0 strconv.ParseFloat: parsing "1e100x": invalid syntax
+Inf strconv.ParseFloat: parsing "1e1000x": value out of range

のようになります。 strconv.ParseFloat は引数の文字列全体が浮動小数点数として解釈できなければエラーを返してくれる関数です。 しかし、prefixが大きすぎる浮動小数点数である場合は、別のエラーになっています。

これはバグでしょうか、意図した変更でしょうか。 この挙動の変更を追いかけたときの私の脳内をトレースしてみます。

まずはリリースノートを確認しましょう。 当初、私は1.14と1.16の間ということしか分かっておらず、1.151.16の両方のリリースノートを開きました。 しかし strconv.ParseFloat のエラーの内容を変更するという記述はどこにもありません。 1.16のParseFloatが Eisel-Lemire アルゴリズムを採用したという変更がいかにも怪しくて最初に疑いましたが、正解は1.15の ParseComplex の追加の方でした。

誰かissue報告はしていないでしょうか。意図した変更なので閉じられているか、gotipでは直っている可能性があります。 strconv ParseFloat value out of rangeのようなキーワードで探してみましたが、ヒットしませんでした。

それでは該当ファイルのHistoryやBlameを確認しましょう。 src/strconv/atof.goの変更を追っていると、複素数 (例: 3+4i) をパースする ParseComplex を実装する直前のコミットである 1d31f9b が目に留まると思います。複素数の実部虚部は浮動小数点数も許されており (例: 1e20+3e40i)、 ParseFloat の処理をうまく使うために parseFloatPrefix を導入したようです。

このコミットよく見ていると、パースが最後まで到達していないエラー処理を遅延させたために、 1e1000x のように全体で浮動小数点数として解釈できないものにはprefixをパースしたときのエラーをそのまま返してしまっていることがわかります。コミットの意図を汲み取ると、複素数のパーサーを書くためにリファクタリングしたコミットであり、エラー内容を変更するという意図はなさそうです。この段階で、意図しない挙動変更だったという確信を持ちます。

最後にドキュメントとの矛盾を探します。ParseFloat のエラーについて次のように記述されています。

The errors that ParseFloat returns have concrete type *NumError and include err.Num = s.

If s is not syntactically well-formed, ParseFloat returns err.Err = ErrSyntax.

If s is syntactically well-formed but is more than 1/2 ULP away from the largest floating point number of the given size, ParseFloat returns f = ±Inf, err.Err = ErrRange. https://golang.org/pkg/strconv/#ParseFloat

1e1000x1e100x と同様に syntactically well-formed ではありませんから、ドキュメントからも err.Err = ErrSyntax を返すのが正しいことがわかります。 もしエラーの変更が正しいならば、ドキュメントを修正する必要がありそうです。

該当するコミットにエラー内容までを変更する意図はなさそうなことや、ドキュメントと挙動が異なることから自信を持ったので、issueをたてました。

github.com

数時間後にはCLが作られて、Mergeされていました。こういう素早さはさすがですね。 やはり意図しない変更ということで合っていたようです。次のリリースでは直っていることでしょう。

strconv.ParseFloat の挙動の変更を見つけて報告したお話でした。おしまい。