この章では、Go言語の構造体(struct)について、基本的な使い方からメソッド定義、タグ、設計思想まで幅広く解説されています。Goらしい構造体設計の考え方を理解するうえで非常に重要な章です。
3.1 構造体の基本的な使い方
- フィールドを持つデータ構造を定義
- 大文字で始まるフィールドはエクスポートされ、外部パッケージからアクセス可能
type User struct {
ID int
Name string
}
3.2 構造体をインスタンス化する3つの方法
3.2 構造体をインスタンス化する3つの方法
Goでは構造体のインスタンスを以下の3通りで生成できます。
① new()
関数(ポインタで返る)
type User struct {
Name string
Age int
}
u := new(User)
u.Name = "Taro"
u.Age = 20
-
*User
型が返る(ゼロ値で初期化された構造体のポインタ)
② var
変数宣言(値として生成)
var u User
u.Name = "Taro"
u.Age = 20
- 値型で生成され、フィールドはゼロ値で初期化
-
&u
とすればポインタとしても扱える
③ 複合リテラル(コンポジットリテラル)
u := User{Name: "Taro", Age: 20} // 値型
up := &User{Name: "Taro", Age: 20} // ポインタ型
- 最も一般的で、初期値の指定が簡潔
- 値型にもポインタ型にもできて柔軟
用途に応じて:
-
ポインタ操作したいとき →
new()
or&構造体{...}
-
単純な構造体生成 →
User{}
やvar
この3パターンを使い分けることで、柔軟で意図の明確な構造体の初期化が可能になります。
3.2.1 ファクトリー関数
func NewUser(id int, name string) *User {
return &User{ID: id, Name: name}
}
- 「New」を最初につけるのが一般的
3.3 構造体にメソッドを定義する
func (u *User) Rename(newName string) {
u.Name = newName
}
- レシーバーを持つ関数としてメソッドを定義
3.3.1 値レシーバーとポインタレシーバーの選択
Goではメソッドのレシーバーに値(Value)とポインタ(Pointer)のどちらを使うかで、挙動と目的が変わります。
値レシーバー(func (u User) Xxx()
)
- コピーされた構造体を受け取る
- 元の構造体には影響を与えない(=イミュータブル)
- 小さい構造体(数フィールド程度)や、読み取り専用処理に適している
func (u User) Display() {
fmt.Println("Name:", u.Name)
}
ポインタレシーバー(func (u *User) Xxx()
)
- 元の構造体を参照する(=ミュータブル)
- フィールドを書き換える処理や、大きな構造体に向いている
- 値コピーを避けられるためパフォーマンスが良い
func (u *User) SetName(name string) {
u.Name = name
}
3.3.2 レシーバーがnilでもメソッドは呼べる
Goでは、ポインタレシーバーのメソッドは、レシーバーがnil
でも呼び出せる設計になっています。
ただし、そのメソッドの中でnilにアクセスするとpanicになります。
例:nilでも呼べるが、注意が必要
type User struct {
Name string
}
func (u *User) Greet() {
if u == nil {
fmt.Println("nil user")
return
}
fmt.Println("Hello,", u.Name)
}
var u *User = nil
u.Greet() // → panicにならず "nil user" と表示される
なぜ nil チェックが一般的か?
- レシーバーが
nil
かどうかはメソッド内でしか判断できない - 多くの場面では、nilならメソッドを呼ばない方が安全
- うっかり
nil
のレシーバーでメソッドを呼び、中で別のフィールドにアクセスしてpanic…という事故が防げる
実務上のベストプラクティス
- 事前にnilチェックする
if user != nil {
user.Greet()
}
- または、メソッド内で明示的に
nil
チェックを入れる(防御的設計)
このように、「nilでもメソッドは呼べる」はGoの柔軟な仕様ですが、呼ぶ側・実装側どちらかで対策しないと予期せぬpanicに繋がるため注意が必要です。
3.3.3 メソッドを関数型として抽出(sqlx.DBとnet/http連携)
構造体に *sqlx.DB
を持たせ、その構造体メソッドで Get()
を実行
構造体とDBアクセスの定義
type User struct {
ID int `db:"id"`
Name string `db:"name"`
}
type Server struct {
DB *sqlx.DB
}
func (s *Server) GetUserHandler(w http.ResponseWriter, r *http.Request) {
var user User
err := s.DB.Get(&user, "SELECT id, name FROM users WHERE id = ?", 1)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
fmt.Fprintf(w, "User: %s (ID: %d)", user.Name, user.ID)
}
3.3.4 クロージャを使った再現
Goではクロージャ(関数内で定義された関数)を使って、
メソッドのような挙動を関数として再現することができます。
例:ユーザー名を更新する関数をクロージャで定義
type User struct {
Name string
}
func main() {
user := &User{Name: "Taro"}
// クロージャ:userを囲い込んで関数化
rename := func(newName string) {
user.Name = newName
}
rename("Jiro")
fmt.Println(user.Name) // → "Jiro"
}
🔍 ポイント
-
rename
はuser
という変数を外からキャプチャして保持している -
func(string)
の形をしているが、内部状態(user)に作用できる - これはレシーバー付きメソッドとほぼ同じような振る舞いを、関数単体で実現する方法
このように、構造体や外部状態に作用する小さな処理を手軽に関数化したい場合、
クロージャは非常に便利な手段となります。
3.3.5 ジェネリクスとメソッド
Go 1.18 以降では、型パラメータ(ジェネリクス)付きの構造体にも、通常の構造体と同様にメソッドを定義できます。
例:数値型に対応した汎用ラッパー構造体
package main
import (
"fmt"
)
// 数値をラップするジェネリクス構造体
type Wrapper[T int | float64] struct {
Value T
}
// メソッド:2倍にして返す
func (w Wrapper[T]) Double() T {
return w.Value * 2
}
利用例:
func main() {
i := Wrapper[int]{Value: 10}
f := Wrapper[float64]{Value: 3.4}
fmt.Println(i.Double()) // 20
fmt.Println(f.Double()) // 6.8
}
ポイント
-
Wrapper[T]
のように、型パラメータ付き構造体に対してもメソッドが定義可能 - 通常の構造体と同じ感覚で扱えるが、型ごとの動作を1つの定義に集約できるのが強み
Goのジェネリクスは、メソッド定義・インターフェース実装・制約付きの操作にも対応しているため、 再利用性の高い抽象的なコードが安全に書けるようになっています。
3.4 構造体の埋め込みで共通部分を使いまわす
- 匿名フィールドにより、埋め込まれた型のメソッドを外部から呼び出せる
type Address struct {
City string
}
type Employee struct {
Name string
Address
}
3.5 タグを使って構造体にメタデータを埋め込む
3.5.1 タグの記法
type User struct {
ID int `json:"id"`
Name string `json:"name"`
}
3.5.2 データの書き込み(reflect を使用)
reflect
を使って構造体のフィールド情報を動的に取得し、簡易的に JSON 風の文字列に変換する例です。
package main
import (
"fmt"
"reflect"
)
type User struct {
ID int
Name string
}
func main() {
user := User{ID: 1, Name: "Taro"}
v := reflect.ValueOf(user)
t := reflect.TypeOf(user)
fmt.Print("{")
for i := 0; i < v.NumField(); i++ {
if i > 0 {
fmt.Print(", ")
}
fmt.Printf(`"%s":`, t.Field(i).Name)
fmt.Printf(`"%v"`, v.Field(i).Interface())
}
fmt.Println("}")
}
3.5.3 データの読み込み(reflect を使用)
文字列から map[string]string
に分解して、reflect
を使って構造体に動的に値を設定する簡易例です。
package main
import (
"fmt"
"reflect"
)
type User struct {
ID int
Name string
}
func main() {
data := map[string]string{
"ID": "1",
"Name": "Taro",
}
var user User
v := reflect.ValueOf(&user).Elem()
for key, val := range data {
f := v.FieldByName(key)
if f.IsValid() && f.CanSet() {
switch f.Kind() {
case reflect.Int:
var i int
fmt.Sscanf(val, "%d", &i)
f.SetInt(int64(i))
case reflect.String:
f.SetString(val)
}
}
}
fmt.Printf("%+v\n", user) // {ID:1 Name:Taro}
}
補足
- 通常は
encoding/json
パッケージを使えば簡単に済む処理ですが、reflect
を使うことで「動的に型やフィールドを扱う処理」を実現できます。 - 実際の用途では、カスタムデシリアライズや汎用フォームバインド処理などで使われることがあります。
3.6 構造体を設計するポイント
3.6.1 ポインタ型で扱うケース
構造体の内部に スライス、map、ポインターなどの参照型フィールドを持つ場合、
その構造体は基本的に Struct
(ポインタ型)として扱うように設計します。
理由と利点
-
フィールドの変更が正しく反映される
値型で扱うと構造体自体がコピーされ、参照型フィールドは共有されても構造体の更新が意図通りに働かないことがあります。
3.6.2 値として扱える場合
- 小さくてイミュータブルなデータ
3.6.3 ミュータブル vs イミュータブル
Goでは、構造体の扱いをミュータブル(変更可能)にするか、イミュータブル(変更不可)にするかは設計上の選択です。
エンティティはミュータブルに設計する
エンティティ(User, Order などドメイン上の主要構造体)は、
「状態が変化する」ことが前提のため、ミュータブル(ポインタ型+更新メソッド)にするのが自然です。
3.6.4 ゼロ値の動作を保証する
- 「ゼロ値でも動く」は便利だが、すべてに適用すべきとは限らない
- 外部依存や初期化が必要な構造体は、ファクトリー関数での生成に限定する方が安全で明確
- ファクトリー関数を使うことで、構造体の不変条件や依存注入を強制できる
3.6.5 実装方法の選択ポイント
- ポインターで扱うのが前提
- ミュータブルなAPIセットを提供
- 特定のファクトリー関数のみ動作(ゼロ値動作を保証しない)
3.7 空の構造体を使ってゴルーチン間での通知を行う
Goでは struct{}
(空の構造体)を使うことで、「何かが起きた」ことだけを伝える最小限の通知が可能です。
struct{}
はフィールドを持たず、サイズも0バイトのため、
通知専用のチャンネルやフラグ用途として非常に効率的に使えます。
基本的な使い方(ゴルーチン間の完了通知)
done := make(chan struct{})
go func() {
// 処理
done <- struct{}{} // 完了を通知
}()
<-done // 処理完了を待つ
主な用途
- ゴルーチン間の「完了通知」に使う(
chan struct{}
) - 「何かが起きた」ことだけを伝える用途に最適
- map[struct{}]bool にして、他言語における「集合(Set)」のように使う
- 状態を持たないメソッド集として使う
3.8 構造体のメモリ割り当てを高速化する
構造体のインスタンスを大量に生成・破棄する場面では、
sync.Pool
を使って使いまわすことで、メモリ割り当てのオーバーヘッドを削減できます。
sync.Pool
の活用
- 使い終わった構造体をプールに戻し、再利用
- 毎回
new()
で確保せずに済むため、GC負荷を大幅に軽減可能
例:User構造体を sync.Pool
で管理
package main
import (
"fmt"
"sync"
)
type User struct {
ID int
Name string
}
var userPool = sync.Pool{
New: func() any {
return &User{}
},
}
func main() {
// プールから取得(なければ New が呼ばれる)
u := userPool.Get().(*User)
u.ID = 1
u.Name = "Taro"
fmt.Printf("User: %+v\n", u)
// 使用後はプールに戻す
userPool.Put(u)
}
利点
- 構造体の割り当てコストが大きいケース(例:リクエストごとの一時データ)で有効
-
sync.Pool
はGCサイクル間で生存しているオブジェクトを再利用するため、
短命オブジェクトのGC対象を減らせる
このように sync.Pool
を使うと、構造体の生成コスト削減とメモリ効率改善が同時に実現できます。
3.9 構造体とオブジェクト指向の違いを知る
3.9.1 構造体の用途
- ライブラリの機能を呼び出すメソッドを集約したインスタンス
- インターフェイス経由で他から呼び出されるエントリーポイント
- データセットを持ち運ぶためのバリューオブジェクト
- タプルと呼ばれるようなデータの塊
3.9.2 埋め込みは継承ではない
Goの構造体埋め込みは継承ではなく、あくまで「フィールドとしてぶら下がっている」だけの関係です。
- 埋め込んだ構造体は 親ではなく子(部品)
- 同名のメソッドを定義してもオーバーライドはできない
例:埋め込みでメソッドは上書きできない
type Base struct{}
func (b Base) Hello() {
fmt.Println("Hello from Base")
}
type Child struct {
Base
}
func (c Child) Hello() {
fmt.Println("Hello from Child")
}
func main() {
c := Child{}
c.Hello() // → Hello from Child
c.Base.Hello() // → Hello from Base
}
ポイント
-
Base.Hello()
は明示的に呼べてしまう → 隠れていない -
Child.Hello()
は別定義であり、継承やオーバーライドとは異なる - 委譲的な使い方として、メソッドの呼び出し先を切り替える目的に使われる
このように、Goの埋め込みは「継承」ではなく、「部品を組み合わせる」という考え方に基づいています。
3.9.3 ストラテジーパターン的設計
Goでは、共通の処理の一部を差し替えたい場合、
テンプレートメソッドパターンのような継承ベースの設計ではなく、ストラテジーパターンを取るのが一般的です。
ストラテジーパターンとは?
- 振る舞い(戦略)を構造体または関数として明示的に分離
- 必要な実装を依存注入(DI)で差し替えることで、柔軟に処理を切り替える
なぜテンプレートメソッドではないのか?
- Goには継承がないため、「一部の処理だけを上書きする」というテンプレートメソッドパターンは適用できない
- 埋め込みで似たことはできるが、オーバーライドできないため制限がある
3.9.4 あえてオーバーライドを実装する
Goには継承によるオーバーライドの仕組みはありませんが、
1番目の引数としてインスタンスのポインターを渡し、インターフェイスで受けることで、他のオブジェクト思考言語と同等のオーバーライドが実現できる
✅ まとめ
Goにおける構造体は、OOPのように見えても実は委譲・明示的設計が本質です。
柔軟かつ安全な構造体の設計には以下のポイントを意識しましょう:
- 値 vs ポインタ
- 構造体の埋め込み活用
- タグによるメタ情報管理
- ゼロ値の安全設計
- オブジェクト指向とは異なるGoらしい抽象化