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

[インデックス 13866] ファイルの概要

このコミットは、Go言語のcmd/apiツールにおけるインターフェースの扱いに関する修正です。具体的には、エクスポートされていない(unexported)メソッドを持つインターフェースのAPIレポート方法を改善し、Go 1互換性チェックの精度を高めています。

コミット

commit a29f3136b40c5a3b5da4034fe5def863d4ad2733
Author: Russ Cox <rsc@golang.org>
Date:   Tue Sep 18 15:57:03 2012 -0400

    cmd/api: allow extension of interfaces with unexported methods
    
    Fixes #4061.
    
    R=golang-dev, bradfitz
    CC=golang-dev
    https://golang.org/cl/6525047

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/a29f3136b40c5a3b5da4034fe5def863d4ad2733

元コミット内容

cmd/api: allow extension of interfaces with unexported methods

このコミットは、エクスポートされていないメソッドを持つインターフェースの拡張を許可するようにcmd/apiツールを変更します。

Fixes #4061.

Go issue #4061を修正します。

変更の背景

Go言語では、インターフェースはメソッドの集合を定義します。メソッドには、パッケージ外からアクセス可能なエクスポートされた(大文字で始まる)ものと、パッケージ内でのみアクセス可能なエクスポートされていない(小文字で始まる)ものがあります。

cmd/apiツールは、Go言語の標準ライブラリのAPIサーフェスを追跡し、Go 1互換性を維持するために使用されます。このツールは、APIの変更を検出し、互換性のない変更が行われていないかを確認します。

以前のcmd/apiの実装では、インターフェースがエクスポートされていないメソッドを含んでいる場合、そのインターフェースのAPIレポートが不完全または誤解を招く可能性がありました。特に、エクスポートされていないメソッドを持つインターフェースは、そのメソッドが定義されているパッケージ内でのみ完全に実装され、拡張されることができます。パッケージ外からは、エクスポートされたメソッドのみが可視であるため、インターフェースの完全なメソッドセットを外部から把握することはできません。

このコミットは、Go issue #4061で報告された問題を解決することを目的としています。この問題は、エクスポートされていないメソッドを含むインターフェースが、cmd/apiによって適切に扱われないことに関連していると考えられます。具体的には、そのようなインターフェースがAPIサーフェスの一部として報告される際に、その特殊な性質(同じパッケージ内でのみ完全に拡張可能であること)が考慮されていなかった可能性があります。これにより、API互換性チェックが不正確になる恐れがありました。

前提知識の解説

  • Go言語のインターフェース: Go言語のインターフェースは、メソッドのシグネチャの集合を定義する型です。Goでは、型がインターフェースのすべてのメソッドを実装していれば、そのインターフェースを「暗黙的に」実装しているとみなされます。interface{}は、メソッドを一つも持たない空のインターフェースであり、すべての型がこれを実装します。
  • エクスポートされたメソッドとエクスポートされていないメソッド: Goでは、識別子(変数名、関数名、メソッド名など)が大文字で始まる場合、それはエクスポートされ、パッケージ外からアクセス可能です。小文字で始まる場合、それはエクスポートされず、その識別子が定義されているパッケージ内でのみアクセス可能です。これはメソッドにも適用されます。
  • cmd/apiツール: Go言語のソースコードを解析し、公開されているAPI(エクスポートされた型、関数、メソッドなど)のリストを生成するツールです。このリストは、Go 1の互換性保証の基盤となります。Go 1の互換性ポリシーでは、既存の公開APIを変更することは厳しく制限されており、cmd/apiはその変更を検出するために使用されます。
  • Go 1互換性: Go 1は、そのリリース以降、既存のプログラムが新しいバージョンのGoでも動作し続けることを保証する「Go 1互換性保証」を提供しています。これは、公開APIの変更を厳しく管理することで実現されています。
  • インターフェースの拡張(埋め込み): Goのインターフェースは、他のインターフェースを埋め込むことで拡張できます。例えば、interface { io.Reader; io.Writer }のように記述することで、io.Readerio.Writerのすべてのメソッドを含む新しいインターフェースを定義できます。

エクスポートされていないメソッドを持つインターフェースは、そのメソッドがパッケージ外から呼び出せないため、そのインターフェースを完全に実装できるのは、そのメソッドが定義されているパッケージ内の型に限られます。これは、APIの互換性を考える上で重要な側面です。

技術的詳細

このコミットの核心は、cmd/apiツールがインターフェースのメソッドセットを解析し、そのインターフェースがエクスポートされていないメソッドを含んでいるかどうかを正確に識別する能力を向上させることにあります。

変更前は、cmd/apiはインターフェースのすべてのメソッド(エクスポートされたものとされていないもの両方)を列挙しようとしていた可能性があります。しかし、エクスポートされていないメソッドはパッケージの内部実装の詳細であり、外部APIサーフェスの一部とは見なされません。したがって、api/go1.txtのようなAPIリストにそれらを詳細に含めることは、誤解を招くか、不必要な詳細を提供することになります。

このコミットでは、src/cmd/api/goapi.go内のinterfaceMethods関数が変更されています。この関数は、インターフェースが持つメソッドのリストを返すだけでなく、そのインターフェースがエクスポートされていないメソッドを含んでいるかどうかを示すブーリアン値completeも返すようになりました。

  • completetrueの場合、そのインターフェースはエクスポートされたメソッドのみで構成されており、そのメソッドセットは外部から完全に把握できます。
  • completefalseの場合、そのインターフェースはエクスポートされていないメソッドを含んでおり、その完全なメソッドセットは定義元のパッケージ内でのみ意味を持ちます。

walkInterfaceType関数は、このcompleteフラグを利用して、api/go1.txtへの出力形式を調整します。

  • completetrueであれば、これまで通りインターフェースの各エクスポートされたメソッドを個別に列挙します。
  • completefalseであれば、個々のメソッドを列挙する代わりに、unexported methodsという一般的な記述を出力します。これにより、APIの利用者は、このインターフェースが内部的なメソッドを持っていることを認識しつつも、その詳細に依存する必要がないことが明確になります。

この変更により、cmd/apiはGo 1互換性保証の観点から、より正確なAPIサーフェスを報告できるようになります。エクスポートされていないメソッドは、パッケージの内部実装の一部であり、外部の利用者が依存すべきものではないため、APIリストからその詳細を省略することは適切です。

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

  1. api/go1.txt:

    • pkg go/ast, type Decl interface { End, Pos } のような行が pkg go/ast, type Decl interface, unexported methods のように変更されています。これは、該当するインターフェースがエクスポートされていないメソッドを持つことを示す新しい表記です。
    • 同様に、Expr, Spec, Stmt, reflect.Type, syscall.RoutingMessage, syscall.Sockaddr などのインターフェースの記述も変更されています。
  2. src/cmd/api/goapi.go:

    • interfaceMethods関数のシグネチャが変更され、complete boolという戻り値が追加されました。
      // 変更前: func (w *Walker) interfaceMethods(pkg, iname string) (methods []method) {
      // 変更後: func (w *Walker) interfaceMethods(pkg, iname string) (methods []method, complete bool) {
      
    • interfaceMethods関数内で、メソッドがエクスポートされていない場合(ast.IsExported(mname.Name)falseの場合)、completeフラグがfalseに設定されます。
    • 埋め込みインターフェース(ast.Identast.SelectorExprの場合)を処理する際にも、再帰的にinterfaceMethodsを呼び出し、その結果のcompleteフラグを現在のcompleteフラグと論理ANDで結合しています(complete = complete && c)。これにより、埋め込まれたインターフェースにエクスポートされていないメソッドが含まれていれば、親インターフェースもcomplete: falseとなります。
    • walkInterfaceType関数内で、interfaceMethodsの戻り値であるcompleteフラグがチェックされます。
      • !completeの場合、つまりインターフェースにエクスポートされていないメソッドが含まれる場合、w.emitFeature("unexported methods")が呼び出され、個々のメソッド名ではなく、一般的な「unexported methods」という特徴がAPIリストに記録されます。
      • !completeの場合、メソッド名のソートとtype %s interface {}の出力はスキップされます。
  3. src/cmd/api/testdata/src/pkg/p1/golden.txt:

    • テストデータであるgolden.txtも更新され、pkg p1, type I interface, unexported methodspkg p1, type Private interface, unexported methodsといった新しい表記が含まれています。
  4. src/cmd/api/testdata/src/pkg/p1/p1.go:

    • 新しいテスト用のインターフェースPublicPrivateが追加されています。
      type Public interface {
      	X()
      	Y()
      }
      
      type Private interface {
      	X()
      	y() // エクスポートされていないメソッド
      }
      
    • Privateインターフェースはエクスポートされていないメソッドy()を持つため、この変更のテストケースとして機能します。

コアとなるコードの解説

src/cmd/api/goapi.gointerfaceMethods関数とwalkInterfaceType関数がこの変更の核心です。

interfaceMethods関数

func (w *Walker) interfaceMethods(pkg, iname string) (methods []method, complete bool) {
    // ... (インターフェースの検索と初期化) ...

    complete = true // 初期値はtrue。エクスポートされていないメソッドが見つかればfalseにする。
    for _, f := range t.Methods.List {
        typ := f.Type
        switch tv := typ.(type) {
        case *ast.Field: // 通常のメソッド
            if mname := tv.Names[0]; ast.IsExported(mname.Name) {
                // エクスポートされたメソッドの場合、リストに追加
                methods = append(methods, method{
                    name: mname.Name,
                    sig:  w.funcSigString(ft),
                })
            } else {
                // エクスポートされていないメソッドの場合、completeをfalseにする
                complete = false
            }
        case *ast.Ident: // 埋め込みインターフェース (同じパッケージ内)
            // ...
            m, c := w.interfaceMethods(pkg, embedded) // 再帰呼び出し
            methods = append(methods, m...)
            complete = complete && c // 埋め込みインターフェースがcompleteでなければ、全体もcompleteでない
        case *ast.SelectorExpr: // 埋め込みインターフェース (異なるパッケージ)
            // ...
            m, c := w.interfaceMethods(fpkg, rhs) // 再帰呼び出し
            methods = append(methods, m...)
            complete = complete && c // 埋め込みインターフェースがcompleteでなければ、全体もcompleteでない
        // ...
        }
    }
    return
}

この関数は、指定されたインターフェースのメソッドを走査し、エクスポートされたメソッドをmethodsスライスに追加します。同時に、エクスポートされていないメソッドが見つかった場合、または埋め込まれたインターフェースがエクスポートされていないメソッドを含む場合、completeフラグをfalseに設定します。これにより、インターフェースの「公開された」メソッドセットが完全に把握できるかどうかを正確に判断できます。

walkInterfaceType関数

func (w *Walker) walkInterfaceType(name string, t *ast.InterfaceType) {
    methNames := []string{}

    pop := w.pushScope("type " + name + " interface")
    methods, complete := w.interfaceMethods(w.curPackageName, name) // completeフラグを取得
    for _, m := range methods {
        methNames = append(methNames, m.name)
        w.emitFeature(fmt.Sprintf("%s%s", m.name, m.sig)) // エクスポートされたメソッドを出力
    }
    if !complete {
        // メソッドセットにエクスポートされていないメソッドが含まれる場合
        // すべての実装が同じパッケージによって提供されるため、メソッドセットは拡張可能。
        // そのため、メソッドの完全なセットを記録する代わりに、
        // エクスポートされていないメソッドがあったことだけを記録する。
        // (インターフェースが縮小した場合、前回のループで出力されたメソッドシグネチャが
        // 消えるため、それに気づくことができる。)
        w.emitFeature("unexported methods")
    }
    pop()

    if !complete {
        return // completeでない場合、これ以降の処理(メソッド名のソートや空インターフェースの出力)はスキップ
    }

    sort.Strings(methNames)
    if len(methNames) == 0 {
        w.emitFeature(fmt.Sprintf("type %s interface {}", name))
    } else {
        w.emitFeature(fmt.Sprintf("type %s interface { %s }", name, strings.Join(methNames, ", ")))
    }
}

この関数は、interfaceMethodsから返されたcompleteフラグに基づいて、APIリストへの出力形式を決定します。completefalseの場合、個々のエクスポートされたメソッドのリストではなく、unexported methodsという一般的な特徴を記録します。これは、エクスポートされていないメソッドがAPI互換性保証の対象外であり、その詳細を公開する必要がないというGoの設計思想を反映しています。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント (特にパッケージ、エクスポート、インターフェースに関するセクション)
  • Go 1 Compatibility Guarantee: https://go.dev/doc/go1compat
  • Go ASTパッケージのドキュメント (Goのソースコードを解析するための抽象構文木に関する情報)
  • Go言語のインターフェースに関する解説記事 (エクスポートされた/されていないメソッドの挙動を含む)
  • cmd/apiツールの目的と機能に関する情報 (Goのソースコードリポジトリ内の関連ドキュメントやコメント)
  • Go issue #4061の議論内容 (問題の詳細と解決策の方向性)
  • Goのソースコード (特にsrc/cmd/api/ディレクトリ内のファイル)
  • Goのテストコード (特にsrc/cmd/api/testdata/ディレクトリ内のファイル)