ペンギン村 Tech Blog

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

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

私は Rust に賭けていた。ポストC++言語としての地位をどのプログラミング言語が取るか、もっといえば Rust vs Go でどちらが勝つかという話においてだ。

私が初めてGo言語を知った時は、こんな非力なプログラミング言語のどこが良いのだろうという感想だった。シンタックスはC言語を踏襲しているもののなんだか奇妙に見えたし、例外もジェネリクスも無いプログラミング言語が流行るわけ無いと思った。もちろんそれが意図的であるというWeb記事をいくつか読んだあとでもだ。それにGCを採用した言語がそれほどの高速さを得られるのかという点においても懐疑的であった。

しかし、予想に反してGo言語は相当受け入れられたらしい。

コンテナ界の世紀末覇者先輩であるDockerはGo言語で書かれているし、私の愛するKubernetesやその周辺ツールまでもがGo言語で書かれているらしい。

それに比べて Rust はといえば愛されている言語No.1に何度か取り上げられつつも、そこまで広く普及しているようには見えない。少なくとも私の観測範囲においてはという話ではあるが。Rust はプログラミング言語としてよくデザインされていると感じるし、Go言語に比べたらその差は圧倒的だと思うくらいだ。しかし、現実的には Go のほうが優勢と言わざるを得ない気もする。

Go言語を学び始める

そんなこんなで、私は1週間ほど前からきちんと Go言語を学び始めた。まだ知識レベルとしては疎いが、そろそろ最初のアウトプットをしても良い頃だろうと思い、このブログ記事を書き始めた。

私は Swift の上級者というわけではないにしろ使い慣れている言語なので、それと比較しながら言語機能を紹介する記事にしたいと思う。Go言語において難しい機能は多くないと思うが、それでも独特なシンタックスは見た目だけで躊躇を覚えてしまうものだ。ペンギン村の住民は iOSエンジニアが多く、最近 Go言語に興味を持っている人も多いようなのでそういった理由もあったりする。

1記事で書ききるのは難しいと思うので、何回かに分けて投稿したいと思っている。(たぶん多くて3回くらいだろう)

更新(2019/01/30):
全体で3記事となった。

blog.penginmura.tech

blog.penginmura.tech

Go言語の概略

Go言語の特徴を羅列すると次のような感じだろうか。

  • コンパイル言語
  • 静的型付け
  • (実用上問題ない程度の)ほどほどな型付け
  • (メッセージングが主な)オブジェクト指向
  • シングルバイナリ
  • C言語ライクな構文
  • GCによる自動メモリ管理
  • 充実したツール群(gofmtなど)
  • ゴルーチン・チャネルによる安全な並列処理
  • 慣習を重視

正直なところプログラミング言語として学んで面白い機能としては、ゴルーチン・チャネルくらいではなかろうかと思う。構造体埋め込みなどの機能は面白いが、それほど新しい何かをもたらしているわけではない。

Go言語を学んだ際に大切だと思ったのは、gofmtなどの周辺ツールも含めて「プログラミング言語Go」と見なす必要があるであろうことだ。すなわち、SwiftFormatやSwiftLintのようにオプショナルなツールではなく、あって当然のツールであるという点だ。

もう1つは短絡的に機能を評価しないということだ。Go言語を学んでいくと、○○言語にはあるのにとか、この機能は安全に設計されてないだとか思うことがたくさんある。それでも、(たぶん)その多くは意図してそのように設計されたものであるということだ。いちいち批判的になっていては学習が進まない、適切に批判したいならちゃんとGo言語を十分に学んだあとであとでも十分だろう。

開発環境

私は VSCode を利用して開発している。

JetBrains製の GoLand もあるが、VSCodeで補完は効くし、インポート文の自動制御も、gofmtによるフォーマットも自動的に行なってくれるので、勉強する程度であれば VSCode で十分なように感じている。

Hello, world

まずは Hello, world を見てみる。

package main

import "fmt"

func main() {
    fmt.Println("Hello, world")
}

冒頭ではpackageを宣言しているが、Goではモジュールをpackageという単位で扱う。ここではmainという名前のパッケージ空間に属しているということだ。

次にimportfmtパッケージをインポートしている。Swiftではモジュール単位にインポートを行うが、Go言語ではパッケージ単位になる。

最後にfuncでmain関数を定義し、fmtパッケージのPrintln関数を呼び出している。Swiftとあまり変わらないように見えるが、対象の関数を呼び出す際にパッケージ名fmtを付けて呼び出しているのが分かる。

また、Printlnが大文字から始まっているのに違和感を覚えるかもしれないが、これはGo言語においては「先頭が大文字」である場合にパッケージ外からアクセスできるというルールがあるためだ。Swiftでは現在アクセス制御は5種類あるが、Go言語においては2つしかない。すなわちパッケージ外に公開する・しないの2択だ。これはオブジェクト指向におけるカプセル化がパッケージ単位でしか行えないということを意味する。

書いたプログラムはgo buildで実行バイナリを出力するか、go run hello.goみたいにして実行することができる。

基本データ型と変数

Go言語にもintfloat64stringboolといったこれ以上分解できない基本的なデータ型が存在する。

変数の宣言は次のような感じで行う。

x := 42
var s string

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

var x = 42
var s = ""

Go言語では定数としてconstという宣言があるが、値はコンパイル時に確定している必要があり、Swiftのようにvar / letを使い分けたりすることはない。

xの例のように初期値がある場合は:=を使って宣言するのがGo言語では一般的らしい。代入は=なので、そこは間違えないように注意が必要だ。

sの例を見て分かるとおり、Go言語でstringを宣言した際に初期値がない場合は空文字列で初期化される。これはゼロ値初期化と呼ばれるもので、stringであれば空文字列intであれば0、参照型であればnilといった次第だ。

なお、Swiftでは未使用変数はWarningとして報告されるが、Go言語ではもっと厳しくコンパイルエラーとなる。これは少し厳しい気もするが妥当な割り切りとも感じる。

配列とマップ

配列

Go言語では固定長配列とスライスと呼ばれる可変長配列として扱えるものが存在する。

var xs [3]int
var ys = []int{1, 2, 3}

この例では、xsはサイズが3固定の固定長配列で、ysがスライスである。Swiftでは厳密な対応ではないが、次のようになるだろう。

let xs: [Int] = [0, 0, 0]
var ys: [Int] = [1, 2, 3]

Go言語では固定長配列はサイズ(ここでは3)も含めて型の一部となっており、例えば[3]int[4]intは異なるという意味だ。

スライスはSwiftでいうところのArraySliceとほとんど同じような仕組みになっている。すなわち配列の一部を指す参照型といえる。配列からスライスを得るのも演算子こそ異なるもののSwiftと似ている。

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

固定長配列は扱いづらいので、基本的にはスライスが利用されるとおぼえておくと良いだろう。なお、スライスは参照型という扱いになっている。

マップ

キーがstringで、値がintのマップは次のように宣言する。

x := map[string]int{
    "one": 1,
    "two": 2,
}

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

var x: [String: Int] = [
    "one": 1,
    "two": 2,
]

ちょっと構文が奇妙にみえるがmap[K]Vというふうに宣言されていると思えばよいだろう。

SwiftではSet型が用意されているが、Go言語ではmap[K]boolといったもので代用するという慣習になっている。また、ArrayLiterarlConvertible みたいなリッチな機能もGo言語にはない。

これは表現力が弱いとも言えるが、実用上十分であれば過度にやりすぎないというGo言語の思想の一旦を表しているようにも感じる。

構造体

Swiftにはクラスと構造体という大きく2つの合成型が存在するが、Go言語には構造体しかない。ゆえに継承(is-a)というものは存在しないが、構造体埋め込みというテクニックでコンポジション(has-a)を表現することはできる。

int型の値を2つもつ構造体Pointは次のように宣言できる。

type Point struct {
    X int
    Y int
}

type Point struct { X, Y int }

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

struct Point {
    var x: Int
    var y: Int
}

typeというキーワードが余計に感じるかもしれないが、これはGo言語において名前付き型を宣言するキーワードになっている。Point型の構造体を宣言したというよりは、struct { X, Y int}という構造体に対して、Pointという別名をつけたようなイメージである。

Go言語にはコンストラクタというものは存在せず、構造体リテラルというものを使って初期化を行う。

p0 := Point{}
p1 := Point{1, 2}
p2 := Point{X: 1, Y: 2}

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

var p0 = Point(x: 0, y: 0)
var p1 = Point(x: 1, y: 2)
var p2 = Point(x: 1, y: 2)

p0の例を見て察しがつくと予想されるが、明示的な初期値を与えなかった場合は、それぞれの型のゼロ値が利用される。すなわちここではintのゼロ値である0である。

Go言語にはフィールドタグと呼ばれる、リフレクションのための付加的なデータを付与することもでき、これはJSONのエンコード・デコード(Go言語ではマーシャリング・アンマーシャリングと呼ばれる)などで活用できる。

type Person struct {
    Age  int    `json:"age"`
    Name string `json:"name"`
}

現時点でリフレクションについての知識は不足しているが、Swiftに比べるとリフレクションは必要であれば使って良いという空気感が強いように感じる。

ちなみにバッククォートで囲まれたのは生文字列と呼ばれるもので、Swiftで言うところの”””で囲むのと同じ意味を持つ。すなわち複数行リテラルやエスケープを省略するのに使える。

関数

関数はシンタックスこそ少し違っているが Swift と大きく変わったりはしない。

func Add(x, y int) int {
    return x + y
}

これはSwiftでは次のように書くだろう。

func add(_ x: Int, _ y: Int) -> Int {
    return x + y
}

記号が少なく最初は読みづらく感じてしまうが、これは単に慣れの問題かもしれない。私はSwiftのほうが(人間工学的に考えると)良いシンタックスだと感じるが、たぶんコンパイラフレンドリーなのだろう。

なお、Swiftではタプルを返すことができるが、Go言語でも似たように多値を返すことができる。

func Div(x, y int) (int, error) {
    if y == 0 {
        return 0, fmt.Errorf("error!")
    }
    return x / y, nil
}

これは次のように利用する。

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

ここでは0除算の場合には、0error値を返却している。これはGo言語における一般的なエラー処理パターンである。

SwiftではおそらくResult<T, E>を使って次のように書きたくなるところだろう。

func div(x: Int, y: Int) -> Result<Int, Error> {
    if y == 0 {
        return Result.failure(Error("error"))
    }
    return Result.success(x / y)
}

この流れから察するかもしれないが、Go言語にはSwiftにおけるenumのような代数データ型が存在しない。もっといえばenumと呼べるような型も存在しない。

冒頭でも軽く触れたが、Go言語においては例外もなければResultのようなものも存在せず、このように関数の戻り値で多値を利用するパターンが基本となる。

これは一見冗長なパターンに見えるし、実際問題として雑なエラー処理で良いプログラムにおいては例外が欲しくなることもあるが、これがGo言語流儀という話になる。すなわちエラーは明示的にその場で処理すべきということだ。

なお、Go言語の関数はSwiftと同様にファーストオブジェクトであり、同じようにクロージャとして振る舞う関数リテラルも記述することができる。

strings.Map(func(r rune) rune {
    return r + 1
}, "hello")

Swiftでは次のように書くだろう。

"hello".map { r in
    return r + 1 // たぶんコンパイルは通らない
}

おわりに

といった感じで書いていたが、疲れてきたので続きは別記事にしたいと思う。

冒頭で述べたように、Go言語をちゃんと学び始めて5日程度なので多くの間違いはあるだろうが、それは気付き次第記事を更新していけたらと思っている。

更新:
中編を書きました。 blog.penginmura.tech

Written by @tobi462