Rのつく財団入り口

ITエンジニア関連の様々な話題を書いているはずのブログです。

【感想】『スターティングGo言語』:入門の定番書でGolangの世界へ【前編】

Golangの世界へスタートしてみよう

 しばらく前にGo言語に入門してみたのでまずは定番の入門書を1冊やろうと読んだ本です。刊行が2016年とちょい時間が経っていますが、ほぼ問題ありませんでした。以下、復習を兼ねて読書記録と感想を。

はじめに

 まずは、プログラム環境全体でシンプルさと合理性を追求することで生産性を高めたGo言語そのものの話から。

  • 2009年登場。意図のある簡潔性から多くのメリットを引き出している。特徴は以下。
  • ネイティブコードへのコンパイルJava仮想マシン(JVM)のような仕組みが不要で、生成したネイティブコードを実行するだけで動く。Windows環境ならexeファイルが生成。オーバーヘッドが少ないので高速。
  • マルチプラットフォームでの動作:動作するといいつつ多少変更が必要な他の言語と比べ、OSやCPUでの実行環境の差をほぼ完全に隠蔽。プラットフォームの差に気を配る必要がほとんどない。
  • OSへの非依存:各OSでの標準的なライブラリにすら依存していない。車輪の再開発をあえてすることで、Go言語で生成された実行ファイルだけで完結。そのため実行ファイルのサイズは大きめ。
  • ガベージコレクター:C言語の根本的な弱点であるメモリリークバッファオーバーフローは、他の言語と同様に防止済み。
  • 並行処理:他の言語では難しく高度な専門知識が必要な並行処理が、「ゴルーチン(goroutine)」やデータ構造の「チャネル(channel)」で解決。

他の言語の経験者には以下のように指針を示しています。

  • C:言語の要素はGoにも備わっていつつ、かつ危険な操作は防げるようになっている。Better C的な位置づけ。Cプログラマなら習得は容易。
  • JavaC#Goは非オブジェクト指向言語であり、効率的なコーディングとビルドも言語レベルでデザインされている。パラダイムの転換の視点で学ぶとよい。
  • PHP/JavaScript/Ruby/Python動的型付け言語に比べると10-100倍の圧倒的な実行速度。これらの言語で隠蔽されている低レベル処理やメモリ領域も扱うので学びになる。JavaScriptでよくある多数のライブラリの混乱や環境問題もGoは少ない。「Cの実行速度とPythonで得られる開発速度」を実現した、Goならではの「実用性の高い手軽さ」に着目。

 決してオブジェクト指向が間違っていたという話ではないよ、などと他の言語を貶めてはいないあたりが入門書としてしっかりしていますね。
 2016年発行の本書が対象にしているGoのバージョンはv1.6。2022年現在はもう1.10を超えて1.18まで行っていますが、ここは仕方ないですね。

『スターティングGo言語』でGolang完全に理解した...(※してません)

Chapter 1:開発環境 ~ WindowsOS XLinuxへのGoのインストール

go.dev

  • 公式サイトのダウンロードページから、Windws/Mac/Linux用それぞれのバイナリファイルをダウンロード。
  • Windowsなら.msiインストーラーがあるのでインストール先を選ぶだけ。
    • システム環境変数Pathにインストール先のC:¥Go¥binなどを追加。
    • 環境変数GOPATHに任意のフォルダを設定。ホームの%USERPROFILE%¥go がオススメ。ここにライブラリが入る。
  • Mac.pkgファイルをダウンロードしてインストール。
    • インストール先は /usr/local/go になる。
  • Linux.tar.gzファイルを解凍して配置。
    • インストール先はMacと同様、その後環境変数PATHGOPATHを設定。

その後Goが対応しているソースコード管理のホスティングサービスとしてGitHubやBitbucketが紹介されています。ここでSCM: Source Code Management という表現を使っていますが、日本語ですとSCMといえばサプライチェーンマネジメントなのであまりSCMという表現は使わないのでは?と思います。2016年の本だからでしょうか。

 Go用の開発環境としては以下。

  • gocode : IDE用のオートコンプリート提供の.exeがGitHubで提供。
  • GoClipse: Eclipse用のプラグイン
  • Atom: Goのシンタックスハイライトにも対応
  • Visual Studio Code: Microsoftの大きな方針転換を象徴しているVSCodeもGoをサポート。
  • Emacs/Vim: 玄人向けのこれらも、Goのオートコンプリートを導入すれば生産性高い。本書の作者さんも愛用。

 本書は2016年の本ですので原稿は2015年ごろでしょうか。まだAtomが紹介されていたり、VSCodeがβ版となっているのが懐かしい...本書ではVSCodeは「将来的に普及が進み有力な開発プラットフォームに成長する可能性があります」と記述されていますが、2016年以降まさにそうなりましたね。僕もGoに入門したときはVSCodeでやりました。

『スターティングGo言語』でGolang完全に理解した...(※してません)

Chapter 2:プログラムの構成と実行 ~ Goプログラムの書き方とビルド・実行・パッケージ作成

基本のところ。

  • テキストエディタIDEは何でも良いが、テキストエンコーディングUTF-8のみ。
  • >go run {ファイルパス} で、ビルドしつつ実行できる。
  • ひとつのファイルに記述できるのは1つのパッケージのみ。先頭に package main のように書く。
  • インポートは import { } の中に改行して全て書く。参照しておいて使わないとコンパイルエラー。
  • mainパッケージの中の関数main がエントリーポイントで、ビルドした.exeの中で最初に実行される。
  • >go build -i {実行ファイル}.exe {元コード}.go でビルドされる。実行ファイルのサイズは大きめ
  • ディレクトリ構成は以下。
someapp
+- pkg1 // 1つのディレクトリに2パッケージ以上定義するとコンパイルエラー
   +- funcs1.go //3ファイルいずれもpkg1パッケージ所属。1ファイルにしても同じ。
   +- funcs2.go
   +- funcs3.go
   +- pkg1_test.go // _testが付いていると、そのパッケージのテスト用ファイルと認識
+- app.go // mainパッケージに所属
+- app_test.go // mainパッケージのテスト用
+- main.go // mainパッケージに所属、エントリーポイント
  • 通常は パッケージ名=ディレクトリ名 で違っても動作するが、普通は同じにする。
  • 上記のsomeapp/直下で> go build すると、自動的にsomeapp.exe ができる。
  • >go test ./pkg1 のようにするとテストが実行。xUnitのような別途インストールは不要で最初から入っている。
  • >go testだとカレントのmainパッケージのテストになる。
  • ファイルの拡張子は基本.goだが、実はC/C++系の拡張子にも対応している。

Javaだとパッケージ名=ディレクトリ名でないとコンパイルエラーになるのですが、一応違うのも許容するあたりはPHPと似ていますね。クラスがないのでパッケージ内のファイルの中身の構成は自由なのも動的型付け言語と似ています。
 一方パッケージを2回定義するとコンパイルエラー、importしといて使わないとコンパイルエラー、後ほど出てきますが変数を定義しといて使わないのもコンパイルエラーと、このへんは厳格。 ビルドはgo build と打つだけだし、何もしなくても言語レベルでテストクラスが作れて最初からTDDも実施可能なのもすごい。
 シンプルかつ厳密な言語仕様でがっちり高品質なコードを書けるようにしているなという感触です。
 なおパッケージ名の推奨は小文字始まり、全て小文字、単数形の短い単語。キャメルケースやスネークケースは使わないとのこと。

『スターティングGo言語』でGolang完全に理解した...(※してません)

Chapter 3:言語の基本 ~変数・型・演算子・関数・定数・スコープ・制御構文・ゴルーチン

 Goのコーディング標準ではインデントは「タブ」というのがけっこうびっくりしたのですが、本書ではサンプルコード上は2桁分インデントで示されています。

  • //が1行コメント。
  • /* */ が複数行可能なコメント。ネストさせるとコンパイルエラー注意。
  • 文(Statement)の区切りはセミコロン ; だが、すべての場合に省略可能な設計にしてあるので記述不要。
  • ただし文の終端に自動でセミコロンを挿入するので、配列には注意。いわゆる「ケツカンマ」が大事。
a := [3]string {
  "apple",
  "banana",
  // "cinamon"のあとが文末と認識されて;が挿入されてしまう。
  "cinamon" //コンパイルエラーだが,を入れると大丈夫
}
  • Goが目指しているのはC言語のような高速性とスクリプト言語のような手軽さの両立」で、一見相反する方向性を両立させようと工夫しているので、こうした奇妙な動作が時々ある。コードの表現力よりコンパイル速度の速さを優先させ、コンパイルのパース機能を軽量に保つためにセミコロンの省略を簡易な仕様にしていると思われる。

 本書はあちこちでGoがなぜその言語仕様にしているのかのコラムがあるのですが、こういう所はありがたいです。もうセミコロンいらない言語の方が多いのですねえ。

  • fmtパッケージのfmt.Println("Hello") が改行付き標準出力。
  • fmt.Printf("数値は%d", 999) がフォーマット付き出力。%のあとにいろいろ。
  • fmtパッケージと関係なく、標準エラー出力に出すprint, println 関数もある。
変数の定義回り
  • var x, y, z int のように明示的な型宣言は変数名の後。複数の変数で最後にまとめて型定義できる。同じ変数を2回以上定義するとエラー。
  • ブロックでまとめて定義もできる。複数の変数を定義する場合はvar {}でまとめるのが推奨。
var {
  a, b int
  fullName string
}

個人的には let fullName: string とTypeScriptのようにコロンがつくほうが分かりやすい気もするのですが、このへんは慣れなのでしょうね。

  • 代入はふつうに = だが、型が違うとエラー。x, y = 1, 2 のようにまとめて代入できる。数が違うとエラー。
  • := が暗黙的な定義で、型が推論されつつ初期値が入る。:= をまたやって再定義するとエラー。ただし=の代入は何回でもできる。
index := 1 // 推論されてint型になる
isTrue := true // 真偽値型
f := 3.14 // float64型
season := "april!" // 文字列型
  • 多くのシーンで明示的な型指定を省略できるようになっているので、Goでは基本的には:=の暗黙的な定義を使うのががGood。型推論を活用して開発効率向上を図る。

  • あるファイルの関数 func hogehoge() の外側で定義した変数はパッケージ変数で、パッケージ内のどこからでも参照可能。

  • 関数の中で定義するとローカル変数になりその関数の中だけ。
  • Goにグローバル変数はないが、パッケージ変数が似た役目。乱用に注意。
  • 参照されていない変数は全てコンパイルエラーで弾かれる厳密な仕様。コーディングミスを可能な限り防いでいる。

 簡単なチュートリアルをやると、この使われていない変数がすべて警告でなくコンパイルエラーになるのが厳しい〜!とも思うのですが、この辺は徹底しています。

  • 基本型は他の言語と大体同じだが、int32int64のようにサイズで別れている。int型は実は実装依存で32bit/64bit CPUで別になる。
  • 桁あふれのオーバーフロー時は「ラップアラウンド」という動作をする。
  • 型のキャストは f := float64(1.23445) のようにカッコで囲う。
  • 浮動小数点型は暗黙的な変換だと自動でfloat64型になる。float32は基本使わない原則でよい。
  • 複素数型は c := 1.0 + 3i のようにする。
  • rune := '魔' のようにシングルクォートで囲むとその文字のUnicodeコードポイントを表すルーン型(rune)というものがある。
  • したがって通常の文字列型は必ずダブルクォート囲み。
  • バッククォートで囲うと「RAW文字列リテラル」で改行を保持し複数行可能。中に書いた\nも変換されずに生の(=raw)文字列として認識される。
rawString := `
Goの
複数行文字列。
PHPでEOFを使って複数行文字を表すときみたい!
Javaでも後のバージョンでようやく実現されました
`

ルーン型というのがあってやっぱり北欧神話ルーン文字が語源なのか?とテンションが上りました。(ファンタジー脳)
コラムに1990年代に誕生したJavaUnicode解釈の話があったりして熱い。その時代時代のコンピュータ性能や技術によって最適な言語仕様は変わっていくのですね...

配列回り
  • 配列は a := [5]int{1,2,3,4,5} のように{}で囲う。型名は[5]int になる。参照時にインデックスを間違うとエラー。初期値を与えないとその型の初期値が入る。数値は0、文字列は空白、真偽値型はfalse。
  • a := [...]int{1,2,3}...で省略すると与えた初期値の数が自動で入る。
  • 配列が入った変数同士の代入は、要素数と要素の型が全て一致しないとエラー。参照渡しにならずに値のコピーが渡る。
  • なお一旦配列型を宣言すると数の拡張はできない。[5]int型は[50]int型に後からはなれない。そういうときは「スライス」を使う。

 配列を一旦宣言するともう伸縮できないあたり、内部でのメモリの効率的な利用を徹底してるんだろうなあと思います。数字などを囲うのが[]でなく{}なのが最初は不思議な感じがします。

  • var x interface{} のようにinterface{}型で宣言すると、あらゆる型と互換性のある何でも入る変数になる。JavaのObject型やTypeScriptのAny型のようなもの。
  • 初期値は「具体的な値を持っていない」ことを表す nil になる。
  • 算術演算子はだいたい他の言語と同じ。+は加算以外に文字列の結合にも使える。
関数回り
  • Goには「メソッド」という特殊な関数があるが、オブジェクト指向におけるクラスの中に定義した関数を表す「メソッド」とまったく違うので注意。基本は関数を定義すること、構造体を定義することがGoプログラミングの根幹。
func samplePlus(x, y, int) (int, int) {
  var rtnCd = 0
  return x + y, rtnCd
}
q, _ := samplePlus(1, 2) // リターンコード的な2つめの戻り値は無視
result, err := samplePlus(100, 200) // 両方使うパターン
  • 引数の型は同じだったら末尾にまとめられる。戻り値の型は最後に書く。戻り値がなかったら何も書かない。void型はない。
  • 戻り値を複数返せる。(!) カンマで区切り、カッコ()で囲う。
  • 関数の戻り値の一部を_で無視できるのと同様、関数の引数も_を使って無視できる。インターフェース型で特定の関数の実装を共用されるときに使う。

  • noNameFunc := func(x, y int) int {return x + y} のように書くと、名前がないが処理をする無名関数が書ける。この例だと型はfunc(int, int) intとなり、このへんTypeScriptっぽい。

  • 定義された関数を変数に代入できる。
func plus (x, y int) int {
  return x + y
}
var funcToVar = plus // ここで()はつけない
funcToVar(1, 2) // 実行すると3が戻り値
  • 関数を返す関数も定義できる。
func returnFunc func() {
  return func() {
    fmt.Println("関数を返しちゃうニャン")
  }
}

f := returnFunc() // 関数を実行するので()つき、その戻り値の無名関数がfに代入
f() // 実行すると"関数を返しちゃうニャン"
  • 関数は、関数を引数にも取れる。
func executeF(f func()) {
  f()
}
sampleF = func() { fmt.Println("関数実行でウィス") }
executeF(sampleF)  //  "関数実行でウィス"
  • Goの無名関数は「クロージャ」である。ある関数の中で無名関数を使うと、その無名関数の中で使われている変数はローカル変数と違う扱いになり、複数回呼ばれても値が保持される。整数が+1されるカウンタアップのジェネレータのようなものができる。

 関数を変数や引数や戻り値に使えるあたりはJavaScriptみやTypeScriptみもあって面白いです。

定数、列挙型
  • const X = 1 のようにして定数を定義。複数はカッコで囲う。2番め以降は値を省略してもよい。式も使える。
const (
  X = 1 // int型でなく"整数値を持つ定数"になる。最大値がないのでint型変換時にオーバーフローすることも
  Y // 1が入る
  Z
  F64 float64 = 1.222 // 明確に型を指定した場合
  F62a = float64(1.222) // 型変換を使うこちらが好まれる
)
  • 識別子iotaを使って列挙型のようなものが作れる。
const (
  A = iota // 0が入る 1+iotaにすると1始まり
  B // B = iota の省略形。1が入る
  C // C = iota の省略形。2が入る
)
  • 変数名、関数名、定数名はGoでは「識別子」。「Unicodeで文字もしくは数字と定義されたもの+アンダーバー」がその範囲。したがって実は日本語も変数名や関数名に使える。(!) なおUnicodeの記号はアウト。
スコープ回り
  • あるパッケージ内の定数、パッケージ変数、関数は「1文字目が大文字」なら他のパッケージから参照可能、publicなもの。
  • 「1文字目が小文字」が参照不可で同一パッケージ内、protected/private的なもの。
package foo
const (
  OUTER_MAX = 999
  internal_max = 99
)
var (
 OuterVar = 1
 internalVar = 2
)
func OuterFunc() {
}
func internalFunc() {
}
  • import( f "fmt") のように書くとパッケージを別名で指定できる。
  • import (. "math") のように書くとパッケージ名なしで関数が呼べるが、関数名や定数名の重複に注意が必要。
  • ひとつのパッケージ内でもファイルが違ったら、インポート宣言は別々扱い。定数は変数は小文字始まりでも別ファイル同士で相互に参照できるのに注意。
  • 関数内で定義された変数、定数のスコープはその関数の中だけ。引数と戻り値の変数も再定義するとエラー。
func defineFireYokai() {
  const word = "メラメーラ!"
  const name = "メラメライオン"
}
func someFunc2(a int) (b sting){
  //fmt.Println(word + name) // スコープ外でエラー
  //var a int // 引数の再定義はエラー
  //var b string // 戻り値の再定義はエラー
}

 Goにはenumがないというはよく言われますが、突然出てくる識別子iotaは初見だと違和感を感じました。
関数名に大文字小文字が混在してるのはなんだろうと思っていたのですが、1文字目でスコープを区別してるんですね。これはシンプルで大胆な解決法だと思いました。
 よって関数名はアッパーキャメルかローワーキャメル。関数内の変数や定数もキャメルケースで、基本的にアンダーバーはファイル名以外はあまり使わないようです。またGoの文化として全体的に変数名は短めが推奨のようですね。

制御構文回り
  • Goにはwhile loopがなくてシンプル。条件なしのforが無限ループ。
  • 配列型やスライス、マップについてrange識別子を使った同じような書き方でfor文で中にアクセスできる。
for {
  fmt.Println("無限ループだニャン")
}
// 伝統的なループ
for i := 0; i < 10; i++ {
  if i == 9 {
    break // breakも他言語と同じ。continueはネストのひとつ外側へ
  }
}
fruits:= [2]{"banana", "cherry"}
for i, name := range fruits {
 // インデックスがi、nameにi番目の文字列が入る
}
  • if文の条件文には全体のカッコが不要。複数条件で優先度をつけるときは必要。else if は多言語と同じ。
  • if文のブロック内が1文の時に{}を省略できる仕様はGoにはない。
  • if文の条件式は論理値、true/falseのみ。
  • if文の中で変数を宣言できる「簡易文付きif」がある。変数の局所性をコントロールしている。
if x== 1 {
  // 真の時の処理
}

if (true) {
  // 実行される。上が変数だったら()なしのはず
}
if x, y := 1, 2: x < y {
  // 実行される。変数x,yのスコープはこのifブロックの中だけ
}
if _, err := doSomething(); err != nil {
  // 関数の2番目の戻り値がエラーを返した時の処理。Goでよくやる書き方
}
  • go fmt コマンドでフォーマットが統一される。言語仕様のレベルでコーディングルールの宗教戦争を防いでいる。基本Gopher神に祈りを捧げて公式に従えばよい。
  • 「式によるswitch」では条件のところに(簡易文;)+式を書く。
n := 3
switch n {
case 1, 2:
  // 条件を満たすときの処理。Goはここにbreakを書かなくても
  // caseの処理が終わる。(=フォールスルーでない) 素晴らしい!
case 3, 4:
  // 逆にここに予約語 fallthrough を書くと、
  // 次のcase節とも比較してくれる。
  n = 5
  fallthrough
case 5:
  // 5のときの処理
default:
}
switch {
case n >= 0:
  // case節に条件でbool型を返す式も書ける。この時、switchの後の式は省略できるし省略すべき
case n < 0;
//case 1:
  // ひとつのswitch文で定数と式の両方があるとエラー。
}

 if文の中はtrueかfalseだけというのは簡潔でよいですね。JavaScriptPHPがこのへん1とか0とか"0"とか""とかnullとかでいろいろ不思議な動きをするので厄介でした...
 僕はJavaC#を書くときもif文のブロック内が1文でも{}は省略しない派ですが、こういう揺れを入れないようにしているのもありがたい。switch文にbreakがいらないというのも、エラーを防ぐ仕組みを言語仕様レベルできっちりやっているなあという印象です。

アサーションと型によるswitch
  • x.(T) の形でinterface{}型で消えた型情報を復活させられる。
// なんでも入るinterface{}型なので、xはint型ではない
var x interface{} = 3.14
i, isInt := x.(int) //iは3、isIntはfalse
f. isFloat64 := x.(float64) // iは3.14、isFloat64はtrue

// switchで分岐できる。この時typeは変数名でなく予約語で参照できない
switch x.(type) {
case bool:
  fmt.Println("xは論理型", x)
case int, uint:
  // 続く
}
deferやpanic
  • Go言語はGoだけになぜかgoto文があるgoto L で関数内にL:で定義したラベルへ。
  • for文にも LOOP: などでラベルを定義してcontinueの飛ぶ先に。
  • 関数内にdeferで処理を定義すると関数の終了処理が記述できる。ファイルやコネクションのクローズなど。
func deferSample() {
 defer fmt.Println("1")
 defer fmt.Println("2") // 複数定義すると最後から淳に実行
 defer func() {
   // 複数処理があったら無名関数で書ける。実行するので最後の()も必要
 }()
 
 // 関数のメイン処理
}
  • 他言語でのランタイムエラーに相当する「ランタイムパニック」がある。そこでプログラムが即座に終了。ただしdefer文は常に実行される。defer文の中で使うrecover関数の戻り値でパニック内容が取れる。
func main() {
  defer func() { // Javaなどのfinally的な処理
    if x := recover(); x != nil {
      // 戻り値のxがnilでなければpanicが実行されている。
      fmt.Println(x) // "パニック発生!"の文字列が取れる。
    }
  }()
  
  panic("パニック発生!") // 文字列以外にもinterface{}型でいろいろ渡せる
  fmt.Println("ここのメイン処理は実行されない")
}
  • 言語の名前と同じgoが並行処理を司る。ゴルーチンを生成して並行動作させられる。
func main() {
  go sub() // sub関数の中でサブの無限ループ
  for {
    //メインの無限ループ
  }
}
// 実行すると両方のループが並行してGO!
  • あるファイル内にmain()関数より先に実行される特殊な関数init()で初期処理が書ける。引数、戻り値なし。なぜか複数書ける。
func init() {
  // パッケージ変数を使ったりして初期処理
}

main() {
  // initのあとに実行
}

 太古の昔、COBOLPL/Iの時代に禁忌の技として封印されたはずのGOTO文が復活していておおッ!とテンションが上がりました。(嘘) 何か深い理由があって言語仕様に取り入れているはずですが、基本的なプログラミングではとりあえずは使い道はなさそうです。
 実行時エラーや例外に相当するものを「パニック」と呼ぶのは面白いですね。マスコットのゴーファー君が小動物的に驚いて手足をバタバタしてわたわたしている様を想像して和めばよい...のかな?(超適当)

つづくよ