はじめに
24新卒のやびくです。
突然ですが、Goといえば後方互換性ですよね。バージョンアップしても過去のコードをそのまま利用できることが保証されていると、安心感があります。公式ドキュメントにも、Go 1は後方互換性を保ちながら開発されることが明言されています。
ところが、Goを1.23にバージョンアップした途端にコンパイルエラーが発生するようになってしまいました。「これってGoの後方互換性を損なっているんじゃないの?」と思ったので、調べたことをまとめます。
発生した事象
弊チームで開発しているプロダクトでGoを1.21から1.23にバージョンアップしたところ、下記のコンパイルエラーが発生しました。
# command-line-arguments link: github.com/bytedance/sonic/ast: invalid reference to encoding/json.safeSet
どうやらbytedance/sonic
パッケージに関連したエラーのようです。こんな時はエラー文をGoogleの検索窓に貼り付けましょう。
エラーの原因
bytedance/sonic
のとあるイシューがヒットしました。Go 1.23以降、内部実装に対するlinknameの利用が原則禁止されたことへの対応を実施するようです。つまり、Goは後方互換性を維持するはずであるにも関わらず、bytedance/sonic
は突如linknameからの脱却を迫られたことになります。
業務で利用していたbytedance/sonic
はv1.11.1
であり、当時最新のv1.12.3
に変更することでコンパイルエラーは解消されました。
bytedance/sonic
のPRを確認してみると、Go1.23の発表を受けて、linknameを廃した実装が取り込まれていました。bytedance/sonic
の素早い対応には頭が上がりません。
しかし、ここでbytedance/sonic
はGoのバージョンアップに合わせてコードの変更を迫られています。内部実装に対するlinknameの利用禁止は、後方互換性に反した変更なのではないでしょうか?
linknameって?
linknameは、//go:linkname
ディレクティブを記載することによって利用できるようになる機能です。他のパッケージに実装された関数を呼び出す際、本来であれば公開された関数しか呼び出せないはずのところを、非公開の関数の呼び出しが可能になります。
下記のように、通常であれば非公開の関数を呼び出すことはできません。
package sub import "fmt" func PublicFunc() { fmt.Println("Public Func was called!") } func privateFunc() { fmt.Println("privateFunc was called!") } package main import "go-linkname/sub" func main() { sub.PublicFunc() // PublicFunc was called! sub.privateFunc() // undefined: sub.privateFunc }
ところが、//go:linkname
ディレクティブを利用することで非公開の関数を呼び出すことができるようになります。
package sub import ( "fmt" _ "unsafe" ) func PublicFunc() { fmt.Println("Public Func was called!") } func privateFunc() { fmt.Println("privateFunc was called!") } package main import ( "go-linkname/sub" _ "unsafe" ) //go:linkname privatefunc go-linkname/sub.privateFunc func privateFunc() func main() { sub.PublicFunc() // PublicFunc was called! privateFunc() // privateFunc was called! }
compileの公式ドキュメントに記載があるとおり、//go:linkname
ディレクティブを利用する際にはunsafeパッケージをブランクインポートする必要があります。
もともとlinkenameは後方互換性の対象ではなかった
「unsafeの後方互換性が損なわれているじゃないか!」そう思いながらunsafe
パッケージの公式ドキュメントを確認すると、2行目にこんなことが書いてあります。
Packages that import unsafe may be non-portable and are not protected by the Go 1 compatibility guidelines.
文の後半に、unsafeはGo 1の後方互換性ガイドラインによって保護されないと書かれています。そうです、unsafeはそもそも後方互換性の対象ではなかったのです。
知らなかった...。
また、Go 1.23のリリースノートには以下のような記述があります。
For backward compatibility, existing usages of //go:linkname found in a large open-source code corpus remain supported.
著名なOSSに限って、今後もlinknameによる内部実装の読み込みを認めるとのことです。後方互換性に対する執念を感じます。
後方互換性の例外
unsafe
が後方互換性の対象ではないように、Go 1の後方互換性には他にも例外があるのでしょうか。実は複数あるようです。
Go 1 and the Future of Go Programsに記載のあるものを、いくつか抜粋します。
1. unsafe
パッケージをインポートしたもの
これまでに言及してきたとおりです。unsafe
を利用することでinternal
の実装を直接読み込めるようになりますが、internal
に含まれる実装は破壊的変更が行われるかもしれません。
とはいえ原則としてGo 1.23以降標準パッケージをlinknameで直接読み込むことはできなくなったため、今後は関係なさそうです。
2. 構造体リテラル
公開されている構造体のフィールドが増えるかもしれません。それに伴って、フィールド名を省略した構造体リテラルのコンパイルが将来的に失敗するかもしれません。
package pkg type T struct { A int B string C string // new field } package main import "pkg" func main() { pkg.T{3, "x"} // too few values in struct literal of type pkg.T }
このコンパイルエラーは、構造体リテラルにフィールド名を明記することで回避できます。フィールド名は明記しましょう。
3. メソッド
構造体に対して、メソッドが追加されるかもしれません。一見すると後方互換性の問題はなさそうに思えますが、構造体を埋め込んで自前で実装したメソッドと新規メソッドの命名が衝突するかもしれません。
4. ドットインポート
import . "path"
のようにパッケージをインポートすることで、インポート元を逐一明記する必要がなくなる記法のことです。メソッドと同様に、パッケージに追加された構造体や関数の命名が、自前の命名と衝突する可能性があります。ドットインポートはなるべく利用しないようにしましょう。
5. Sub-repositories
golang.org/x/
配下からインポートするパッケージ達のことで、「準標準パッケージ」のような位置付けになっています。標準パッケージの後方互換性は保証されているものの、Sub-repositoriesは比較的弱い互換性の下で開発されます。
まとめ
Goのバージョンアップに伴って、特定バージョンのOSSをコンパイルできなくなる事象が発生しました。しかしこの破壊的変更はGoの掲げる後方互換性に反するものではなく、特定のケースに限って破壊的変更がなされるとわかりました。
真に厳密な後方互換性ではないものの、今回の例では著名なOSSに限って今後もlinknameによる内部実装への参照を許可するなど、後方互換性への強いこだわりを感じることができました。局所的な破壊的変更に対するOSSの対処も素早く、開発に大きな影響が発生することはありませんでした。