構造体
構造体
記入用紙を想像してください。氏名、年齢、電話番号、住所 — これらの情報はお互いに関連しており、まとめて一人の人間を表します。Goでは、struct がその「記入用紙」にあたります。複数の異なる型のフィールドを組み合わせて、カスタムの複合型を作成します。マップと比較すると、構造体のフィールドはコンパイル時に決定されるため、型安全で意味が明確です。構造体はGoにおけるデータ整理の中心的な方法です。
1. コアコンセプト
| コンセプト | 説明 |
|---|---|
| 定義 | type StructName struct { ... }、フィールド名は型の前に記述 |
| 初期化 | リテラル S{f1: v1, f2: v2}、new(S) はポインタを返す、&S{} |
| フィールドアクセス | ドット記法 s.Field、ポインタでもドット記法 p.Field(-> は不要) |
| 値型 | 構造体は値型。代入や関数引数で構造体全体がコピーされる |
| ポインタレシーバ | &s でポインタを取得。ポインタ経由で構造体を変更すると元の値に影響する |
| 無名フィールド | フィールド名なしで型のみ記述。型名がフィールド名となり、「合成」を実現 |
| ネストされた構造体 | 構造体のフィールド自体が構造体になることができ、多階層のネストをサポート |
| 構造体タグ | バッククォートで囲まれたメタデータ。JSON/データベースのマッピングによく使用 |
*S を渡してください。
2. 基本構文/使い方
構造体の定義
// Person 構造体を定義
type Person struct {
Name string
Age int
City string
}
初期化方法
// 方法1: フィールド名: 値(推奨、可読性が高く、順序は問わない)
p1 := Person{Name: "Alice", Age: 25, City: "Beijing"}
// 方法2: 順序で代入(フィールドが多い場合は非推奨、エラーが起きやすい)
p2 := Person{"Bob", 30, "Shanghai"}
// 方法3: 先に宣言してから代入
var p3 Person
p3.Name = "Charlie"
p3.Age = 28
p3.City = "Guangzhou"
// 方法4: new で作成(ポインタを返す)
p4 := new(Person) // *Person
p4.Name = "Diana"
// 方法5: アドレスを取得(ポインタを返す)
p5 := &Person{Name: "Eve", Age: 22, City: "Shenzhen"}
var p Person で宣言された構造体は、すべてのフィールドがそれぞれの型のゼロ値で初期化されます(string → ""、int → 0、bool → false)。
フィールドへのアクセスと変更
p := Person{Name: "Alice", Age: 25}
// フィールドの読み取り
fmt.Println(p.Name) // Alice
// フィールドの変更
p.Age = 26
fmt.Println(p.Age) // 26
p->Name は不要です。
構造体ポインタ
p := &Person{Name: "Alice", Age: 25}
// Goが自動的にデリファレンスするため、直接ドット記法を使用
fmt.Println(p.Name) // Alice
// ポインタ経由で元の値を変更
p.Age = 30
-> 演算子がありません。p が値であってもポインタであって、常に p.Field でフィールドにアクセスします。
3. サンプルコード
例:基本的な使い方(難易度⭐)
Book 構造体を定義し、インスタンスを作成して情報を表示します。
package main
import "fmt"
// Book は書籍の構造体を定義
type Book struct {
Title string
Author string
Pages int
Price float64
}
func main() {
// リテラルで作成
book1 := Book{
Title: "The Go Programming Language",
Author: "Alan Donovan",
Pages: 350,
Price: 59.9,
}
// 構造体を表示
fmt.Println(book1)
// 個別のフィールドにアクセス
fmt.Printf("Title: %s\n", book1.Title)
fmt.Printf("Author: %s\n", book1.Author)
fmt.Printf("Price: %.1f\n", book1.Price)
// フィールドを変更
book1.Price = 49.9
fmt.Printf("New price: %.1f\n", book1.Price)
// 順序で作成(非推奨)
book2 := Book{"Python Crash Course", "Eric Matthes", 280, 45.0}
fmt.Println(book2)
}
出力:
{The Go Programming Language Alan Donovan 350 59.9}
Title: The Go Programming Language
Author: Alan Donovan
Price: 59.9
New price: 49.9
{Python Crash Course Eric Matthes 280 45}
例:応用的な使い方(難易度⭐⭐)
ポインタ、ネストされた構造体、無名フィールドの使用例です。
package main
import "fmt"
// Address 構造体
type Address struct {
City string
Street string
ZipCode string
}
// Employee 構造体(無名フィールド Address を使用)
type Employee struct {
Name string
Age int
Address // 無名フィールド:型名がフィールド名になる
Salary float64
}
func main() {
// ========== ネストされた構造体の初期化 ==========
emp := Employee{
Name: "Alice",
Age: 30,
Salary: 15000,
Address: Address{
City: "Beijing",
Street: "1 Zhongguancun Street",
ZipCode: "100080",
},
}
fmt.Printf("Employee: %s\n", emp.Name)
fmt.Printf("City: %s\n", emp.Address.City) // 明示的なアクセス
// ========== 無名フィールドの「プロモーション」アクセス ==========
// 無名フィールドは型名で直接アクセスできる
fmt.Printf("Street: %s\n", emp.Street) // emp.Address.Street と同じ
fmt.Printf("ZipCode: %s\n", emp.ZipCode) // emp.Address.ZipCode と同じ
// ========== ポインタ操作 ==========
empPtr := &emp
// ポインタ経由で変更(Goが自動的にデリファレンス)
empPtr.Age = 31
empPtr.Salary = 18000
empPtr.City = "Shanghai" // ポインタ経由で無名フィールドを変更
fmt.Printf("\n変更後: %+v\n", *empPtr)
// ========== 値型とポインタ型 ==========
fmt.Println("\n--- 値型とポインタ型 ---")
// 値型:代入はコピー
emp2 := emp
emp2.Name = "Bob"
fmt.Printf("emp.Name: %s\n", emp.Name) // まだ Alice
fmt.Printf("emp2.Name: %s\n", emp2.Name) // Bob
// ポインタ型:代入は共有
emp3 := empPtr
emp3.Name = "Charlie"
fmt.Printf("empPtr.Name: %s\n", empPtr.Name) // Charlie(変更された)
}
出力:
Employee: Alice
City: Beijing
Street: 1 Zhongguancun Street
ZipCode: 100080
変更後: {Name:Alice Age:31 Address:{City:Shanghai Street:1 Zhongguancun Street ZipCode:100080} Salary:18000}
--- 値型とポインタ型 ---
emp.Name: Alice
emp2.Name: Bob
empPtr.Name: Charlie
例:総合的な応用(難易度⭐⭐⭐)
構造体タグを使ったJSONシリアライズ/デシリアライズと、学生管理システムの構築を行います。
package main
import (
"encoding/json"
"fmt"
"sort"
"strings"
)
// Student 構造体(JSONタグ付き)
type Student struct {
ID int `json:"id"`
Name string `json:"name"`
Age int `json:"age"`
Score float64 `json:"score"`
Tags []string `json:"tags,omitempty"` // omitempty: 空の場合は省略
secret string // アンエクスポートフィールド、JSONからはアクセス不可
}
// ClassRoom(ネストされた構造体)
type ClassRoom struct {
ClassName string `json:"class_name"`
Students []Student `json:"students"`
}
// AddStudent は学生を追加する
func (c *ClassRoom) AddStudent(s Student) {
c.Students = append(c.Students, s)
}
// GetTopN は成績上位N名の学生を取得する
func (c *ClassRoom) GetTopN(n int) []Student {
// 元のデータを変更しないようコピーを作成
sorted := make([]Student, len(c.Students))
copy(sorted, c.Students)
// 成績の降順でソート
sort.Slice(sorted, func(i, j int) bool {
return sorted[i].Score > sorted[j].Score
})
if n > len(sorted) {
n = len(sorted)
}
return sorted[:n]
}
// Average は平均点を計算する
func (c *ClassRoom) Average() float64 {
if len(c.Students) == 0 {
return 0
}
total := 0.0
for _, s := range c.Students {
total += s.Score
}
return total / float64(len(c.Students))
}
// SearchByName は名前で検索する(あいまい一致)
func (c *ClassRoom) SearchByName(keyword string) []Student {
var result []Student
for _, s := range c.Students {
if strings.Contains(s.Name, keyword) {
result = append(result, s)
}
}
return result
}
func main() {
// クラスを作成
class := &ClassRoom{ClassName: "2024年度1組"}
// 学生を追加
students := []Student{
{ID: 1, Name: "Zhang San", Age: 18, Score: 92.5, Tags: []string{"数学が得意", "学級委員"}},
{ID: 2, Name: "Li Si", Age: 19, Score: 88.0, Tags: []string{"スポーツが得意"}},
{ID: 3, Name: "Wang Wu", Age: 18, Score: 95.5},
{ID: 4, Name: "Zhao Liu", Age: 20, Score: 78.0, Tags: []string{"芸術の才能"}},
{ID: 5, Name: "Zhang Wei", Age: 19, Score: 91.0, Tags: []string{"数学が得意", "コンテスト受賞"}},
}
for _, s := range students {
class.AddStudent(s)
}
// ========== JSONシリアライズ ==========
fmt.Println("=== JSONシリアライズ ===")
jsonData, err := json.MarshalIndent(class, "", " ")
if err != nil {
fmt.Println("シリアライズに失敗しました:", err)
} else {
fmt.Println(string(jsonData))
}
// ========== JSONデシリアライズ ==========
fmt.Println("\n=== JSONデシリアライズ ===")
jsonStr := `{"id":6,"name":"Sun Qi","age":17,"score":97.0,"tags":["英語が得意"]}`
var newStudent Student
if err := json.Unmarshal([]byte(jsonStr), &newStudent); err != nil {
fmt.Println("デシリアライズに失敗しました:", err)
} else {
fmt.Printf("新しい学生: %+v\n", newStudent)
}
// ========== 機能デモ ==========
fmt.Println("\n=== クラス情報 ===")
fmt.Printf("クラス: %s、合計%d名\n", class.ClassName, len(class.Students))
fmt.Printf("平均点: %.1f\n", class.Average())
fmt.Println("\n=== 成績上位3名 ===")
top3 := class.GetTopN(3)
for i, s := range top3 {
fmt.Printf(" #%d: %s (%.1f)\n", i+1, s.Name, s.Score)
}
fmt.Println("\n=== 「Zhang」で検索 ===")
results := class.SearchByName("Zhang")
for _, s := range results {
fmt.Printf(" %s (ID: %d, 成績: %.1f)\n", s.Name, s.ID, s.Score)
}
}
出力:
=== JSONシリアライズ ===
{
"class_name": "2024年度1組",
"students": [
{
"id": 1,
"name": "Zhang San",
"age": 18,
"score": 92.5,
"tags": [
"数学が得意",
"学級委員"
]
},
{
"id": 2,
"name": "Li Si",
"age": 19,
"score": 88
},
{
"id": 3,
"name": "Wang Wu",
"age": 18,
"score": 95.5
},
{
"id": 4,
"name": "Zhao Liu",
"age": 20,
"score": 78,
"tags": [
"芸術の才能"
]
},
{
"id": 5,
"name": "Zhang Wei",
"age": 19,
"score": 91,
"tags": [
"数学が得意",
"コンテスト受賞"
]
}
]
}
=== JSONデシリアライズ ===
新しい学生: {ID:6 Name:Sun Qi Age:17 Score:97 Tags:[英語が得意]}
=== クラス情報 ===
クラス: 2024年度1組、合計5名
平均点: 89.0
=== 成績上位3名 ===
#1: Wang Wu (95.5)
#2: Zhang San (92.5)
#3: Zhang Wei (91.0)
=== 「Zhang」で検索 ===
Zhang San (ID: 1, 成績: 92.5)
Zhang Wei (ID: 5, 成績: 91.0)
3. よくある使用場面
場面1:JSONデータのやり取り
Web開発では、構造体がJSONリクエスト/レスポンスを処理する標準的な方法です。タグがJSONフィールド名を制御します。
package main
import (
"encoding/json"
"fmt"
)
// APIResponse は統一されたAPIレスポンス構造体
type APIResponse struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
}
// UserDTO はユーザーデータ転送オブジェクト
type UserDTO struct {
ID int `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
Password string `json:"-"` // "-" はJSONでこのフィールドを無視することを意味する
}
func main() {
// レスポンスを構築
user := UserDTO{
ID: 1,
Username: "alice",
Email: "alice@example.com",
Password: "secret123", // シリアライズされない
}
resp := APIResponse{
Code: 200,
Message: "success",
Data: user,
}
// シリアライズ
jsonBytes, _ := json.MarshalIndent(resp, "", " ")
fmt.Println(string(jsonBytes))
// デシリアライズ
jsonStr := `{"code":404,"message":"User not found"}`
var errResp APIResponse
json.Unmarshal([]byte(jsonStr), &errResp)
fmt.Printf("\nエラーコード: %d、メッセージ: %s\n", errResp.Code, errResp.Message)
}
出力:
{
"code": 200,
"message": "success",
"data": {
"id": 1,
"username": "alice",
"email": "alice@example.com"
}
}
エラーコード: 404、メッセージ: User not found
場面2:オブジェクト指向のシミュレーション(継承より合成)
Goにはクラスや継承がありません。構造体のネスト(合成)を通じてコードの再利用を実現します。
package main
import "fmt"
// Logger はログ機能
type Logger struct {
Prefix string
}
// Log はログメッセージを出力する
func (l *Logger) Log(msg string) {
fmt.Printf("[%s] %s\n", l.Prefix, msg)
}
// Cache はキャッシュ機能
type Cache struct {
data map[string]string
}
// Get はキャッシュから取得する
func (c *Cache) Get(key string) (string, bool) {
val, ok := c.data[key]
return val, ok
}
// Set はキャッシュに設定する
func (c *Cache) Set(key, value string) {
if c.data == nil {
c.data = make(map[string]string)
}
c.data[key] = value
}
// UserService は合成により Logger と Cache の機能を再利用する
type UserService struct {
Logger // Logger を埋め込み
Cache // Cache を埋め込み
Name string
}
func main() {
svc := UserService{
Logger: Logger{Prefix: "UserService"},
Name: "User Service",
}
// 埋め込まれたメソッドを直接呼び出し
svc.Log("サービスを開始しました") // Logger からの継承
svc.Set("user:1", "Alice") // Cache からの継承
svc.Log("キャッシュを初期化しました")
if val, ok := svc.Get("user:1"); ok {
svc.Log(fmt.Sprintf("ユーザーを見つけました: %s", val))
}
}
出力:
[UserService] サービスを開始しました
[UserService] キャッシュを初期化しました
[UserService] ユーザーを見つけました: Alice
❓ よくある質問
質問1:構造体は値型ですか?参照型ですか?
構造体は値型です。代入や関数引数で構造体全体がコピーされます。元の値を変更するには、ポインタを渡してください。
func updateAge(p *Person, age int) {
p.Age = age // 元の値を直接変更
}
p := Person{Name: "Alice", Age: 25}
updateAge(&p, 30)
fmt.Println(p.Age) // 30
質問2:無名フィールドとは?ネストされた構造体と何が違うの?
無名フィールドは「合成」を実装するためのものです。内側の型のメソッドとフィールドが外側の型に「プロモーション」されます。
type Address struct {
City string
}
type Person struct {
Name string
Address // 無名フィールド
}
p := Person{Name: "Alice", Address: Address{City: "Beijing"}}
fmt.Println(p.City) // 直接アクセス、p.Address.City と同じ
fmt.Println(p.Address.City) // 明示的なアクセスでも動作
ネストされた構造体に同名のフィールドがある場合は、完全パスを使用して曖昧さを解消する必要があります。
質問3:構造体タグとは?どうやって使うの?
タグはバッククォートで囲まれたメタデータで、JSON、データベースORM、その他のライブラリの動作を制御するためによく使用されます。
type User struct {
ID int `json:"id" db:"user_id"`
Name string `json:"name" db:"user_name"`
Password string `json:"-" db:"password"` // JSONでは無視
Email string `json:"email,omitempty" db:"email"` // JSONで空の場合に省略
}
よく使われるタグ:
json:"fieldName"— JSONフィールド名を制御json:"-"— シリアライズ時にフィールドを無視json:",omitempty"— ゼロ値の場合に省略db:"column_name"— データベースフィールドのマッピング(ORMのサポートが必要)
質問4:構造体は比較できますか?
フィールドの型に依存します。すべてのフィールドが比較可能な型(int、string、bool、配列など)であれば、== と != で比較できます。スライス、マップ、関数などの比較不可能なフィールドが含まれている場合は、直接比較できません。
type Point struct { X, Y int }
p1 := Point{1, 2}
p2 := Point{1, 2}
fmt.Println(p1 == p2) // true
// スライスを含む構造体は == を使用できない
type Data struct { Items []int }
// d1 == d2 // コンパイルエラー!
📖 まとめ
- struct はGoのカスタム複合型で、複数のフィールドを一つの単位にまとめます
- 構造体は値型です。代入や関数引数で構造体全体がコピーされます。元の値を変更するにはポインタ
*Sを渡してください - フィールドへのアクセスにはドット記法
s.Fieldを使用します。ポインタでもp.Fieldで、->は不要です - 無名フィールド(型名のみ、フィールド名なし)は合成を実装します。内側のフィールドが外側に「プロモーション」されます
- ネストされた構造体は多階層のデータ整理をサポートします
- 構造体タグはバッククォートで定義され、JSON/ORMのフィールドマッピングによく使用されます
json:"-"はフィールドを無視し、json:",omitempty"はゼロ値を省略します- 構造体が比較可能かどうかは、フィールドの型に依存します
📝 演習
演習1(⭐)
Width と Height フィールドを持つ Rectangle 構造体を定義してください。以下を実装してください:
- Rectangleインスタンスを作成して表示する
Area()メソッドで面積を計算するPerimeter()メソッドで周囲の長さを計算するIsSquare()メソッドで正方形かどうかを判定する
演習2(⭐⭐)
Owner(string)と Balance(float64)フィールドを持つ BankAccount 構造体を定義してください。以下を実装してください:
Deposit(amount)で入金Withdraw(amount)で出金(残高不足の場合は通知)Transfer(other *BankAccount, amount)で振込- JSONタグを使って口座情報をシリアライズする(
json:"-"で残高を隠す)
演習3(⭐⭐⭐)
商品在庫管理システムを実装してください:
Product構造体(ID、Name、Price、Stock、Tags)を定義Inventory構造体(ShopName、Productsスライス)を定義- メソッドを実装:
AddProduct、RemoveProduct(id)、FindByTag(tag)、TotalValue()(在庫総額) - JSONシリアライズを実装(要件:Priceは小数点以下2桁、Stockが0の場合は省略)
- インスタンスを作成し、商品を追加、タグで検索、総額を計算、JSONとして出力



