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

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

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のポインタを受け取り、メソッド内でその内部を書き換えています。ポインタを経由して書き換えているため、呼び出し側にもその変更が反映されています。

インタフェース

Goのインタフェースは、その型がどのようなメソッドを実装するべきかを規定する役割を持ちます。

インタフェースの宣言

たとえば、先ほどTaskに実装したString()という振る舞いが規定されていることを表すインタフェースは、次のように定義します。

type Stringer interface {
    String() string
}

インタフェースの名前は、実装すべき関数名が単純な場合は、その関数名にerを加えた名前を付ける慣習があります。よってString()を実装するインタフェースはStringerとなります。

インタフェースの実装

Goでは、Javaのimplements構文のように、インタフェースを実装していることを明示的に宣言する構文はありません。Goは、型がインタフェースに定義されたメソッドを実装していれば、インタフェースを満たしているとみなします。たとえば先ほどTaskにはString()メソッドを実装しているため、Stringerを引数に取る次のような関数に渡すことができます。

func Print(stringer Stringer) {
    fmt.Println(stringer.String())
}

Print(task)

実は、このStringerインタフェースはGoのfmtパッケージに標準で定義されており、レシーバの文字列表現を取得するためのAPIになっています。

ほかにも、代表的な標準インタフェースとしては表1のようなものがあります。

表1 Goの主な標準インタフェース
インタフェース名定義説明
io.ReaderRead(p []byte)
(n int,err error)
リソースからデータの読み出しを行う
io.WriterWrite(p []byte)
(n int,err error)
リソースへのデータの書き込みを行う
io.CloserClose()errorリソースのクローズ処理を行う
http.HandlerServeHTTP
(ResponseWriter,*Request)
HTTPリクエストに対するレスポンスを行う
json.MarshalerMarshalJSON()
([]byte,error)
構造体やスライスなどをJSONに変換する
json.UnmarshalerUnmarshalJSON
([]byte)error
JSONを構造体やスライスなどに変換する

interface{}

次のようなインタフェースを考えてみます。

type Any interface {
}

このインタフェースは、実装すべきメソッドを指定していません。つまり、すべての型はこのインタフェースを実装していることになります。

これを利用すると、次のようにどんな型も受け取ることができる関数を定義できます。

func Do(e Any) {
  // do something
}

Do("a") // どのような型も渡すことができる

また、Anyのような型を定義しなくても、次のように直接記述することもできます。

func Do(e interface{}) {
  // do something
}

Do("a") // どのような型も渡すことができる

たとえば、fmt.Println()などのいわゆるプリント関数は、これを用いて次のように定義されているため、型を気にせずに複数の値を渡すことができるのです。

// fmt.Printlnの定義
// 任意の型を可変長引数で受け取る
func Println(a ...interface{}) (n int, err error)

型の埋め込み

Goでは、継承はサポートされていません。代わりにほかの型を「埋め込む」Embedという方式で、構造体やインタフェースの振る舞いを拡張できます。

構造体の埋め込み

例として、先ほどのTaskに対して、Userの情報を埋め込んでみましょう。

User構造体の定義は以下とし、メソッドとしてFullName()とコンストラクタ関数を実装します。

type User struct {
    FirstName string
    LastName string
}

func (u *User) FullName() string {
    fullname := fmt.Sprintf("%s %s",
        u.FirstName, u.LastName)
    return fullname
}

func NewUser(firstName, lastName string) *User {
    return &User{
        FirstName: firstName,
        LastName: lastName,
    }
}

これをTaskに埋め込みます。Taskの構造体型宣言時に、フィールドではなく型のみを記述することで、その型を埋め込むことができます。

type Task struct {
    ID int
    Detail string
    done bool
    *User // Userを埋め込む
}

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

埋め込まれたUserのフィールドやメソッドは、Taskが実装しているかのように振る舞います。また、埋め込まれた型の実体にもアクセスできます。

func main() {
    task := NewTask(1, "buy the milk", "Jxck", "Daniel")
    // TaskにUserのフィールドが埋め込まれている
    fmt.Println(task.FirstName)
    fmt.Println(task.LastName)
    // TaskにUserのメソッドが埋め込まれている
    fmt.Println(task.FullName())
    // Taskから埋め込まれたUser自体にもアクセス可能
    fmt.Println(task.User)
}

インタフェースの埋め込み

インタフェースも埋め込み可能です。主な用途は、複数のインタフェースを埋め込んで新たなインタフェースを定義することです。

たとえばioパッケージでは、ReaderWriterといったインタフェースが定義されています。

type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

このとき、Read()Write()を両方定義したインタフェースであるReadWriterは、2つのインタフェースを埋め込んで次のように定義できます。

type ReadWriter interface {
    Reader
    Writer
}

ioパッケージには同様に、Closerインタフェースを埋め込んだReadCloserWriteCloserReadWrite Closerなども定義されています。このように既存のインタフェースをさらに拡張したインタフェースを定義するのにも、埋め込みの概念が使われます。

型の変換

Goでは、暗黙的な型変換が起こることはありません。しかし型を変換できないわけではなく、明示的に変換する方法がいくつか提供されています。

キャスト

キャストは、キャストしたい型を指定して次のように行います。

var i uint8 = 3
var j uint32 = uint32(i) // uint8 -> uint32
fmt.Println(j)           // 3

var s string = "abc"
var b []byte = []byte(s) // string -> []byte
fmt.Println(b)           // [97 98 99]

// cannot convert "a" (type string) to type int
a := int("a")

キャストに失敗した場合はパニックが発生します。

Type Assertion――型の検査

あるインタフェース値が指定した型であるかを調べるには、Type Assertion(型の検査)を使用します。以下では引数をinterface{}型で受け取る関数内で、Type Assertionによって文字列であることを判定しています。

func Print(value interface{}) {
    s, ok := value.(string) // Type Assertion
    if ok {
        fmt.Printf("value is string: %s\n", s)
    } else {
        fmt.Printf("value is not string\n")
    }
}

func main() {
    Print("abc") // value is string: abc
    Print(10) // value is not string
}

第一戻り値には判定が成功した場合にその型に変換された値が返り、第二戻り値には判定が成功したかどうかが真偽値で返ります。もし第二戻り値をとらなかった場合は、判定に失敗したときにパニックが発生します。

Type Switch――型による分岐

Type Assertionは単一の型に対する検査しかできませんが、Type Switch(型での分岐)を使うと複数の型に対する検査を実行できます。そのインタフェース値がどの型なのかによって処理を分岐したい場合は、switchと組み合わせることで型ごとに処理を分岐できます。

type Stringer interface {
    String() string
}

func Print(value interface{}) {
    switch v := value.(type) {
    case string:
        fmt.Printf("value is string: %s\n", v)
    case int:
        fmt.Printf("value is int: %d\n", v)
    case Stringer:
        fmt.Printf("value is Stringer: %s\n", v)
    }
}

func main() {
    Print("abc") // value is string: abc
    Print(10) // value is int: 10
}

まとめ

本章では、Goの型システムと構造体などについて解説しました。型の扱いは堅牢なプログラムにするために重要ですので、きちんと押さえておきましょう。

おすすめ記事

記事・ニュース一覧