Go言語において、データ構造を設計する際に最も重要な要素となるのが「struct(構造体)」です。
オブジェクト指向言語におけるクラスに近い役割を持ちながら、継承を持たずシンプルに設計されている点がGo言語の大きな特徴です。
効率的なプログラムを記述するためには、構造体の基礎から応用、そしてメモリ効率に至るまでを深く理解する必要があります。
本記事では、初心者から中級者の方を対象に、構造体の定義方法からメソッドの活用、実戦的なタグの使い方までを詳しく解説します。
構造体の基本:定義と初期化
構造体は、異なる型を持つデータを一つの単位としてまとめることができる「ユーザー定義型」です。
Go言語では type キーワードと struct キーワードを組み合わせて定義を行います。
package main
import "fmt"
// User構造体の定義
type User struct {
ID int
Name string
Email string
Age int
}
func main() {
// フィールド名を指定して初期化
u := User{
ID: 1,
Name: "田中太郎",
Email: "tanaka@example.com",
Age: 30,
}
fmt.Printf("%+v\n", u)
}
{ID:1 Name:田中太郎 Email:tanaka@example.com Age:30}
ゼロ値による初期化
構造体は、値を明示的に代入しなくても、各フィールドの型の「ゼロ値」で初期化されます。
例えば、int 型であれば 0、string 型であれば "" (空文字) が自動的に割り当てられます。
不完全なデータ状態を防ぐため、初期化時にはリテラルを用いるのが一般的です。
フィールドへのアクセス
構造体のフィールドには、ドット . 演算子を用いてアクセスします。
ポインタ型の構造体であっても、Go言語のコンパイラが自動的にデリファレンスを行うため、同じドット演算子でアクセス可能です。
埋め込み(Embedding)による構成
Go言語にはクラスの継承はありませんが、「埋め込み」という仕組みを使って機能を再利用します。
構造体の中に別の構造体をフィールド名なしで記述することで、内側の構造体のフィールドやメソッドに直接アクセスできるようになります。
匿名フィールドの活用
以下の例では、BaseModel を Article 構造体に埋め込んでいます。
type BaseModel struct {
ID int
CreatedAt string
}
type Article struct {
BaseModel // 埋め込み(名前なしフィールド)
Title string
Content string
}
func main() {
a := Article{
BaseModel: BaseModel{ID: 101, CreatedAt: "2026-01-01"},
Title: "Goの構造体について",
Content: "記事の内容です。",
}
// BaseModelのフィールドに直接アクセス可能
fmt.Println("ID:", a.ID)
fmt.Println("Title:", a.Title)
}
ID: 101
Title: Goの構造体について
この仕組みにより、コードの重複を避けつつ、柔軟なデータモデリングが可能になります。
構造体メソッドの定義
構造体には「メソッド」を関連付けることができます。
メソッドを定義するには、関数名の前に「レシーバ」を指定します。
値レシーバとポインタレシーバ
レシーバには「値レシーバ」と「ポインタレシーバ」の2種類が存在します。
用途に応じてこれらを使い分けることが、Goプログラミングにおいて非常に重要です。
| 種類 | 記法 | 特徴 |
|---|---|---|
| 値レシーバ | (u User) | 構造体のコピーが渡される。元の値は変更できない。 |
| ポインタレシーバ | (u *User) | 構造体の参照が渡される。元の値を変更できる。メモリ効率が良い。 |
基本的には、構造体の状態を変更する場合や、構造体のサイズが大きい場合はポインタレシーバを選択します。
type Counter struct {
Count int
}
// 値レシーバ(コピーを操作するため、元のCountは増えない)
func (c Counter) IncrementValue() {
c.Count++
}
// ポインタレシーバ(参照を操作するため、元のCountが増える)
func (c *Counter) IncrementPointer() {
c.Count++
}
func main() {
c := Counter{Count: 0}
c.IncrementValue()
fmt.Println("Value Receiver:", c.Count)
c.IncrementPointer()
fmt.Println("Pointer Receiver:", c.Count)
}
Value Receiver: 0
Pointer Receiver: 1
構造体タグ(Struct Tags)の活用
Go言語の構造体には「タグ」と呼ばれるメタ情報を付与することができます。
これは、JSONのシリアライズやデータベースのORマッパー、バリデーションライブラリなどで頻繁に利用されます。
JSONマッピングの例
API開発において、構造体のフィールド名をJSONのキー名と対応させる際にタグを使用します。
import (
"encoding/json"
"fmt"
)
type Product struct {
Name string `json:"product_name"`
Price int `json:"price"`
Stock int `json:"-"` // JSON出力から除外
}
func main() {
p := Product{Name: "ノートPC", Price: 150000, Stock: 10}
data, _ := json.Marshal(p)
fmt.Println(string(data))
}
{"product_name":"ノートPC","price":150000}
タグを適切に設定することで、外部システムとのデータ連携が非常にスムーズになります。
コンストラクタ関数のパターン
Go言語には言語仕様としてのコンストラクタは存在しませんが、慣習として New構造体名 という名前の関数を作成します。
これにより、複雑な初期化ロジックをカプセル化し、不正な状態の構造体生成を防ぐことができます。
type Database struct {
DSN string
}
// NewDatabase はDatabase構造体のコンストラクタ
func NewDatabase(dsn string) (*Database, error) {
if dsn == "" {
return nil, fmt.Errorf("DSNが空です")
}
return &Database{DSN: dsn}, nil
}
特にポインタを返すコンストラクタを作成することで、共有可能なインスタンスを効率よく生成できます。
構造体のメモリ効率とアライメント
上級者向けの知識として、構造体のフィールドを並べる順序がメモリ消費量に影響を与えることを知っておく必要があります。
Goのコンパイラは、CPUが効率よくアクセスできるように「メモリ配置のアライメント」を行います。
フィールド順序によるサイズの変化
例えば、bool 型(1バイト)と int64 型(8バイト)を交互に並べると、パディング(空きスペース)が発生します。
サイズの大きいフィールドを先に定義することで、パディングを最小限に抑えることが可能です。
大量のデータを構造体のスライスとして保持する場合、この最適化がパフォーマンスに大きく寄与します。
構造体の比較と注意点
構造体は、含まれるすべてのフィールドが比較可能(等価演算子 == が使える型)であれば、構造体同士を直接比較できます。
しかし、フィールドにスライスやマップ、関数などの「比較不可能な型」が含まれている場合、その構造体は == で比較できず、コンパイルエラーとなります。
そのような場合は、reflect.DeepEqual を使用するか、手動で各フィールドをチェックするメソッドを実装する必要があります。
まとめ
Go言語の struct(構造体)は、単なるデータの集まりではなく、メソッドや埋め込み、タグを活用することで非常に強力なツールとなります。
クラス継承がないからこそ、コンポジション(合成)を中心としたシンプルな設計を心がけることが、メンテナンス性の高いコードへの近道です。
まずは基本となる定義と初期化をマスターし、次にポインタレシーバと値レシーバの使い分けを意識してみてください。
そして実務では、JSONタグやコンストラクタ関数を適切に組み合わせて、堅牢なアプリケーションを構築していきましょう。
構造体の深い理解は、Go言語のパフォーマンスを最大限に引き出すための第一歩です。
