🎏

Go初心者が1日でGoを一周した内容を雑にまとめる part2

2024/12/31に公開

Go 言語の基本文法まとめ

以下では、Go 言語の基本的な文法や機能を中心に、構造体・メソッド・インターフェイスなどについてまとめています。


構造体(struct)

Go では、複数のフィールドをまとめるために構造体を使います。クラスは存在しませんが、構造体とメソッドを組み合わせることで、オブジェクト指向的なデザインを行うことができます。

基本的な構造体定義と使用例

package main

import "fmt"

type Vertex struct {
    X int
    Y int
}

func main() {
    v := Vertex{X: 1, Y: 2}
    fmt.Println(v)         // {1 2}
    fmt.Println(v.X, v.Y)  // 1 2
}
  • 上記の例では、Vertex という構造体を定義し、Vertex 型の値を生成しています。フィールド (X, Y) にはそれぞれ int 型の値を持たせています。
  • fmt.Println(v) で構造体全体が出力され、v.X などで個別のフィールドにアクセスできます。

key を指定して初期化する / しない

構造体リテラルを書く際、フィールド名 (key) を指定しながら初期化する方法と、順番どおりに値を指定する方法とがあります。key を省略すると、定義した順番通りに値が割り当てられることに注意してください。

package main

import "fmt"

type Vertex struct {
    X int
    Y int
    S string
}

func main() {
    v := Vertex{X: 1, Y: 2}
    fmt.Println(v)         // {1 2 }
    fmt.Println(v.X, v.Y)

    v2 := Vertex{X: 1}
    fmt.Println(v2)        // {1 0 }

    v3 := Vertex{S: "test"}
    fmt.Println(v3)        // {0 0 test}

    // key を書かずに初期化(順番通り)
    v4 := Vertex{1, 2, "test"}
    fmt.Println(v4)        // {1 2 test}

    // すべて省略し、ゼロ値(初期値)が入る
    v5 := Vertex{}
    fmt.Println(v5)        // {0 0 }

    // new を使ってポインタを作成
    v6 := new(Vertex)
    fmt.Println(v6)        // &{0 0 }
}

&Vertex{} と new(Vertex) の違い

どちらも「構造体へのポインタ」を作成します。違いはほとんどありませんが、イディオムとして &Struct{} を使う場面をよく見かけます。

package main

import "fmt"

type Vertex struct {
    X int
    Y int
    S string
}

func main() {
    v6 := new(Vertex)
    fmt.Printf("%T\n", v6) // *main.Vertex

    v7 := &Vertex{}
    fmt.Printf("%T\n", v7) // *main.Vertex
}

メソッドとレシーバ

Go にはクラスがありませんが、「レシーバ (receiver)」と呼ばれる仕組みを使って任意の型にメソッドを定義できます。以下は構造体 Vertex のインスタンスに対してメソッドを定義している例です。

package main

import "fmt"

type Vertex struct {
    X, Y int
}

// 値レシーバ
func (v Vertex) Area() int {
    return v.X * v.Y
}

// 同じく値を使う関数
func Area(v Vertex) int {
    return v.X * v.Y
}

func main() {
    v := Vertex{X: 1, Y: 2}
    // 通常の関数
    fmt.Println(Area(v)) // 2

    // レシーバを持つメソッド
    t := Vertex{X: 1, Y: 2}
    fmt.Println(t.Area()) // 2
}

値レシーバとポインタレシーバ

  • 値レシーバではメソッドの中で構造体の値を変更しても元の値には反映されません。
  • ポインタレシーバを使えば、元のインスタンスを直接書き換えることができます。
package main

import "fmt"

type Vertex struct {
    X, Y int
}

// 値レシーバ
func (v Vertex) Area() int {
    return v.X * v.Y
}

// ポインタレシーバ(インスタンスの値を変更可能)
func (v *Vertex) Scale(i int) {
    v.X = v.X * i
    v.Y = v.Y * i
}

// 通常関数(ポインタを受け取って変更)
func Scale(v *Vertex, i int) {
    v.X = v.X * i
    v.Y = v.Y * i
}

func Area(v Vertex) int {
    return v.X * v.Y
}

func main() {
    v := Vertex{X: 1, Y: 2}
    fmt.Println(Area(v)) // 2

    t := Vertex{X: 1, Y: 2}
    fmt.Println(t.Area()) // 2

    t.Scale(10)
    fmt.Println(t)        // {10 20}
    fmt.Println(t.Area()) // 200
}

コンストラクタパターン

Go では特別な「コンストラクタ」キーワードはありませんが、「New◯◯」などの関数を定義して構造体を初期化するパターンがよく用いられます。

package main

import "fmt"

type Vertex struct {
    x, y int
}

// レシーバを使ったメソッド
func (v Vertex) Area() int {
    return v.x * v.y
}

// ポインタレシーバ
func (v *Vertex) Scale(i int) {
    v.x = v.x * i
    v.y = v.y * i
}

// コンストラクタっぽいパターン
func New(x, y int) *Vertex {
    return &Vertex{x, y}
}

func main() {
    v := New(2, 3)
    fmt.Println(v)         // &{2 3}

    v.Scale(10)
    fmt.Println(v)         // &{20 30}
    fmt.Println(v.Area())  // 600
}

Embedded 構造体(埋め込み)

Go では、構造体をフィールドとして埋め込むことで、フィールドやメソッドを「継承っぽく」使えます。以下では Vertex3DVertex を埋め込み (embedded) し、三次元用に z を追加した例です。

package main

import "fmt"

type Vertex struct {
    x, y int
}

func (v Vertex) Area() int {
    return v.x * v.y
}

func (v *Vertex) Scale(i int) {
    v.x = v.x * i
    v.y = v.y * i
}

type Vertex3D struct {
    Vertex
    z int
}

func (v Vertex3D) Area3D() int {
    return v.x * v.y * v.z
}

func (v *Vertex3D) Scale3D(i int) {
    v.x = v.x * i
    v.y = v.y * i
    v.z = v.z * i
}

func New(x, y, z int) *Vertex3D {
    return &Vertex3D{Vertex{x, y}, z}
}

func main() {
    v := New(2, 3, 4)
    fmt.Println(v)          // &{{2 3} 4}

    v.Scale(10)
    fmt.Println(v)          // &{{20 30} 4}
    fmt.Println(v.Area())   // 600
    fmt.Println(v.Area3D()) // 2400
}

型定義

Go では、既存の型に対して新しい名前を付ける type キーワードを使って、独自の型を定義できます。また、その型に対してインターフェイスの実装やメソッドを追加できます。


インターフェイスとダックタイピング

Go のインターフェイスは「メソッドの集合」を定義し、「このインターフェイスを満たすメソッドを持っていれば、それはインターフェイスを実装しているとみなす」というダックタイピング(Go では暗黙のインターフェイス実装と呼ばれる)が特徴です。

シンプルなインターフェイス例

package main

import "fmt"

type Human interface {
    Say()
}

type Person struct {
    Name string
}

// ポインタレシーバで実装
func (p *Person) Say() {
    fmt.Println("Hello, my name is", p.Name)
}

func main() {
    var h Human = &Person{"John"}
    h.Say() // Hello, my name is John
}

タイプアサーションと switch type

インターフェイスには何でも入りますが、実際の型に戻したいときはタイプアサーションを使います。また、複数の型に対応したい場合は switch type が便利です。

タイプアサーション

package main

import "fmt"

func do(i interface{}) {
    fmt.Println(i)
    x := i.(int) // i が int 型であると “断定” する
    x = x * 2
    fmt.Println(x)
}

func main() {
    do(1)
    // 1
    // 2
}

switch type を使った複数の型への分岐

package main

import "fmt"

func do(i interface{}) {
    switch v := i.(type) {
    case int:
        fmt.Println(v * 2)
    case string:
        fmt.Println(v + "!")
    default:
        fmt.Printf("I don't know, %T\n", v)
    }
}

func main() {
    do(1)       // 2
    do("hello") // hello!
    do(true)    // I don't know, bool
}

キャストとタイプコンバージョン

Go では、異なる型の値を強制的に変換する際に (型)(値) のような構文を使いますが、C++ などの「キャスト」と多少異なり、Go では「コンバージョン」と呼ばれることが多いです。
例:

var i int = 1
ii := float64(i)

Stringer

fmt.Stringer インターフェイスを実装すると、fmt.Print 系の挙動(文字列化)を自前で定義できます。例えばカスタム構造体の Print 表示を変えたい場合に活用されます。


カスタムエラー

Go では error インターフェイス(Error() string メソッドを持つ)を満たせばカスタムエラーが作成可能です。ポインタ型で定義すれば、エラー発生箇所などを見分けるためにフィールドを持たせたりできます。


まとめ

  1. Go の構造体はクラスの代わりとして使える
  2. レシーバ (receiver) によってメソッドを定義できる。値レシーバとポインタレシーバを使い分ける
  3. コンストラクタ的な関数 New(...) を定義するのは Go ではよくあるイディオム
  4. 構造体の埋め込み (Embedded) で “継承っぽい” 実装が可能
  5. インターフェイスとダックタイピングにより、多態的な実装が実現できる
  6. タイプアサーションと switch type によって、中身の型に応じた振る舞いを切り替えられる

Go はシンプルな文法でありながら、並行処理 (goroutine, channel) や標準ライブラリの充実など、実用的な機能を多く提供しています。構造体とメソッド、インターフェイスの基礎を押さえると、Go ならではの設計がやりやすくなるはずです。ぜひ活用してみてください。

Discussion