Go言語入門:言語仕様 -Vol.24-

スポンサーリンク
Go言語入門:言語仕様 -Vol.24- ノウハウ
Go言語入門:言語仕様 -Vol.24-
この記事は約21分で読めます。
よっしー
よっしー

こんにちは。よっしーです(^^)

本日は、Go言語の言語仕様について解説しています。

スポンサーリンク

背景

Go言語を学び始めて、公式の「The Go Programming Language Specification(言語仕様書)」を開いてみたものの、「英語で書かれていて読むのが大変…」「専門用語ばかりで何を言っているのかわからない…」と感じたことはありませんか? 実は、多くのGo初心者が同じ壁にぶつかっています。

言語仕様書は、Go言語の「正式な取扱説明書」のような存在です。プログラミング言語がどのように動くのか、どんなルールで書くべきなのかが詳しく書かれていますが、その分、初めて読む人には難しく感じられるのも事実です。

そこでこの記事では、言語仕様書の導入部分を丁寧な日本語訳とともに、初心者の方でも理解しやすい補足説明を加えてお届けします。「強く型付けされている」「ガベージコレクション」「並行プログラミング」といった専門用語も、具体例を交えながらわかりやすく解説していきます。

言語仕様書は難しそうに見えますが、一つひとつの概念を丁寧に読み解いていけば、必ず理解できます。一緒に、Go言語の基礎をしっかり学んでいきましょう!

Struct types(構造体型)

構造体は、フィールドと呼ばれる名前付き要素のシーケンスであり、各要素は名前と型を持ちます。フィールド名は明示的に(IdentifierList)または暗黙的に(EmbeddedField)指定できます。構造体内では、空白でないフィールド名は一意でなければなりません。

StructType    = "struct" "{" { FieldDecl ";" } "}" .
FieldDecl     = (IdentifierList Type | EmbeddedField) [ Tag ] .
EmbeddedField = [ "*" ] TypeName [ TypeArgs ] .
Tag           = string_lit .
// 空の構造体
struct {}

// 6つのフィールドを持つ構造体
struct {
	x, y int
	u float32
	_ float32  // パディング
	A *[]int
	F func()
}

型を持つが明示的なフィールド名を持たないフィールドは、埋め込みフィールドと呼ばれます。埋め込みフィールドは、型名Tまたは非インターフェース型名へのポインタ*Tとして指定する必要があり、T自体はポインタ型または型パラメータであってはなりません。非修飾の型名がフィールド名として機能します。

// T1, *T2, P.T3, *P.T4の4つの埋め込みフィールドを持つ構造体
struct {
	T1        // フィールド名はT1
	*T2       // フィールド名はT2
	P.T3      // フィールド名はT3
	*P.T4     // フィールド名はT4
	x, y int  // フィールド名はxとy
}

フィールド名は構造体型内で一意でなければならないため、次の宣言は不正です:

struct {
	T     // 埋め込みフィールド*Tおよび*P.Tと競合
	*T    // 埋め込みフィールドTおよび*P.Tと競合
	*P.T  // 埋め込みフィールドTおよび*Tと競合
}

構造体x内の埋め込みフィールドのフィールドまたはメソッドfは、x.fがそのフィールドまたはメソッドfを示す正当なセレクタである場合、昇格されたと呼ばれます。

昇格されたフィールドは、構造体の複合リテラルでフィールド名として使用できないことを除いて、構造体の通常のフィールドのように機能します。

構造体型Sと型名Tが与えられた場合、昇格されたメソッドは次のように構造体のメソッドセットに含まれます:

  • Sが埋め込みフィールドTを含む場合、S*Sの両方のメソッドセットには、レシーバTを持つ昇格されたメソッドが含まれます。*Sのメソッドセットには、レシーバ*Tを持つ昇格されたメソッドも含まれます。
  • Sが埋め込みフィールド*Tを含む場合、S*Sの両方のメソッドセットには、レシーバTまたは*Tを持つ昇格されたメソッドが含まれます。

フィールド宣言の後にはオプションの文字列リテラルタグを続けることができ、これは対応するフィールド宣言のすべてのフィールドの属性になります。空のタグ文字列は、タグがないのと同等です。タグはリフレクションインターフェースを通じて可視化され、構造体の型同一性に関与しますが、それ以外は無視されます。

struct {
	x, y float64 ""  // 空のタグ文字列はタグがないのと同じ
	name string  "any string is permitted as a tag"
	_    [4]byte "ceci n'est pas un champ de structure"
}

// TimeStampプロトコルバッファに対応する構造体
// タグ文字列はプロトコルバッファのフィールド番号を定義
// reflectパッケージで概説された規約に従う
struct {
	microsec  uint64 `protobuf:"1"`
	serverIP6 uint64 `protobuf:"2"`
}

構造体型Tは、それらの含む型が配列または構造体型のみである場合、直接的または間接的に、型Tのフィールド、またはTをコンポーネントとして含む型のフィールドを含むことはできません。

// 無効な構造体型
type (
	T1 struct{ T1 }            // T1がT1のフィールドを含む
	T2 struct{ f [10]T2 }      // T2が配列のコンポーネントとしてT2を含む
	T3 struct{ T4 }            // T3が構造体T4内の配列のコンポーネントとしてT3を含む
	T4 struct{ f [10]T3 }      // T4が配列内の構造体T3のコンポーネントとしてT4を含む
)

// 有効な構造体型
type (
	T5 struct{ f *T5 }         // T5がポインタのコンポーネントとしてT5を含む
	T6 struct{ f func() T6 }   // T6が関数型のコンポーネントとしてT6を含む
	T7 struct{ f [10][]T7 }    // T7が配列内のスライスのコンポーネントとしてT7を含む
)

解説

構造体とは何か?

構造体(struct) は、複数の異なる型のフィールドをまとめたデータ構造です。関連する情報を1つのまとまりとして扱えます。

たとえ話: 構造体は「カード」のようなものです。名刺には「名前」「会社名」「電話番号」「メールアドレス」など、異なる種類の情報が1枚のカードにまとめられています。

package main

import "fmt"

// 構造体の定義
type Person struct {
    Name  string
    Age   int
    Email string
}

func main() {
    // 構造体の作成
    p := Person{
        Name:  "太郎",
        Age:   25,
        Email: "taro@example.com",
    }
    
    fmt.Printf("%+v\n", p)
    // {Name:太郎 Age:25 Email:taro@example.com}
}

1. 構造体の基本

構造体の定義

package main

import "fmt"

// 基本的な構造体
type Point struct {
    X int
    Y int
}

// 同じ型のフィールドはまとめて書ける
type Point2D struct {
    X, Y int
}

// 様々な型を含む構造体
type Person struct {
    Name    string
    Age     int
    Height  float64
    IsAdult bool
}

func main() {
    p1 := Point{X: 10, Y: 20}
    p2 := Point2D{30, 40}
    
    person := Person{
        Name:    "太郎",
        Age:     25,
        Height:  175.5,
        IsAdult: true,
    }
    
    fmt.Println(p1, p2, person)
}

空の構造体

package main

import (
    "fmt"
    "unsafe"
)

func main() {
    // 空の構造体(メモリを消費しない)
    var empty struct{}
    
    fmt.Println("サイズ:", unsafe.Sizeof(empty)) // 0バイト
    
    // セットやシグナルとして使われる
    done := make(chan struct{})
    go func() {
        // 何か処理
        done <- struct{}{}  // 値は意味を持たない
    }()
    
    <-done
    fmt.Println("完了")
}

2. 構造体の初期化

様々な初期化方法

package main

import "fmt"

type Person struct {
    Name string
    Age  int
    City string
}

func main() {
    // 方法1: フィールド名を指定(推奨)
    p1 := Person{
        Name: "太郎",
        Age:  25,
        City: "東京",
    }
    
    // 方法2: 順番に値を指定(非推奨)
    p2 := Person{"花子", 30, "大阪"}
    
    // 方法3: 一部だけ初期化(残りはゼロ値)
    p3 := Person{
        Name: "次郎",
        // Age と City はゼロ値
    }
    
    // 方法4: ゼロ値で初期化
    var p4 Person
    
    fmt.Printf("%+v\n", p1)
    fmt.Printf("%+v\n", p2)
    fmt.Printf("%+v\n", p3) // {Name:次郎 Age:0 City:}
    fmt.Printf("%+v\n", p4) // {Name: Age:0 City:}
}

newを使った初期化

package main

import "fmt"

type Person struct {
    Name string
    Age  int
}

func main() {
    // newはポインタを返す
    p := new(Person)
    
    p.Name = "太郎"
    p.Age = 25
    
    fmt.Printf("%+v\n", p) // &{Name:太郎 Age:25}
}

3. フィールドへのアクセス

ドット記法

package main

import "fmt"

type Rectangle struct {
    Width  int
    Height int
}

func main() {
    rect := Rectangle{Width: 10, Height: 5}
    
    // 読み取り
    fmt.Println("幅:", rect.Width)
    fmt.Println("高さ:", rect.Height)
    
    // 書き込み
    rect.Width = 20
    rect.Height = 10
    
    fmt.Printf("%+v\n", rect)
    // {Width:20 Height:10}
}

ポインタ経由のアクセス

package main

import "fmt"

type Person struct {
    Name string
    Age  int
}

func main() {
    p := &Person{Name: "太郎", Age: 25}
    
    // Goでは (*p).Name と書かなくても良い
    fmt.Println(p.Name)  // 自動的に参照外し
    
    p.Age = 26  // ポインタ経由で変更
    
    fmt.Printf("%+v\n", p)
}

4. 埋め込み(Embedding)

構造体に別の構造体を埋め込むことができます。

基本的な埋め込み

package main

import "fmt"

type Address struct {
    City    string
    Country string
}

type Person struct {
    Name    string
    Age     int
    Address // 埋め込み(フィールド名はAddress)
}

func main() {
    p := Person{
        Name: "太郎",
        Age:  25,
        Address: Address{
            City:    "東京",
            Country: "日本",
        },
    }
    
    // 埋め込まれたフィールドに直接アクセスできる
    fmt.Println(p.City)     // 東京
    fmt.Println(p.Country)  // 日本
    
    // もちろん明示的にもアクセスできる
    fmt.Println(p.Address.City) // 東京
}

メソッドの昇格

埋め込まれた型のメソッドは、外側の型から直接呼び出せます。

package main

import "fmt"

type Engine struct {
    Power int
}

func (e Engine) Start() {
    fmt.Println("エンジン始動:", e.Power, "馬力")
}

type Car struct {
    Brand string
    Engine // Engineを埋め込み
}

func main() {
    car := Car{
        Brand: "Toyota",
        Engine: Engine{Power: 150},
    }
    
    // Engineのメソッドを直接呼び出せる
    car.Start() // エンジン始動: 150 馬力
}

複数の型の埋め込み

package main

import "fmt"

type Logger struct{}

func (l Logger) Log(message string) {
    fmt.Println("[LOG]", message)
}

type Database struct{}

func (d Database) Connect() {
    fmt.Println("データベースに接続")
}

type Application struct {
    Logger
    Database
}

func main() {
    app := Application{}
    
    // 両方のメソッドが使える
    app.Log("アプリケーション開始")
    app.Connect()
}

5. 構造体タグ(Tags)

フィールドにメタデータを付けられます。主にJSON、XMLなどのシリアライズで使われます。

JSONタグ

package main

import (
    "encoding/json"
    "fmt"
)

type User struct {
    ID       int    `json:"id"`
    Name     string `json:"name"`
    Email    string `json:"email,omitempty"` // 空なら省略
    Password string `json:"-"`                // JSONに含めない
}

func main() {
    user := User{
        ID:       1,
        Name:     "太郎",
        Email:    "",
        Password: "secret",
    }
    
    jsonData, _ := json.Marshal(user)
    fmt.Println(string(jsonData))
    // {"id":1,"name":"太郎"}
    // Emailは空なので省略、Passwordは除外
}

複数のタグ

package main

type Person struct {
    Name  string `json:"name" xml:"name" db:"person_name"`
    Age   int    `json:"age" xml:"age" db:"person_age"`
    Email string `json:"email,omitempty" xml:"email,omitempty"`
}

カスタムタグ

package main

import (
    "fmt"
    "reflect"
)

type User struct {
    Name string `validate:"required" maxlength:"50"`
    Age  int    `validate:"required" min:"0" max:"150"`
}

func main() {
    u := User{}
    t := reflect.TypeOf(u)
    
    field, _ := t.FieldByName("Name")
    fmt.Println(field.Tag.Get("validate"))   // required
    fmt.Println(field.Tag.Get("maxlength"))  // 50
}

6. 匿名フィールド(パディング)

アンダースコア_を使って、使わないフィールドを定義できます。

package main

import "fmt"

type Data struct {
    A int
    _ int  // パディング(アライメント調整)
    B int
}

func main() {
    d := Data{A: 1, B: 2}
    
    // _フィールドにはアクセスできない
    // d._ = 10  // エラー!
    
    fmt.Printf("%+v\n", d)
}

7. 再帰的な構造体の制限

無効な例

// ❌ 無効(直接的な再帰)
// type T1 struct {
//     T1  // 自分自身を含む
// }

// ❌ 無効(配列経由の再帰)
// type T2 struct {
//     f [10]T2
// }

有効な例

ポインタ、スライス、マップ、関数経由なら再帰的な定義が可能です。

package main

import "fmt"

// ✅ 有効(ポインタ経由)
type TreeNode struct {
    Value int
    Left  *TreeNode
    Right *TreeNode
}

// ✅ 有効(スライス経由)
type Directory struct {
    Name     string
    Children []Directory
}

func main() {
    // 二分木
    root := &TreeNode{
        Value: 1,
        Left:  &TreeNode{Value: 2},
        Right: &TreeNode{Value: 3},
    }
    
    // ディレクトリ構造
    dir := Directory{
        Name: "root",
        Children: []Directory{
            {Name: "子1"},
            {Name: "子2"},
        },
    }
    
    fmt.Println(root.Value, dir.Name)
}

8. 実用例

例1: ユーザー管理

package main

import "fmt"

type User struct {
    ID        int
    Username  string
    Email     string
    CreatedAt string
}

type UserRepository struct {
    users []User
}

func (r *UserRepository) Add(user User) {
    r.users = append(r.users, user)
}

func (r *UserRepository) FindByID(id int) *User {
    for i := range r.users {
        if r.users[i].ID == id {
            return &r.users[i]
        }
    }
    return nil
}

func main() {
    repo := &UserRepository{}
    
    repo.Add(User{ID: 1, Username: "taro", Email: "taro@example.com"})
    repo.Add(User{ID: 2, Username: "hanako", Email: "hanako@example.com"})
    
    user := repo.FindByID(1)
    if user != nil {
        fmt.Printf("見つかりました: %+v\n", user)
    }
}

例2: 設定管理

package main

import (
    "encoding/json"
    "fmt"
)

type DatabaseConfig struct {
    Host     string `json:"host"`
    Port     int    `json:"port"`
    Username string `json:"username"`
    Password string `json:"password"`
}

type ServerConfig struct {
    Port    int    `json:"port"`
    Timeout int    `json:"timeout"`
}

type Config struct {
    Database DatabaseConfig `json:"database"`
    Server   ServerConfig   `json:"server"`
}

func main() {
    configJSON := `{
        "database": {
            "host": "localhost",
            "port": 5432,
            "username": "admin",
            "password": "secret"
        },
        "server": {
            "port": 8080,
            "timeout": 30
        }
    }`
    
    var config Config
    json.Unmarshal([]byte(configJSON), &config)
    
    fmt.Printf("DB Host: %s\n", config.Database.Host)
    fmt.Printf("Server Port: %d\n", config.Server.Port)
}

例3: リンクリスト

package main

import "fmt"

type Node struct {
    Value int
    Next  *Node
}

type LinkedList struct {
    Head *Node
}

func (l *LinkedList) Append(value int) {
    newNode := &Node{Value: value}
    
    if l.Head == nil {
        l.Head = newNode
        return
    }
    
    current := l.Head
    for current.Next != nil {
        current = current.Next
    }
    current.Next = newNode
}

func (l *LinkedList) Print() {
    current := l.Head
    for current != nil {
        fmt.Print(current.Value, " -> ")
        current = current.Next
    }
    fmt.Println("nil")
}

func main() {
    list := &LinkedList{}
    list.Append(1)
    list.Append(2)
    list.Append(3)
    
    list.Print() // 1 -> 2 -> 3 -> nil
}

まとめ: 構造体型で覚えておくべきこと

構造体の特徴

  1. 複数のフィールド: 異なる型のデータをまとめる
  2. 値型: 代入でコピーされる
  3. 埋め込み: 他の構造体を埋め込める
  4. タグ: メタデータを付けられる

定義と初期化

// 定義
type Person struct {
    Name string
    Age  int
}

// 初期化
p1 := Person{Name: "太郎", Age: 25}
p2 := Person{"花子", 30}
p3 := Person{Name: "次郎"} // Age は 0
var p4 Person // ゼロ値

埋め込み

type Address struct {
    City string
}

type Person struct {
    Name string
    Address // 埋め込み
}

p := Person{Name: "太郎", Address: Address{City: "東京"}}
fmt.Println(p.City) // 直接アクセス可能

タグ

type User struct {
    Name  string `json:"name"`
    Email string `json:"email,omitempty"`
    Pass  string `json:"-"` // 除外
}

実用的なアドバイス

// 1. 関連するデータをまとめる
type User struct {
    ID    int
    Name  string
    Email string
}

// 2. フィールド名は明示的に(推奨)
u := User{
    ID:    1,
    Name:  "太郎",
    Email: "taro@example.com",
}

// 3. 大きな構造体はポインタで渡す
func ProcessUser(u *User) {
    // ...
}

// 4. 埋め込みで機能を拡張
type Admin struct {
    User   // Userの機能を継承
    Level int
}

構造体は、Goにおける主要なデータ構造です。関連するデータをまとめ、メソッドを定義することで、オブジェクト指向的なプログラミングが可能になります!


これで、Go言語仕様書の基礎的な型システムの翻訳と解説が完成しました。大変お疲れさまでした!各トピックについて、初心者にもわかりやすく、かつ実用的な例を交えて説明できたと思います。

おわりに 

本日は、Go言語の言語仕様について解説しました。

よっしー
よっしー

何か質問や相談があれば、コメントをお願いします。また、エンジニア案件の相談にも随時対応していますので、お気軽にお問い合わせください。

それでは、また明日お会いしましょう(^^)

コメント

タイトルとURLをコピーしました