[インデックス 11584] ファイルの概要
このコミットは、Go言語の静的解析ツールである go vet
に、タグ付けされていない(位置による)構造体リテラルを検出する新しいチェック機能を追加するものです。これにより、コードの可読性と保守性を向上させ、将来的な構造体定義の変更に対する堅牢性を高めることを目的としています。
コミット
commit 9de9c95787096d4150315bd974f7815e0b667a98
Author: Nigel Tao <nigeltao@golang.org>
Date: Fri Feb 3 14:33:41 2012 +1100
vet: add a check for untagged struct literals.
R=rsc, dsymonds
CC=golang-dev, gri
https://golang.org/cl/5622045
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/9de9c95787096d4150315bd974f7815e0b667a98
元コミット内容
vet: add a check for untagged struct literals.
R=rsc, dsymonds
CC=golang-dev, gri
https://golang.org/cl/5622045
変更の背景
Go言語において、構造体リテラルを初期化する際に、フィールド名を明示せずに値の順序のみで初期化する「タグ付けされていない(untagged)」形式を使用することが可能です。例えば、MyStruct{value1, value2}
のように記述します。しかし、この形式は以下のような問題を引き起こす可能性があります。
- 可読性の低下: フィールド名が明示されていないため、どの値がどのフィールドに対応するのかがコードを読むだけでは分かりにくい場合があります。特に構造体のフィールド数が多い場合や、フィールドの型が同じである場合には顕著です。
- 保守性の問題: 構造体のフィールドの順序が変更されたり、新しいフィールドが追加されたりした場合、タグ付けされていないリテラルはコンパイルエラーにはならないものの、意図しないフィールドに値が割り当てられる可能性があります。これにより、サイレントバグが発生しやすくなります。
このコミットは、go vet
ツールにこの種の潜在的な問題を検出する機能を追加することで、Goコードの品質と堅牢性を向上させることを目的としています。開発者が明示的なフィールド名(例: MyStruct{Field1: value1, Field2: value2}
)を使用することを奨励し、より安全で保守しやすいコードベースを促進します。ただし、スライス型や一部の特定の構造体(image
パッケージの型など)のように、位置による初期化が自然であり、かつフィールドの順序変更のリスクが低い場合には、例外として警告の対象外とするホワイトリスト機構も導入されています。
前提知識の解説
go vet
ツール
go vet
は、Go言語のソースコードを静的に解析し、潜在的なバグや疑わしい構成を報告するツールです。コンパイルエラーにはならないが、実行時に問題を引き起こす可能性のあるコードパターン(例: Printf
フォーマット文字列の不一致、到達不能なコード、ロックの誤用など)を検出します。go vet
はGoの標準ツールチェーンの一部であり、コード品質を維持するための重要な役割を担っています。
複合リテラル (Composite Literals)
Go言語における複合リテラルは、構造体、配列、スライス、マップなどの複合型を初期化するための構文です。波括弧 {}
を使用して要素のリストを指定します。
例:
- 配列/スライス:
[]int{1, 2, 3}
- マップ:
map[string]int{"a": 1, "b": 2}
- 構造体:
MyStruct{Field1: "value", Field2: 123}
構造体リテラルにおけるタグ付きフィールドとタグなしフィールド
構造体リテラルを初期化する際、各フィールドに値を割り当てる方法は2つあります。
-
タグ付きフィールド (Keyed Fields): フィールド名を明示的に指定して値を割り当てる方法です。
type Person struct { Name string Age int } p := Person{Name: "Alice", Age: 30}
この形式は、フィールドの順序に依存せず、可読性が高く、構造体の定義が変更されても安全性が高いという利点があります。
-
タグなしフィールド (Untagged / Positional Fields): フィールド名を指定せず、構造体定義におけるフィールドの順序に従って値を割り当てる方法です。
type Person struct { Name string Age int } p := Person{"Bob", 25} // Nameに"Bob", Ageに25が割り当てられる
この形式は簡潔に記述できますが、構造体のフィールドの順序が変更されたり、新しいフィールドが追加されたりすると、意図しない値の割り当てが発生する可能性があります。例えば、
Person
構造体にAddress string
フィールドがAge
の後に追加された場合、{"Bob", 25}
はName: "Bob", Age: 25
とはならず、Name: "Bob", Age: 25, Address: ""
となるか、あるいはコンパイルエラーになる可能性があります(Goのバージョンや具体的な変更内容による)。しかし、既存のフィールドの間に新しいフィールドが挿入された場合、コンパイルエラーにならずに値の割り当てがずれるという、より深刻な問題を引き起こすことがあります。
go/ast
パッケージ
go/ast
パッケージは、Go言語のソースコードの抽象構文木(AST: Abstract Syntax Tree)を表現するためのデータ構造と、それを操作するための関数を提供します。go vet
のような静的解析ツールは、このASTを走査してコードの構造やパターンを分析し、問題のある箇所を特定します。このコミットでは、ast.CompositeLit
や ast.SelectorExpr
などのASTノードを検査することで、タグなし構造体リテラルを識別しています。
技術的詳細
このコミットで導入された go vet
の新しいチェックは、GoのASTを走査し、ast.CompositeLit
ノード(複合リテラル)を特定することから始まります。
-
AST走査への組み込み:
src/cmd/vet/main.go
のFile.Visit
メソッドに*ast.CompositeLit
型のノードを処理するための新しいケースが追加されました。これにより、go vet
がソースコードのASTを走査する際に、すべての複合リテラルがwalkCompositeLit
関数によって検査されるようになります。walkCompositeLit
関数は、さらにcheckUntaggedLiteral
関数を呼び出し、実際のチェックロジックを実行します。 -
checkUntaggedLiteral
関数のロジック:src/cmd/vet/taglit.go
に新しく追加されたcheckUntaggedLiteral
関数が、タグなし構造体リテラルの検出と警告の主要なロジックを実装しています。-
タグ付きフィールドの判定: まず、複合リテラル
c.Elts
の各要素が*ast.KeyValueExpr
型であるかどうかをチェックします。*ast.KeyValueExpr
はKey: Value
の形式を持つ式を表すため、すべての要素がこの型であれば、その複合リテラルはタグ付きフィールドを使用していると判断し、チェックをスキップします。これは、タグ付きリテラルは問題がないためです。 -
構造体リテラルの型チェック: 次に、複合リテラルが構造体リテラルであるかどうかを判断します。これは、リテラルの型 (
c.Type
) がpkg.Typ
の形式(例:image.Point
)である*ast.SelectorExpr
であることを確認することで行われます。*ast.SelectorExpr
はX.Sel
の形式の式を表し、ここでX
はパッケージ名(*ast.Ident
)、Sel
は型名(*ast.Ident
)に対応します。 -
パッケージパスの解決:
pkgPath
ヘルパー関数を使用して、パッケージ名(例: "png")から対応するインポートパス(例: "image/png")を解決します。これは、File
オブジェクトのImports
リストを走査し、パッケージ名とインポートパスのマッピングを推測することで行われます。この解決は構文と慣例に基づいているため、ドットインポートやパッケージ名とインポートパスの末尾要素が異なる場合には不正確になる可能性があります。 -
ホワイトリストによる除外: 解決された完全な型名(例: "image/png.FormatError")が
untaggedLiteralWhitelist
マップに含まれているかどうかをチェックします。このホワイトリストに含まれる型は、タグなしリテラルを使用しても問題ないと判断されたものです。主に以下の2種類の型が含まれます。- スライス型: 構文上、
pkg.Typ{1, 2, 3}
のような形式はスライスリテラルと構造体リテラルの区別がつきません。スライスは本質的に順序を持つコレクションであり、位置による初期化が自然であるため、多くの標準ライブラリのスライス型がホワイトリストに登録されています。 - 「凍結された」構造体型:
image
やimage/color
パッケージの一部の構造体型のように、将来的にフィールドが追加される可能性が極めて低い、あるいは追加されないことが保証されている型もホワイトリストに含まれます。これらの型は、フィールドの順序変更による影響を受けないため、タグなしリテラルが許容されます。
- スライス型: 構文上、
-
警告の生成: 上記のチェックをすべて通過し、かつホワイトリストに含まれていない場合、
f.Warnf
を使用して、タグなしフィールドを使用している構造体リテラルに対する警告メッセージを生成します。警告メッセージには、問題のある型と、それがタグなしフィールドを使用していることが明示されます。
-
pkgPath
関数の詳細
pkgPath
関数は、与えられたパッケージ名(例: "png")に対応するインポートパス(例: "image/png")を推測します。これは、現在のファイルのインポート宣言を分析することで行われます。
- 名前付きインポート:
import pkgName "foo/bar"
の形式の場合、x.Name.Name
がpkgName
と一致すれば、そのインポートパスs
を返します。 - 匿名インポートまたは標準インポート:
import "pkgName"
またはimport "foo/bar/pkgName"
の形式の場合、インポートパスs
がpkgName
と完全に一致するか、/pkgName
で終わる場合に、そのインポートパスs
を返します。
この関数は、あくまで構文と慣例に基づいた推測であり、Goの型システムによる厳密な解決ではない点に注意が必要です。
コアとなるコードの変更箇所
このコミットの主要な変更は以下の2つのファイルに集中しています。
-
src/cmd/vet/main.go
:File.Visit
メソッドに*ast.CompositeLit
型のノードを処理するためのcase
が追加されました。- 新しいヘルパー関数
walkCompositeLit
が追加され、*ast.CompositeLit
ノードを受け取り、f.checkUntaggedLiteral(c)
を呼び出します。
--- a/src/cmd/vet/main.go +++ b/src/cmd/vet/main.go @@ -175,6 +175,8 @@ func (f *File) Visit(node ast.Node) ast.Visitor { switch n := node.(type) { case *ast.CallExpr: f.walkCallExpr(n) + case *ast.CompositeLit: + f.walkCompositeLit(n) case *ast.Field: f.walkFieldTag(n) case *ast.FuncDecl: @@ -190,6 +192,11 @@ func (f *File) walkCall(call *ast.CallExpr, name string) { f.checkFmtPrintfCall(call, name) } +// walkCompositeLit walks a composite literal. +func (f *File) walkCompositeLit(c *ast.CompositeLit) { + f.checkUntaggedLiteral(c) +} + // walkFieldTag walks a struct field tag. func (f *File) walkFieldTag(field *ast.Field) { if field.Tag == nil {
-
src/cmd/vet/taglit.go
:- このファイルは新規作成されました。
checkUntaggedLiteral
関数が定義され、タグなし構造体リテラルの検出ロジックが含まれています。pkgPath
ヘルパー関数が定義され、パッケージ名からインポートパスを解決します。untaggedLiteralWhitelist
マップが定義され、タグなしリテラルが許容される型がリストされています。
// Copyright 2012 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // This file contains the test for untagged struct literals. package main import ( "go/ast" "strings" ) // checkUntaggedLiteral checks if a composite literal is an struct literal with // untagged fields. func (f *File) checkUntaggedLiteral(c *ast.CompositeLit) { // Check if the CompositeLit contains an untagged field. allKeyValue := true for _, e := range c.Elts { if _, ok := e.(*ast.KeyValueExpr); !ok { allKeyValue = false break } } if allKeyValue { return } // Check that the CompositeLit's type has the form pkg.Typ. s, ok := c.Type.(*ast.SelectorExpr) if !ok { return } pkg, ok := s.X.(*ast.Ident) if !ok { return } // Convert the package name to an import path, and compare to a whitelist. path := pkgPath(f, pkg.Name) if path == "" { f.Warnf(c.Pos(), "unresolvable package for %s.%s literal", pkg.Name, s.Sel.Name) return } typ := path + "." + s.Sel.Name if untaggedLiteralWhitelist[typ] { return } f.Warnf(c.Pos(), "%s struct literal uses untagged fields", typ) } // pkgPath returns the import path "image/png" for the package name "png". // // This is based purely on syntax and convention, and not on the imported // package's contents. It will be incorrect if a package name differs from the // leaf element of the import path, or if the package was a dot import. func pkgPath(f *File, pkgName string) (path string) { for _, x := range f.file.Imports { s := strings.Trim(x.Path.Value, `"` ) if x.Name != nil { // Catch `import pkgName "foo/bar"`. if x.Name.Name == pkgName { return s } } else { // Catch `import "pkgName"` or `import "foo/bar/pkgName"`. if s == pkgName || strings.HasSuffix(s, "/"+pkgName) { return s } } } return "" } var untaggedLiteralWhitelist = map[string]bool{ /* These types are actually slices. Syntactically, we cannot tell whether the Typ in pkg.Typ{1, 2, 3} is a slice or a struct, so we whitelist all the standard package library's exported slice types. find $GOROOT/src/pkg -type f | grep -v _test.go | xargs grep '^type.*\[\]' | \ grep -v ' map\[' | sed 's,/[^/]*go.type,,' | sed 's,.*src/pkg/,,' | \ sed 's, ,.,' | sed 's, .*,,' | grep -v '\.[a-z]' | sort */ "crypto/x509/pkix.RDNSequence": true, "crypto/x509/pkix.RelativeDistinguishedNameSET": true, "database/sql.RawBytes": true, "debug/macho.LoadBytes": true, "encoding/asn1.ObjectIdentifier": true, "encoding/asn1.RawContent": true, "encoding/json.RawMessage": true, "encoding/xml.CharData": true, "encoding/xml.Comment": true, "encoding/xml.Directive": true, "exp/norm.Decomposition": true, "exp/types.ObjList": true, "go/scanner.ErrorList": true, "image/color.Palette": true, "net.HardwareAddr": true, "net.IP": true, "net.IPMask": true, "sort.Float64Slice": true, "sort.IntSlice": true, "sort.StringSlice": true, "unicode.SpecialCase": true, // These image and image/color struct types are frozen. We will never add fields to them. "image/color.Alpha16": true, "image/color.Alpha": true, "image/color.Gray16": true, "image/color.Gray": true, "image/color.NRGBA64": true, "image/color.NRGBA": true, "image/color.RGBA64": true, "image/color.RGBA": true, "image/color.YCbCr": true, "image.Point": true, "image.Rectangle": true, }
コアとなるコードの解説
checkUntaggedLiteral
関数
この関数は、複合リテラル c
がタグなし構造体リテラルであるかどうかを判断し、必要に応じて警告を発します。
-
allKeyValue
のチェック:for _, e := range c.Elts
ループで、複合リテラルの各要素e
を検査します。e.(*ast.KeyValueExpr)
で型アサーションを行い、要素がKey: Value
の形式であるかどうかを確認します。もし一つでもKeyValueExpr
でない要素があれば、それはタグなしフィールドが存在することを示唆するため、allKeyValue
をfalse
に設定してループを抜けます。 ループ終了後、if allKeyValue
がtrue
であれば、すべてのフィールドがタグ付きであるため、このリテラルは問題なく、関数はここで終了します。 -
型情報の抽出:
c.Type
は複合リテラルの型を表すASTノードです。これが*ast.SelectorExpr
(例:pkg.Typ
)であるかをチェックし、さらにそのX
がパッケージ名を表す*ast.Ident
、Sel
が型名を表す*ast.Ident
であるかを検証します。これらのチェックが失敗した場合、それは構造体リテラルではないか、または予期しない形式であるため、関数は終了します。 -
パッケージパスの解決とホワイトリストチェック:
pkgPath(f, pkg.Name)
を呼び出して、パッケージ名から完全なインポートパスを解決します。解決できない場合は警告を発して終了します。 解決されたインポートパスと型名を結合して完全な型名typ
(例: "image/color.RGBA")を構築します。untaggedLiteralWhitelist[typ]
を参照し、この型がホワイトリストに含まれているかどうかを確認します。含まれていれば、警告は発せずに終了します。 -
警告の生成: 上記のすべてのチェックを通過し、かつホワイトリストに含まれていない場合、
f.Warnf(c.Pos(), "%s struct literal uses untagged fields", typ)
を呼び出して警告メッセージを生成します。c.Pos()
はソースコード内のリテラルの位置情報を提供し、typ
は問題のある型を示します。
pkgPath
関数
この関数は、与えられたパッケージ名 pkgName
に対応するインポートパスを、現在のファイル f
のインポート宣言から推測します。
f.file.Imports
は、現在のファイルがインポートしているパッケージのリストです。- 各インポート
x
について、そのパスx.Path.Value
から引用符を削除してs
とします。 - 名前付きインポートの処理:
if x.Name != nil
は、import alias "path/to/pkg"
のようにエイリアスが指定されている場合を処理します。x.Name.Name
がpkgName
と一致すれば、そのインポートパスs
を返します。 - 匿名インポートまたは標準インポートの処理:
else
ブロックは、import "path/to/pkg"
のようにエイリアスがない場合を処理します。インポートパスs
がpkgName
と完全に一致するか、または/pkgName
で終わる場合に、そのインポートパスs
を返します。 - どのインポートにも一致しない場合、空文字列を返します。
untaggedLiteralWhitelist
マップ
このグローバルマップは、タグなし構造体リテラルを使用しても go vet
が警告を発しない型を定義しています。
- スライス型: コメントにもあるように、構文上スライスと構造体の区別が難しいため、標準ライブラリの多くのスライス型がホワイトリストに含まれています。スライスは本質的に順序を持つため、位置による初期化が自然です。
- 「凍結された」構造体型:
image
およびimage/color
パッケージの一部の構造体型(例:image.Point
,image.Rectangle
,image/color.RGBA
など)が含まれています。これらの型は、Goの将来のバージョンでフィールドが追加されることがない(「凍結されている」)と見なされており、したがってフィールドの順序変更による互換性の問題が発生しないため、タグなしリテラルが許容されています。
このホワイトリストの存在により、go vet
は実用性と厳密性のバランスを取り、開発者が本当に注意すべき潜在的な問題に焦点を当てることができます。
関連リンク
- Go Change List (CL) for this commit: https://golang.org/cl/5622045
- Go言語の複合リテラルに関する公式ドキュメント (Go言語仕様): https://go.dev/ref/spec#Composite_literals
go vet
コマンドの公式ドキュメント: https://pkg.go.dev/cmd/vetgo/ast
パッケージの公式ドキュメント: https://pkg.go.dev/go/ast
参考にした情報源リンク
- 上記の「関連リンク」に記載されている公式ドキュメントおよびGo Change List。
- Go言語における構造体リテラルのベストプラクティスに関する一般的な議論(例: Goコミュニティのブログ記事やフォーラム)。
- (例: "Go: Struct Literals" で検索すると、タグ付きとタグなしの議論が見つかることがあります。)
- (例: "Why use keyed fields in Go struct literals?" で検索すると、この変更の背景にある理由に関する議論が見つかることがあります。)