はじめてのGo―シンプルな言語仕様,型システム,並行処理

第3章 型システム―型を用いた安全なプログラミング

この記事を読むのに必要な時間:およそ 6 分

2章ではstringやintなど,基本的な組み込み型を紹介しましたが,本章では独自の型の宣言方法と使い方について解説します。

type

次のような,IDと優先度を取得してタスクを処理する関数を考えてみます。2つの情報は両方とも数値で表すため,intとして宣言しています。

func ProcessTask(id, priority int) {
}

この関数を呼び出す側は次のようになります。

id := 3 // int
priority := 5 // int
ProcessTask(id, priority)

正しく呼び出せていますが,もし次のように順番を間違えたらどうなるでしょうか。

id := 3
priority := 5
ProcessTask(priority, id) // コンパイルは通る

引数の型が合っているため,コンパイルは通ってしまいます。こうしたミスはテストによって発見することもできますが,それぞれが単なるintではなく,意味に応じた型を持っていれば,コンパイル時に間違いを知ることができます。

このような場合,Goではtypeを用いて既存の型を拡張した独自の型を定義できます。

type ID int
type Priority int

func ProcessTask(id ID, priority Priority) {
}

typeのあとには,型の名前,その型の定義が続きます。上記では,intを拡張してIDと優先度それぞれに型を定義し,この型を用いて関数の定義を書き換えています。

呼び出す際には,型が適合していないとコンパイルエラーになります。

var id ID = 3
var priority Priority = 5
ProcessTask(priority, id) // コンパイルエラー

このように適切な型を用意することで,型レベルの整合性をコンパイル時にチェックでき,堅牢なプログラムを記述できます。IDEIntegrated Development Environment統合開発環境)のサポートも得やすくなり,リファクタリング時のリグレッションなども防ぎやすくなります。

構造体(struct)

Goには,構造体というデータ構造があります。構造体は複数のデータを1つにまとめることが基本的な役割ですが,後述するメソッドを持つことができ,RubyやJavaでのクラスに近い役割も担います。

構造体型の宣言

ここでは,id,detail(タスクの詳細)⁠done(完了フラグ)の3つのフィールドを持つ,Taskという型を定義してみます。

type Task struct {
    ID int
    Detail string
    done bool
}

構造体型もtypeを用いて宣言し,構造体名のあとにそのフィールドを記述します。各フィールドの可視性は名前で決まり,大文字で始まる場合はパブリック,小文字の場合はパッケージ内に閉じたスコープとなります。

この型から値を生成するには,次のように各フィールドに値を割り当てます。

func main() {
    var task Task = Task{
        ID: 1,
        Detail: "buy the milk",
        done: true,
    }
    fmt.Println(task.ID) // 1
    fmt.Println(task.Detail) // "buy the milk"
    fmt.Println(task.done) // true
}

変数taskには,生成された構造体が格納され,各フィールドにはドットでアクセスできます。

構造体に定義した順でパラメータを渡すことで,フィールド名を省略することもできます。

func main() {
    var task Task = Task{
        1, "buy the milk", true,
    }
    fmt.Println(task.ID) // 1
    fmt.Println(task.Detail) // "buy the milk"
    fmt.Println(task.done) // true
}

構造体の生成時に値を明示的に指定しなかった場合は,ゼロ値で初期化されます。

func main() {
    task := Task{}
    fmt.Println(task.ID) // 0
    fmt.Println(task.Detail) // ""
    fmt.Println(task.done) // false
}

ポインタ型

構造体型もアドレスを取得し,ポインタ型で扱うことができます。構造体から値を生成するときに,構造体の名前の前に&を付けると,変数には構造体の値ではなくアドレスが格納されます。Taskのポインタ型は*Taskという型になります。

var task Task = Task{} // Task型
var task *Task = &Task{} // Taskのポインタ型

たとえば,関数に対して構造体を値渡しするとデータはコピーされるため,関数内での構造体への変更は呼び出し側には反映されません。

type Task struct {
    ID int
    Detail string
    done bool
}

func Finish(task Task) {
    task.done = true
}

func main() {
    task := Task{done: false}
    Finish(task)
    fmt.Println(task.done) // falseのまま
}

この関数の引数をポインタ型にするには,引数の型を*Taskとします。ポインタを渡すことで,渡した側に関数内での変更が反映されます。

func Finish(task *Task) {
    task.done = true
}

func main() {
    task := &Task{done: false}
    Finish(task)
    fmt.Println(task.done) // true
}

このように,Goでは値とポインタを用途に応じて使い分けることができます。

new()

構造体は,組込みの関数new()を用いて初期化することもできます。new()は,構造体のフィールドをすべてゼロ値で初期化し,そのポインタを返します。

type Task struct {
    ID int
    Detail string
    done bool
}

func main() {
    var task *Task = new(Task)
    task.ID = 1
    task.Detail = "buy the milk"
    fmt.Println(task.done) // false
}

コンストラクタ

Goには構造体のコンストラクタにあたる構文がありません。代わりにNewで始まる関数を定義し,その内部で構造体を生成するのが通例です。たとえばTaskNewする関数はNewTask()という関数にし,内部でTaskを生成し,そのポインタを返します。

func NewTask(id int, detail string) *Task {
    task := &Task{
        ID: id,
        Detail: detail,
        done: false,
    }
    return task
}

func main() {
    task := NewTask(1, "buy the milk")
    // &{ID:1 Detail:buy the milk done:false}
    fmt.Printf("%+v", task)
}

メソッド

型にはメソッドを定義できます。メソッドは,そのメソッドを実行した対象の型をレシーバとして受け取り,メソッドの内部で使用できます。たとえば,Taskの文字列表現を返すString()というメソッドをTaskに定義してみます。

func (task Task) String() string {
    str := fmt.Sprintf("%d) %s", task.ID, task.Detail)
    return str
}

func main() {
    task := NewTask(1, "buy the milk")
    fmt.Printf(“%s”, task) // 1) buy the milk
}

このメソッドは,レシーバのフィールド値から文字列を生成し,それを戻り値として呼び出し側に返します。このとき,Taskのコピーがレシーバとして渡されるため,もしメソッド内部でレシーバの中身を変更していても,呼び出し側には反映されません。

呼び出し側に変更を反映したい場合は,レシーバをポインタとして受け取るようにします。たとえば,タスクを終了済みにするFinish()というメソッドをTaskに定義してみます。

func (task *Task) Finish() {
    task.done = true
}

func main() {
    task := NewTask(1, "buy the milk")
    task.Finish()
    // &{ID:1 Detail:buy the milk done:true}
    fmt.Printf("%+v", task)
}

今回は,Finish()が実行されたTaskのポインタを受け取り,メソッド内でその内部を書き換えています。ポインタを経由して書き換えているため,呼び出し側にもその変更が反映されています。

著者プロフィール

コメント

コメントの記入