弊社のとある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.15と1.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
1e1000x
は 1e100x
と同様に syntactically well-formed ではありませんから、ドキュメントからも err.Err = ErrSyntax
を返すのが正しいことがわかります。
もしエラーの変更が正しいならば、ドキュメントを修正する必要がありそうです。
該当するコミットにエラー内容までを変更する意図はなさそうなことや、ドキュメントと挙動が異なることから自信を持ったので、issueをたてました。
数時間後にはCLが作られて、Mergeされていました。こういう素早さはさすがですね。 やはり意図しない変更ということで合っていたようです。次のリリースでは直っていることでしょう。
strconv.ParseFloat
の挙動の変更を見つけて報告したお話でした。おしまい。