Goにおける可視性 - 大文字と小文字の識別子の謎を解き明かす
Takashi Yamamoto
Infrastructure Engineer · Leapcell

Goのシンプルさは、その可視性ルールにも及び、驚くほど簡単でエレガントです。
public
、private
、protected
のような明示的なキーワードに依存する言語とは異なり、Goは識別子の命名に深く根ざした慣習、つまり最初の文字の大文字・小文字を利用します。
識別子が大文字または小文字のどちらで始まるかという、一見些細な詳細が、その可視性を決定する唯一の要因となります。
この記事では、これらのルールのニュアンスを掘り下げ、それらがさまざまなGoの構造へのアクセス可能性をどのように管理するかを説明します。
コア原則:エクスポートされたもの vs. エクスポートされていないもの
Goは、識別子に2つの可視性レベルを定義しています。
- エクスポートされた(公開): 大文字で始まる識別子は「エクスポートされた」と見なされます。これは、宣言されたパッケージの外部からも表示およびアクセス可能であることを意味します。
- エクスポートされていない(プライベート/パッケージローカル): 小文字で始まる識別子は「エクスポートされていない」と見なされます。これは、宣言されたパッケージの内部でのみ表示およびアクセス可能であることを意味します。そのパッケージの外部からはアクセスできません。
このルールは、すべてのトップレベル宣言に普遍的に適用されます。
- 変数
- 定数
- 関数
- 型(構造体、インターフェースなど)
- 構造体のフィールド
- インターフェースのメソッド
例を挙げて詳しく見てみましょう。
パッケージレベルの可視性
geometry
という名前のパッケージがあると仮定します。
// geometry/shapes.go package geometry import "fmt" // Circleは円を表すエクスポートされた構造体です。 type Circle struct { Radius float64 // Radiusはエクスポートされたフィールドです color string // colorはエクスポートされていないフィールドです } // areaは円の面積を計算します。エクスポートされていません。 func area(r float64) float64 { return 3.14159 * r * r } // NewCircleはエクスポートされたコンストラクタ関数です。 func NewCircle(radius float64, c string) *Circle { return &Circle{Radius: radius, color: c} } // GetAreaは円の面積を返すエクスポートされたメソッドです。 func (c *Circle) GetArea() float64 { // 同じパッケージ内にあるため、プライベートフィールド'color'とエクスポートされていない関数'area'にアクセスできます。 fmt.Printf("Calculating area for a %s circle.\n", c.color) return area(c.Radius) } // privateConstantはエクスポートされていない定数です。 const privateConstant = "This is private to the geometry package." // ExportedConstantはエクスポートされた定数です。 const ExportedConstant = "This is public to the geometry package." // privateVarはエクスポートされていないパッケージレベルの変数です。 var privateVar = 10 // PublicVarはエクスポートされたパッケージレベルの変数です。 var PublicVar = 20
次に、main
のような別のパッケージがgeometry
とどのようにやり取りするかを見てみましょう。
// main.go package main import ( "fmt" "your_module/geometry" // your_module を実際のモジュールパスに置き換えてください ) func main() { // エクスポートされた型、関数、定数にアクセスする c := geometry.NewCircle(5.0, "red") fmt.Println("Circle Radius:", c.Radius) // OK: Radius はエクスポートされています fmt.Println("Circle Area:", c.GetArea()) // OK: GetArea はエクスポートされています // fmt.Println("Circle Color:", c.color) // コンパイルエラー: c.color はエクスポートされていません // fmt.Println(geometry.area(10)) // コンパイルエラー: area はエクスポートされていません // fmt.Println(geometry.privateConstant) // コンパイルエラー: privateConstant はエクスポートされていません // fmt.Println(geometry.privateVar) // コンパイルエラー: privateVar はエクスポートされていません fmt.Println("Exported Constant:", geometry.ExportedConstant) // OK: ExportedConstant はエクスポートされています fmt.Println("Public Variable:", geometry.PublicVar) // OK: PublicVar はエクスポートされています // Circle を直接作成する。エクスポートされたフィールドのみ直接設定できます。 // Radius は設定できますが、color は設定できません。 // color を設定する必要がある場合は、エクスポートされたメソッドまたはコンストラクタを使用する必要があります。 c2 := geometry.Circle{Radius: 7.0} // エクスポートされたフィールドの設定はOKです // c3 := geometry.Circle{radius: 7.0} // コンパイルエラー: radius はエクスポートされていません(フィールド名がRadiusであっても) // c4 := geometry.Circle{color: "blue"} // コンパイルエラー: color はエクスポートされていません fmt.Println("Circle2 Radius:", c2.Radius) // 重要な注意:composite literal を使用して構造体を直接作成する場合でも、 // パッケージの外部からはエクスポートされたフィールドのみを初期化できます。 // composite literal を使用してエクスポートされていないフィールドを直接設定することはできません // もし literal が異なるパッケージにある場合。 }
スタイルされていない識別子にアクセスしようとした場合に得られるエラーメッセージは、次のようになります。
c.color undefined (cannot refer to unexported field or method color)
geometry.area undefined (cannot refer to unexported name geometry.area)
なぜこの設計なのか?
Goのアプローチは、いくつかの利点を提供します。
- シンプルさと読みやすさ: このルールは、記憶し適用するのが非常に簡単です。複数のキーワードや複雑なアクセス修飾子を学ぶ必要はありません。識別子を一目見れば、その可視性がわかります。
- 明示的な意図: 慣習として、
Foo
と書くことは、その公開性を明示的に述べていることになり、foo
は内部使用を意味します。これは、良いAPI設計を強化します。 - より良い設計の奨励: コードのどの部分をAPIとして公開する必要があり、どの部分を実装の詳細として保持すべきかを、開発者に自然に考えさせることを奨励します。これは、必然的に、より良いカプセル化とモジュール化につながります。
- カプセル化の強制: エクスポートされていない要素は、パッケージの内部コンポーネントとして機能し、パッケージの保守者が外部ユーザーに影響を与えることなく、実装をリファクタリングまたは変更できるようにします。エクスポートされたAPIのみが「契約」の一部となります。
特定のケースとベストプラクティス
構造体のフィールド
Circle
の例で示されているように、struct
内の個々のフィールドもこれらのルールに従います。これにより、データ構造のどの部分がパッケージの外部からアクセス可能かを細かく制御できます。多くの場合、エクスポートされていないフィールドは、エクスポートされたメソッド(例:color
を初期化するためのNewCircle
や、それを使用するためのGetArea
)を介して管理される内部状態に使用されます。
インターフェースのメソッド
構造体フィールドと同様に、インターフェース内に宣言されたメソッドも可視性ルールに従います。インターフェースメソッドが大文字で始まる場合、それはエクスポートされたメソッドであり、このインターフェースを実装する型は、そのシグネチャを持つエクスポートされたメソッドを提供する必要があることを意味します。
// geometry/shapes.go (続き) package geometry // Shapeはエクスポートされたインターフェースです type Shape interface { Area() float64 // エクスポートされたメソッド perimeter() float64 // エクスポートされていないメソッド - これは可能ですが、外部使用を意図したインターフェースではあまり一般的ではありません。 // なぜなら、このインターフェースを実装する外部の型もエクスポートされていない 'perimeter' メソッドを提供する必要があり、 // それは、それらが*同じ*パッケージにある場合にのみ提供できます。 // これは内部インターフェースによく使用されます。 } // Square は Shape インターフェースを(仮に)実装します type Square struct { side float64 } func (s *Square) Area() float64 { // Shape のエクスポートされた Area() を満たすにはエクスポートされる必要があります return s.side * s.side } func (s *Square) perimeter() float64 { // Shape のエクスポートされていない perimeter() を満たすにはエクスポートされていない必要があります return 4 * s.side }
コンストラクタ関数
エクスポートされていない構造体型を持ち、これらの型のインスタンスを作成するための「コンストラクタ」として機能する1つ以上のエクスポートされた関数を持つことは、一般的なGoの慣習です。これにより、パッケージは型がどのようにインスタンス化および初期化されるかを厳密に制御できます。
// internal/user.go package internal // user はユーザーを表すエクスポートされていない構造体です。 type user struct { id string name string } // NewUser は 'user' 型のエクスポートされたコンストラクタ関数です。 func NewUser(id, name string) *user { return &user{id: id, name: name} } // GetName はユーザー名にアクセスするためのエクスポートされたメソッドです。 func (u *user) GetName() string { return u.name } // privateMethod はエクスポートされていません。 func (u *user) privateMethod() { // ...内部で何かを行う... }
// main.go package main import ( "fmt" "your_module/internal" ) func main() { // u := internal.user{id: "123", name: "Alice"} // コンパイルエラー: user はエクスポートされていません u := internal.NewUser("456", "Bob") // OK: NewUser はエクスポートされています fmt.Println("User Name:", u.GetName()) // OK: GetName はエクスポートされています // fmt.Println("User ID:", u.id) // コンパイルエラー: u.id はエクスポートされていません(uはエクスポートされていない型のポインタであっても) // u.privateMethod() // コンパイルエラー: privateMethod はエクスポートされていません }
ここでは、internal.user
はmain
から完全に隠されています。main
パッケージは、エクスポートされたNewUser
関数とGetName
メソッドを介してのみ、それにアクセスできます。これにより、堅牢なカプセル化が提供されます。
名前付け規則とコンテキスト
大文字・小文字のルールは厳格ですが、「公開」または「プライベート」の意味は常にパッケージ境界に関して相対的です。package metrics
内のtempCount
という変数はエクスポートされていない可能性があり、外部からは見えません。しかし、その値がエクスポートされた関数metrics.GetMetricCount()
によって返される場合、概念的には、その値は関数の介して「公開」されます。これは、Goの可視性設計が、生のデータアクセスだけでなく、APIサーフェスをガイドすることを示しています。
結論
最初の文字の大文字・小文字(エクスポートされたものは大文字、エクスポートされていないものは小文字)のみによって決定されるGoの可視性ルールは、その設計哲学の礎です:シンプルさ、明瞭さ、そして設定より規約です。このエレガントなメカニズムは、カプセル化をデフォルトにし、API公開に明示的な意図を要求することで、優れたソフトウェアアーキテクチャを促進します。これらのルールを理解し、遵守することは、慣用的で保守可能で堅牢なGoアプリケーションを作成するための基本であり、パッケージの内部が分離されたままであることを保証し、安定したAPIが明確に定義されていることを保証します。