ペンギン村 Tech Blog

技術をこよなく愛するエンジニア集団が在住するペンギン村から、世界へ役立つ(かもしれない)技術情報を発信する技術系ブログです。某アラレちゃんが済む村とは一切関係ありません。んちゃ!

Swift プログラマが Go言語を学び始めた話(中編)

以下の記事の続きです。 blog.penginmura.tech

制御構文

Go言語の制御構文としては次のものがある。

  • if
  • switch
  • for

ゴルーチン・チャネルにおいて利用できる select という制御構文もあるが、それは後ほど触れることにする。

if 文

基本的な書き方は Swift とほとんど変わらない。次のコードは Go言語 / Swift ともにコンパイルできる。

if 0 <= x && x < 100 {
} else {
}

Go言語では if 文において事前にローカル変数を宣言することができ、これは Go言語におけるエラー処理のパターンで多用される。前編において 0除算された場合にエラーを返すDiv関数を呼び出した例として、次のようなエラー処理を記述した。

n, err := Div(6, 2)
if err != nil {
    panic("error!")
}

これは次のように書き換えることができる。

if n, err := Div(6, 2); err != nil {
    panic("error!")
} else {
    // use `n`
}

; の直前が変数宣言になっており、直後が本来の if 文の条件式となっている。ただし、これらの変数は if 文のスコープに限定されるので、Swift の guard let のように、束縛した変数をあとから参照することはできない。

言い換えると次のコードはコンパイルエラーとなる。

if n, err := Div(6, 2); err != nil {
    ...
}
fmt.Println(n) // undefined: n

Go の慣習に触れておくと、成功したときの値を利用するのであれば前編のような書き方をし、そうでなければ後者の書き方をして、if 文内でエラー処理をしていくのが一般的となる。

if err := os.Mkdir("foo.txt", 0755); err != nil {
    panic("Error!")
}
// 成功時の処理

switch 文

前編で少し触れたが、Go言語には enum に相当するものが存在しないため、コンパイル時に網羅性検査などは行われない。 それを除けば、見た目的にはほとんど変わらない。

x := 42

switch x {
case 0:
case 1, 2:
    fallthrough
case 3:
    fmt.Println("1 or 2 or 3")
case 42:
    fmt.Println("42")
default:
    fmt.Println("other")
}

明示的なbreakが不要な点やfallthroughで後続のcaseに移る点、caseに複数の値を定義できる点も Swift と同じである。1点だけ細かい違いをあげるとcase 0:の例のように実行コードが無くてもコンパイルがとおる点だろうか。

SwiftにないGo言語におけるswitchの便利な機能として、式を与えずに利用することでif-elseの羅列を表現できる仕組みがある。

x := 42

switch {
case x < 0:
case 0 <= x && x < 10:
case 10 <= x && x < 100:
default:
}

なお Swift では ~= 演算子をオーバーロードすることで、Rangeようにcaseでのマッチをカスタマイズすることができるが、Go 言語ではそのような機能は存在しない。もっといえば演算子のオーバーロード自体ができない。

iota 列挙子

ここまで Go言語に enum は存在しないと書いてきたが、似たようなものを実現する iota というものが存在する。

type Weekday int

const (
    Sunday Weekday = iota
    Monday
    Tuesday
...
)

w := Monday
switch w {
case Sunday:
case Monday:
}

iota は Swift において数値の rawValue が自動生成される仕組みとほとんど同じであり、Swift においては以下のコードと同等である。

enum Weekday: Int {
    case sunday
    case monday
    case tuesday
}

しかし、Go言語では網羅検査が可能なenumではなく、単なる数値型についた定数ラベルでしかない。すなわち以下のコードの糖衣構文といえる。

const (
    Sunday  = 0
    Monday  = 1
    Tuesday = 2
)

これは網羅検査が機能しない理由の説明にもなるだろう。

for 文

Swift 3.0 では C言語スタイルの for 文は廃止されたが、Go言語では引き続き利用可能である。

for i := 0; i < 10; i++ {
}

と言っても、インデックスを意識したコードは間違いやすいので、Swift における for-in のように range というものが用意されており、配列やマップを走査する際に利用できる。

xs := []int{1, 2, 3}

for i, x := range xs {
    fmt.Printf("xs[%v] = %v\n", i, x)
}

Swift では次のようになるだろう。

let xs = [1, 2, 3]

for (i, x) in xs.enumerated() {
    print("xs[\(i)] = \(x)")
}

なお、Swift では whiledo-while があるが、Go言語では for 文にすべて集約されている。

// while相当
for i < 10 {
}

// 無限ループ
for {
}

breakcontinue については同じなので割愛する。

メソッド

Go言語のオブジェクト指向のアプローチは限定的である。インターフェースによる多態性を除けば、ある型に対するメッセージング、すなわちメソッド呼び出しが主になる。

長方形を表す Rectangle 構造体に対して、面積を求める Area というメソッドを実装するコードは次のようになる。

type Rectangle struct {
    Width, Height int
}

func (r Rectangle) Area() int {
    return r.Width * r.Height
}

初見だとこの構文は読みづらいが、通常の関数と違い func の直後にレシーバ(Swift でいう self)を受け取るパラメータが r が追加されている。(r Rectangle) を削除したコードを考えると、通常の関数宣言と同じ形になることが分かる。

func /* (レシーバを削除) */ Area() int {}

Swift では次のようになるだろう。

struct Rectangle {
    var width: Int
    var height: Int
}

extension Rectangle {
    func area() -> Int {
        return self.width * self.height
    }
}

シンタックスや、レシーバをselfではなく明示的なパラメータとして受け取る点を除けば、ほとんど違いがないことが分かる。なお、レシーバ名はメソッド内で完結するものなので、今回の r のように短い名前をつけるのが Go言語の慣習である。

話が前後するが、Go言語における関数呼び出しにおいて、引数は Swift と同様にコピーされたものが渡される。これはメソッド呼び出しにおけるレシーバにおいても同様で、今回の例では Rectangle 構造体はメソッド呼び出し時にコピーされる。

これは大きなデータ構造で問題になるのは間違いないが、それを避けるために Swift におけるクラスのようにポインタを受け取るようにすることもできる。

func (r *Rectangle) Area() int {
    return r.Width * r.Height
}

r := Rectangle{Width: 5, Height: 4}
ptr := &r
ptr.Area() // => 20

C言語を書ける人は馴染みがあると思われるが *T型T へのポインタを表す。また、ポインタの値を得るには Swift で参照渡しをする際に利用する & 演算子を使用する。

なお、上記のコード例では呼び出し元で明示的にポインタを取得していたが、実は次のコードでも同じ結果が得られる。

r := Rectangle{Width: 5, Height: 4}
r.Area()

本来、構造体のポインタ(*Rectangle)をレシーバとして受け取るメソッドで、構造体(Rectangle)を使用しているので型の不一致にならないかと疑問を持つかもしれない。これは型とポインタを区別せず、常に .foo() と呼び出せる仕組みであり、次のコードの糖衣構文とみなせる。

r := Rectangle{Width: 5, Height: 4}
(&r).Area()

型とポインタを区別せず と表現したように、逆のパターンでも同じである。

func (r Rectangle) Area() int { ... }

r := Rectangle{Width: 5, Height: 4}
ptr := &r
ptr.Area() // `(*ptr).Area()`と等価

インターフェース

インターフェースは Swift で言うところの Protocol にあたる。前編で少し触れたが、Go言語には構造体しかないため、継承を利用した多態性は利用できないため、Go言語ではインターフェスが多態性を実現する唯一の方法である。そういった意味では Protocol Oriented Programming のみが利用できるという言い方もできるかもしれない。

宣言と準拠

Swift では Protocol に準拠する際に明示的な宣言が必要となるが、Go 言語ではインターフェースを満たすメソッドを実装するだけで準拠(Go言語では「満足」と表現する)したことになる。これは Ruby などに代表されるダックタイピングと似ているかもしれない。

先程の Rectangle に対して、インターフェースを導入したコードは次のようになる。

// インターフェースの定義
type Area interface {
    Area() int
}

// 構造体の定義
type Rectangle struct {
    Width, Height int
}

// Area()メソッドの実装(これで`Area`インターフェースを満たす)
func (r Rectangle) Area() int {
    return r.Width * r.Height
}

インターフェースはキーワード interface で宣言し、ここでは Area という名前を付けている。見て分かるとおり、コード的には最初の4行のインターフェース定義が追加された以外(Rectangle構造体とメソッドの定義)は変化していない。

これを利用するコードは次のようになる。

var x Area = Rectangle{Width: 5, Height: 4}
x.Area() // => 20

Swift で書くと次のようになるだろう。

protocol Area {
    func area() -> Int
}

struct Rectangle {
    var width: Int
    var height: Int
}

extension Rectangle: Area {
    func area() -> Int {
        return self.width * self.height
    }
}

let r: Area = Rectangle(width: 5, height: 4)
r.area()

空インターフェース

Go言語において特徴的なものとして「空インターフェース」(interface {})と呼ばれるものがある。これは何のメソッド定義も持たないインターフェースであるが、どんな型でも受け付けるという意味で Swift における Any 型にあたる。つまり、何の制約もないからどんな型でもOKであるということだ。

どんな値でも受け取れる独自の println() 関数を実装するとしたら次のようになるだろう。

func println(x interface{}) {
}

もちろんこのままでは x に対して何も操作できないため、実際の型が何であるかを判定し、キャストした上で利用する必要がある。

Go言語では「型アサーション」というものでそれを行うことができる。

func println(x interface{}) {
    if b, ok := x.(bool); ok {
        if b {
            fmt.Println("TRUE")
        } else {
            fmt.Println("FALSE")
        }
    }
}

これも慣れないうちは読みづらい構文だが、変数 x について x.(型) と記述する感じになる。結果は多値で返ってきて、キャストに成功していれば2つめの戻り値(ここではok)に true が入り、キャスト後の結果が1つめの戻り値(ここではb)に入るという感じである。

Swift では次のようになるだろう。

func println(_ x: Any) {
    if let b = x as? Bool {
        if b {
            print("TRUE")
        } else {
            print("FALSE")
        }
    }
}

なお、このようなパターンを簡単に記述する方法として「型switch文」と呼ばれるものを利用することができる。

func println(x interface{}) {
    switch x := x.(type) {
    case bool:
        if x {
            fmt.Println("TRUE")
        } else {
            fmt.Println("FALSE")
        }
    default:
    }
}

Swift では次のようになるだろう。

func println(_ x: Any) {
    switch x {
    case let x as Bool:
        if x {
            print("TRUE")
        } else {
            print("FALSE")
        }
    default:
        break
    }
}

おわりに

といったところで中編はこれくらいにしようと思う。

次がこの連載における最後の記事になる予定であるが、ゴルーチン・チャネルは語ることが多そうなので、もしかしたら別記事としてしっかり書くかもしれない。

Written by @tobi462

P.S.
はてぶコメントは読ませていただいています。

前編の記事に対するはてブコメントでは enum について iota で代用可能ではないかというコメントもあったので、今回の記事で触れさせていただきました。