Goの構造体:定義、使用方法、匿名フィールド、ネストの理解
Grace Collins
Solutions Engineer · Leapcell

Goの型システムはシンプルさと効率性を目指して設計されており、データ集約の核心にはstruct
があります。struct
は、0個以上の異なる型の名前付きフィールドを1つの論理的な単位にまとめた複合データ型です。これはGoでカスタムデータ型を定義する主要な方法であり、オブジェクト指向言語のクラスに似ていますが(Goは純粋なOOPではありません)、継承階層はありません。この記事では、Goにおける構造体の定義、実用例、匿名フィールドというユニークな概念、そして構造体ネストのパワーについて掘り下げていきます。
構造体の定義と初期化
構造体は、type
キーワード、構造体の名前、そして中括弧で囲まれたフィールドをstruct
キーワードに続けて定義します。各フィールドには名前と型があります。
簡単な例から始めましょう:Person
構造体の定義です。
package main import "fmt" // Person は個人の情報を保持する構造体を定義します。 type Person struct { Name string Age int IsAdult bool } func main() { // 構造体をいくつかの方法で初期化: // 1. Zero-value 初期化: // 全てのフィールドはそれぞれのゼロ値 (例: "", 0, false) で初期化されます。 var p1 Person fmt.Println("p1:", p1) // 出力: p1: { 0 false} // 2. フィールドごとの代入: p1.Name = "Alice" p1.Age = 30 p1.IsAdult = true fmt.Println("p1 updated:", p1) // 出力: p1 updated: {Alice 30 true} // 3. 構造体リテラル (順序指定) の使用: // これは、構造体定義で宣言された順序でフィールドを指定する必要があります。 p2 := Person{"Bob", 25, false} fmt.Println("p2:", p2) // 出力: p2: {Bob 25 false} // 4. 構造体リテラル (名前指定) の使用: // これは最も一般的で推奨される方法です。可読性が高く、フィールドの順序変更にも強いです。 p3 := Person{ Name: "Charlie", Age: 40, IsAdult: true, } fmt.Println("p3:", p3) // 出力: p3: {Charlie 40 true} // 5. 構造体へのポインタの作成: p4 := &Person{Name: "Diana", Age: 22, IsAdult: true} fmt.Println("p4 (pointer):", p4) // 出力: p4 (pointer): &{Diana 22 true} fmt.Println("p4.Name:", p4.Name) // Goは構造体フィールドのポインタを自動的に逆参照します fmt.Println("(*p4).Name:", (*p4).Name) // 明示的な逆参照も機能します }
フィールドへのアクセスと変更
構造体(または構造体へのポインタ)のフィールドへのアクセスは、ドット .
演算子で行います。変更は単純な代入で行えます。
package main import "fmt" type Car struct { Make string Model string Year int Color string } func main() { myCar := Car{ Make: "Toyota", Model: "Camry", Year: 2020, Color: "Silver", } fmt.Println("My car:", myCar) fmt.Println("Make:", myCar.Make) fmt.Println("Year:", myCar.Year) // フィールドの変更 myCar.Color = "Blue" fmt.Println("New color:", myCar.Color) // Goでは構造体は値型です。 // 一方の構造体をもう一方に代入すると、コピーが作成されます。 anotherCar := myCar // `anotherCar` は `myCar` の独立したコピーです anotherCar.Year = 2023 fmt.Println("My car year:", myCar.Year) // 出力: My car year: 2020 fmt.Println("Another car year:", anotherCar.Year) // 出力: Another car year: 2023 // 基本データ(アドレス)を共有したい場合は、ポインタを使用します。 carPtr := &myCar carPtr.Year = 2024 // これは元の`myCar`を変更します fmt.Println("My car year (after ptr update):", myCar.Year) // 出力: My car year (after ptr update): 2024 }
匿名フィールド(埋め込みフィールド)
Goは「匿名フィールド」または「埋め込みフィールド」と呼ばれる強力な機能を提供します。フィールドに名前を付ける代わりに、型を指定するだけです。これにより、匿名型のフィールドが自動的に包含構造体に埋め込まれ、トップレベルに昇格します。このメカニズムはGoが継承よりもコンポジションを重視する手段であり、「型昇格」の一形態を提供します。
package main import "fmt" type Engine struct { Type string Horsepower int } type Wheels struct { Count int Size int } // Car (匿名フィールド付き) type ModernCar struct { Make string Model string Engine // Engine型の匿名フィールド Wheels // Wheels型の匿名フィールド Price float64 } func main() { myModernCar := ModernCar{ Make: "Tesla", Model: "Model 3", Engine: Engine{ // 埋め込みのEngineフィールドを初期化 Type: "Electric", Horsepower: 450, }, Wheels: Wheels{ // 埋め込みのWheelsフィールドを初期化 Count: 4, Size: 19, }, Price: 55000.00, } fmt.Println("My modern car:", myModernCar) // 埋め込みフィールドに直接アクセス: fmt.Println("Engine Type:", myModernCar.Type) // Engine.Typeに直接アクセス fmt.Println("Engine HP:", myModernCar.Horsepower) // Engine.Horsepowerに直接アクセス fmt.Println("Wheel Count:", myModernCar.Count) // Wheels.Countに直接アクセス fmt.Println("Wheel Size:", myModernCar.Size) // Wheels.Sizeに直接アクセス // 必要であれば、元の複合型を通じてアクセスすることもできます: fmt.Println("Full Engine:", myModernCar.Engine) fmt.Println("Full Wheels:", myModernCar.Wheels) // 埋め込み型間、または埋め込み型と包含構造体の間でフィールド名の衝突が発生した場合、 // 埋め込み構造体に直接宣言されたフィールドが優先されます。 // 2つの埋め込み型間で衝突が発生した場合、フィールドを明示的に指定する必要があります。 type Dimensions struct { Width float64 Height float64 } type SpecificCar struct { Make string Dimensions // 埋め込み構造体 Weight float64 } type Garage struct { Name string Dimensions // 埋め込み構造体、SpecificCar.Dimensions と名前衝突 Location string } // 例1: 埋め込みフィールドへのアクセス sc := SpecificCar{ Make: "Sedan", Dimensions: Dimensions{ Width: 1.8, Height: 1.5, }, Weight: 1500, } fmt.Println("SpecificCar Width:", sc.Width) // Dimensions.Widthにアクセス // 例2: 名前衝突 (この単純な例では直接示されていませんが、Goのルールが適用されます) // `type Car struct { Color string; Vehicle }` と `Vehicle` にも `Color string` があった場合、 // `Car.Color` は `Vehicle.Color` ではなく、`Car` の `Color` を参照します。 // `Vehicle.Color` にアクセスするには、`Car.Vehicle.Color` を使用します。 // `ModernCar` の例では、`Engine` と `Wheels` の両方に `ID` というフィールドがあった場合、 // `myModernCar.Engine.ID` および `myModernCar.Wheels.ID` としてアクセスする必要があります。 }
匿名フィールドは、インターフェースのコンポジション(インターフェースの埋め込み)や、再利用可能なコンポーネントの構築に特に強力です。
構造体ネスト
構造体ネストとは、ある構造体を別の構造体内の名前付きフィールドとして含めるプラクティスを指します。匿名フィールドのように、埋め込み型フィールドが昇格されるのとは異なり、名前付きネストでは、名前付き内部構造体フィールドを通じてフィールドを明示的にアクセスします。これは複雑なデータを整理し、名前の衝突を回避するのに役立ちます。
package main import "fmt" type Manufacturer struct { Name string Country string } type EngineDetails struct { FuelType string Cylinders int Horsepower int } type Vehicle struct { ID string Manufacturer Manufacturer // Manufacturerはネストされた構造体です Engine EngineDetails // EngineDetailsはネストされた構造体です Price float64 } func main() { // ネストされた構造体を持つVehicleインスタンスの作成 myVehicle := Vehicle{ ID: "V1001", Manufacturer: Manufacturer{ // ネストされたManufacturer構造体を初期化 Name: "Honda", Country: "Japan", }, Engine: EngineDetails{ // ネストされたEngineDetails構造体を初期化 FuelType: "Gasoline", Cylinders: 4, Horsepower: 150, }, Price: 25000.00, } fmt.Println("Vehicle ID:", myVehicle.ID) fmt.Println("Manufacturer Name:", myVehicle.Manufacturer.Name) // ネストされたフィールドへのアクセス fmt.Println("Manufacturer Country:", myVehicle.Manufacturer.Country) // ネストされたフィールドへのアクセス fmt.Println("Engine Fuel Type:", myVehicle.Engine.FuelType) // ネストされたフィールドへのアクセス fmt.Println("Engine Horsepower:", myVehicle.Engine.Horsepower) // ネストされたフィールドへのアクセス // ネストされたフィールドの変更 myVehicle.Engine.Horsepower = 160 fmt.Println("New Engine Horsepower:", myVehicle.Engine.Horsepower) // 構造体全体を代入することも可能です myVehicle.Manufacturer = Manufacturer{Name: "Toyota", Country: "Japan"} fmt.Println("New Manufacturer Name:", myVehicle.Manufacturer.Name) }
匿名フィールドと名前付きネストの選択
匿名フィールドと名前付きネストの選択は、外部構造体と内部構造体の間にどのような意味的な関係を表現したいかによります。
- 匿名フィールド(埋め込み): 外部構造体が埋め込み構造体の「一種である」または「そのプロパティを持つ」場合、およびフィールドをトップレベルに昇格させたい場合に使用します。これは、より強力で統合された関係を示唆し、コードの再利用やインターフェースの適合によく使用されます。能力や属性を混ぜ合わせるようなものです。
- 名前付きネスト: 外部構造体が、それ自体が構造体である明確なコンポーネントまたは部分を「持つ」場合に使用します。これは明確な包含関係を示唆し、コンポーネントは名前で明示的にアクセスされます。複雑な階層的データ構造のモデリングに最適です。
構造体タグ
Goの構造体は、フィールドに関連付けられた「タグ」を持つこともできます。これらは、JSONやXMLへのシリアライズ/デシリアライズ、またはバリデーションライブラリなどのメタデータ目的でリフレクションによって使用されます。
package main import ( "encoding/json" "fmt" ) type User struct { ID int `json:"id"` // JSONキー名を指定します Username string `json:"username,omitempty"` // JSONキーを指定し、空の場合は省略します Email string `json:"email"` Password string `json:"-"` // `-` タグはJSONシリアライズ中にこのフィールドを無視します CreatedAt string `json:"created_at,string"` // JSONの文字列として扱います } func main() { u := User{ ID: 1, Username: "gopher", Email: "gopher@example.com", Password: "supersecret", // このフィールドはJSONで無視されます CreatedAt: "2023-10-27T10:00:00Z", } // 構造体をJSONにマーシャリングします jsonData, err := json.MarshalIndent(u, "", " ") if err != nil { fmt.Println("Error marshalling JSON:", err) return } fmt.Println("JSON output:\n", string(jsonData)) // 出力: // JSON output: // { // "id": 1, // "username": "gopher", // "email": "gopher@example.com", // "created_at": "2023-10-27T10:00:00Z" // } // JSONを構造体にアンマーシャリングします jsonString := `{"id":2, "username":"anon", "email":"anon@example.com", "password":"abcd", "created_at":"2023-10-28T11:00:00Z"}` var u2 User err = json.Unmarshal([]byte(jsonString), &u2) if err != nil { fmt.Println("Error unmarshalling JSON:", err) return } fmt.Println("\nUnmarshalled User 2:", u2) fmt.Println("User 2 Password (無視されたためゼロ値になります):", u2.Password) // 出力: 空文字列になります }
構造体に対するメソッド
Goにおける構造体の最も強力な側面の一つは、それにメソッドを関連付けられることです。メソッドは、構造体のインスタンス(値レシーバー)または構造体インスタンスへのポインタ(ポインタレシーバー)を引数として持つ関数です。
package main import "fmt" type Rectangle struct { Width float64 Height float64 } // Area は値レシーバーを持つメソッドです。 // これは長方形の面積を計算します。 // r Rectangle はRectangle構造体のコピーが渡されることを意味します。 func (r Rectangle) Area() float64 { return r.Width * r.Height } // Scale はポインタレシーバーを持つメソッドです。 // これは長方形の寸法をスケーリングします。 // *r Rectangle は元のRectangle構造体が変更されることを意味します。 func (r *Rectangle) Scale(factor float64) { r.Width *= factor r.Height *= factor } // Describe も値レシーバーを持つメソッドで、詳細を表示します。 func (r Rectangle) Describe() { fmt.Printf("Rectangle: Width=%.2f, Height=%.2f, Area=%.2f\n", r.Width, r.Height, r.Area()) } func main() { rect1 := Rectangle{Width: 10, Height: 5} rect1.Describe() // 出力: Rectangle: Width=10.00, Height=5.00, Area=50.00 // 値レシーバーを持つメソッドの呼び出しは、元の構造体を変更しません // (Areaは何も変更しませんが、計算するだけです) area := rect1.Area() fmt.Printf("Calculated Area: %.2f\n", area) // Scaleメソッドをポインタレシーバーで呼び出し、元のrect1を変更します rect1.Scale(2.0) // Goは自動的にポインタレシーバーのためにrect1のアドレスを取ります fmt.Println("After scaling:") rect1.Describe() // 出力: Rectangle: Width=20.00, Height=10.00, Area=200.00 // ポインタを明示的に渡すこともできます ptrRect2 := &Rectangle{Width: 3, Height: 4} fmt.Println("\nBefore scaling ptrRect2:") ptrRect2.Describe() ptrRect2.Scale(3.0) // Scaleメソッドを呼び出します fmt.Println("After scaling ptrRect2:") ptrRect2.Describe() // 出力: Rectangle: Width=9.00, Height=12.00, Area=108.00 }
結論
Goの構造体は非常に多用途であり、Goにおけるデータモデリングの基本です。単純なデータ集約から複雑なネスト構造、コード再利用のための匿名フィールド埋め込み、メソッドとの統合まで、構造体はカスタムタイプを定義するための堅牢で明確な方法を提供します。それらの定義、初期化、アクセスパターン、そして名前付きネストと匿名フィールド間の微妙な違いを理解することは、慣用的で効率的なGoコードを書く上で不可欠です。構造体タグとメソッドと組み合わせることで、強力で保守可能なアプリケーションのバックボーンを形成します。