Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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} のように記述します。しかし、この形式は以下のような問題を引き起こす可能性があります。

  1. 可読性の低下: フィールド名が明示されていないため、どの値がどのフィールドに対応するのかがコードを読むだけでは分かりにくい場合があります。特に構造体のフィールド数が多い場合や、フィールドの型が同じである場合には顕著です。
  2. 保守性の問題: 構造体のフィールドの順序が変更されたり、新しいフィールドが追加されたりした場合、タグ付けされていないリテラルはコンパイルエラーにはならないものの、意図しないフィールドに値が割り当てられる可能性があります。これにより、サイレントバグが発生しやすくなります。

このコミットは、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つあります。

  1. タグ付きフィールド (Keyed Fields): フィールド名を明示的に指定して値を割り当てる方法です。

    type Person struct {
        Name string
        Age  int
    }
    p := Person{Name: "Alice", Age: 30}
    

    この形式は、フィールドの順序に依存せず、可読性が高く、構造体の定義が変更されても安全性が高いという利点があります。

  2. タグなしフィールド (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.CompositeLitast.SelectorExpr などのASTノードを検査することで、タグなし構造体リテラルを識別しています。

技術的詳細

このコミットで導入された go vet の新しいチェックは、GoのASTを走査し、ast.CompositeLit ノード(複合リテラル)を特定することから始まります。

  1. AST走査への組み込み: src/cmd/vet/main.goFile.Visit メソッドに *ast.CompositeLit 型のノードを処理するための新しいケースが追加されました。これにより、go vet がソースコードのASTを走査する際に、すべての複合リテラルが walkCompositeLit 関数によって検査されるようになります。 walkCompositeLit 関数は、さらに checkUntaggedLiteral 関数を呼び出し、実際のチェックロジックを実行します。

  2. checkUntaggedLiteral 関数のロジック: src/cmd/vet/taglit.go に新しく追加された checkUntaggedLiteral 関数が、タグなし構造体リテラルの検出と警告の主要なロジックを実装しています。

    • タグ付きフィールドの判定: まず、複合リテラル c.Elts の各要素が *ast.KeyValueExpr 型であるかどうかをチェックします。*ast.KeyValueExprKey: Value の形式を持つ式を表すため、すべての要素がこの型であれば、その複合リテラルはタグ付きフィールドを使用していると判断し、チェックをスキップします。これは、タグ付きリテラルは問題がないためです。

    • 構造体リテラルの型チェック: 次に、複合リテラルが構造体リテラルであるかどうかを判断します。これは、リテラルの型 (c.Type) が pkg.Typ の形式(例: image.Point)である *ast.SelectorExpr であることを確認することで行われます。*ast.SelectorExprX.Sel の形式の式を表し、ここで X はパッケージ名(*ast.Ident)、Sel は型名(*ast.Ident)に対応します。

    • パッケージパスの解決: pkgPath ヘルパー関数を使用して、パッケージ名(例: "png")から対応するインポートパス(例: "image/png")を解決します。これは、File オブジェクトの Imports リストを走査し、パッケージ名とインポートパスのマッピングを推測することで行われます。この解決は構文と慣例に基づいているため、ドットインポートやパッケージ名とインポートパスの末尾要素が異なる場合には不正確になる可能性があります。

    • ホワイトリストによる除外: 解決された完全な型名(例: "image/png.FormatError")が untaggedLiteralWhitelist マップに含まれているかどうかをチェックします。このホワイトリストに含まれる型は、タグなしリテラルを使用しても問題ないと判断されたものです。主に以下の2種類の型が含まれます。

      • スライス型: 構文上、pkg.Typ{1, 2, 3} のような形式はスライスリテラルと構造体リテラルの区別がつきません。スライスは本質的に順序を持つコレクションであり、位置による初期化が自然であるため、多くの標準ライブラリのスライス型がホワイトリストに登録されています。
      • 「凍結された」構造体型: imageimage/color パッケージの一部の構造体型のように、将来的にフィールドが追加される可能性が極めて低い、あるいは追加されないことが保証されている型もホワイトリストに含まれます。これらの型は、フィールドの順序変更による影響を受けないため、タグなしリテラルが許容されます。
    • 警告の生成: 上記のチェックをすべて通過し、かつホワイトリストに含まれていない場合、f.Warnf を使用して、タグなしフィールドを使用している構造体リテラルに対する警告メッセージを生成します。警告メッセージには、問題のある型と、それがタグなしフィールドを使用していることが明示されます。

pkgPath 関数の詳細

pkgPath 関数は、与えられたパッケージ名(例: "png")に対応するインポートパス(例: "image/png")を推測します。これは、現在のファイルのインポート宣言を分析することで行われます。

  • 名前付きインポート: import pkgName "foo/bar" の形式の場合、x.Name.NamepkgName と一致すれば、そのインポートパス s を返します。
  • 匿名インポートまたは標準インポート: import "pkgName" または import "foo/bar/pkgName" の形式の場合、インポートパス spkgName と完全に一致するか、/pkgName で終わる場合に、そのインポートパス s を返します。

この関数は、あくまで構文と慣例に基づいた推測であり、Goの型システムによる厳密な解決ではない点に注意が必要です。

コアとなるコードの変更箇所

このコミットの主要な変更は以下の2つのファイルに集中しています。

  1. 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 {
    
  2. 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 がタグなし構造体リテラルであるかどうかを判断し、必要に応じて警告を発します。

  1. allKeyValue のチェック: for _, e := range c.Elts ループで、複合リテラルの各要素 e を検査します。e.(*ast.KeyValueExpr) で型アサーションを行い、要素が Key: Value の形式であるかどうかを確認します。もし一つでも KeyValueExpr でない要素があれば、それはタグなしフィールドが存在することを示唆するため、allKeyValuefalse に設定してループを抜けます。 ループ終了後、if allKeyValuetrue であれば、すべてのフィールドがタグ付きであるため、このリテラルは問題なく、関数はここで終了します。

  2. 型情報の抽出: c.Type は複合リテラルの型を表すASTノードです。これが *ast.SelectorExpr (例: pkg.Typ)であるかをチェックし、さらにその X がパッケージ名を表す *ast.IdentSel が型名を表す *ast.Ident であるかを検証します。これらのチェックが失敗した場合、それは構造体リテラルではないか、または予期しない形式であるため、関数は終了します。

  3. パッケージパスの解決とホワイトリストチェック: pkgPath(f, pkg.Name) を呼び出して、パッケージ名から完全なインポートパスを解決します。解決できない場合は警告を発して終了します。 解決されたインポートパスと型名を結合して完全な型名 typ (例: "image/color.RGBA")を構築します。 untaggedLiteralWhitelist[typ] を参照し、この型がホワイトリストに含まれているかどうかを確認します。含まれていれば、警告は発せずに終了します。

  4. 警告の生成: 上記のすべてのチェックを通過し、かつホワイトリストに含まれていない場合、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.NamepkgName と一致すれば、そのインポートパス s を返します。
  • 匿名インポートまたは標準インポートの処理: else ブロックは、import "path/to/pkg" のようにエイリアスがない場合を処理します。インポートパス spkgName と完全に一致するか、または /pkgName で終わる場合に、そのインポートパス s を返します。
  • どのインポートにも一致しない場合、空文字列を返します。

untaggedLiteralWhitelist マップ

このグローバルマップは、タグなし構造体リテラルを使用しても go vet が警告を発しない型を定義しています。

  • スライス型: コメントにもあるように、構文上スライスと構造体の区別が難しいため、標準ライブラリの多くのスライス型がホワイトリストに含まれています。スライスは本質的に順序を持つため、位置による初期化が自然です。
  • 「凍結された」構造体型: image および image/color パッケージの一部の構造体型(例: image.Point, image.Rectangle, image/color.RGBA など)が含まれています。これらの型は、Goの将来のバージョンでフィールドが追加されることがない(「凍結されている」)と見なされており、したがってフィールドの順序変更による互換性の問題が発生しないため、タグなしリテラルが許容されています。

このホワイトリストの存在により、go vet は実用性と厳密性のバランスを取り、開発者が本当に注意すべき潜在的な問題に焦点を当てることができます。

関連リンク

参考にした情報源リンク

  • 上記の「関連リンク」に記載されている公式ドキュメントおよびGo Change List。
  • Go言語における構造体リテラルのベストプラクティスに関する一般的な議論(例: Goコミュニティのブログ記事やフォーラム)。
    • (例: "Go: Struct Literals" で検索すると、タグ付きとタグなしの議論が見つかることがあります。)
    • (例: "Why use keyed fields in Go struct literals?" で検索すると、この変更の背景にある理由に関する議論が見つかることがあります。)