Rのつく財団入り口

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

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

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

 同書の読書記録と感想、後編です。

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

Chapter 4:参照型 ~スライス・マップ・チャネル

スライス
  • 参照型はどれも引数を1~3つ(型、要素数、容量)取ったmake関数で生成する。
  • 配列と違い可変長なのがスライス。見た目は配列と似ているが実装上はかなり異なり、強力な機能。
var s []int // 型としては配列と違い[]内に数を書かない。
s := make([]int, 5) // [0 0 0 0 0]
s[5] = 5 // 代入も同じ
s[6] = 6 // 要素数を超えているのでパニック発生
len(s) // len関数で要素数の5が返る
s2 := make([]int, 5, 10) // 6~10個目の値を格納できる領域があらかじめ確保される
cap(s2) // cap関数で確保された領域10が返る
s3 := []int{1,2,3,4,5} // 要素数5、容量5で配列と似たように生成するリテラル
s4 := s3[0:2] // [1,2] と部分的に新しいスライス。最初だけ、最後だけ、全部なども:で指定可能
// 文字列抽出にも使えるがバイト列なので、マルチバイト文字には使えない
sAlphabet := "ABC"[0:3] // [A B] 
sMoji := "あいうえお"[0:3] // うまくいかない
// append関数で末尾に複数要素追加。Go側でメモリ領域を適宜増やす
s5 := append(s4, 3, 4) // [1,2,3,4] 
c1 := []int{1, 2, 3} // [1, 2, 3]
c2 := []int{10,11} //「10, 11]
//copy関数で第1引数のsliceに塗りつぶしコピー。コピー数が返る
n := copy(c1, c2) // n=2 c1=[10,11,3] 
c3 = c1[0, 2, 5] //完全スライス式。[10,11,容量確保,容量確保,容量確保]

// ...でx個のint型の可変長引数が取れる
func sum(s ...int) int {
  // rangeを使い配列とスライスの各要素に簡単にアクセス
  for _, v := range s {
    // 取り出して何か処理
  }
}
sum(1, 2) // 戻り値3 引数いくつでも
sum(1, 2, 3) // 戻り値6
sliceArg := []int{1, 2, 3}
sum(sliceArg...) // 引数にスライスを渡すときはスライス変数名+...で渡せる
  • Goでは配列は「値渡し」。関数に配列を渡して中で弄ると、元の配列とは別。(Javaだと配列も参照渡し)
  • Goではスライスが「参照渡し」。関数にスライスを渡して中で弄ると、メモリ領域は同一なので元のスライスの中身が変わる。
マップ
countryMap := make(map[int]string) // この書き方が独特
countryMap[1] = "Japan"
countryMap[2] = "China"
countryMap[1] = "Korea" // キーが重複すると上書き
countryMap[3] = ...

//リテラルでも書ける。複数行の場合は最後にカンマ忘れずに
fruitsMap  := map[int]string{1: "Apple", 2:"Peach"}
// [キー:要素] の要素に配列やスライス、さらにマップを入れたりもできる。
// キーが登録されていないので要素が取れず、fruitは空文字
fruit := fruitsMap[3] 

if _, ok := fruitsMap[2]; ok {
  // fruitsMap[2]が取れた時の処理。変数名はokを使うのがGoのお作法
}
for k, v := range fruitsMap {
  fmt.Println(k, v) // 配列やスライス同様、1件1件にアクセスできる
}
len(fruitsMap) // スライスと同じく、要素数の2が返る。
// ちなみにスライスと違って容量を得るcap関数は使えない。
delete(fruitsMap, 1) //キーの値で取り除いて、 [2: Peach] のみになる

// 要素数に応じた初期スペースを第2引数で指定
largeLargeFruitsMap := make(map[int]string, 100) 
// なおこれはメモリ領域確保のヒント的なもので、スライスの容量とはちょっと違う。

 配列の上位互換がスライスのような感じでしょうか。マップは他の言語のマップや連想配列とだいたい同じな感じがしました。
 DBから単にデータを数百件取ってきて画面に表示するだけのアプリなどだと関係なさそうです。巨大なスライス同士をバイト単位要素単位で注意しながらいろいろやりくりして高度な演算をしていくような世界に突入していくと、このへんが効いてきそうです。

チャネル
  • ゴルーチン間でデータを受け渡しを行うためにデザインされたGo特有のデータ構造。
  • 中身はキュー(待ち行列)で先入れ先出しFIFO
var ch chan int // int型のチャネル。chanをつける
var ch4r <-chan int //受信専用 常に矢印は左向き
var ch4s chan<- chan int  //送信専用
ch := make(chan int, 4) // 組み込み関数makeで生成、バッファが格納領域のサイズ
ch <- 999 // チャネルに向かって整数を送信
i := <- ch // チャネルから受信

func receiveSample(ch chan int) {
 // 無限ループでchから受け取った整数をfmt.Printlnする
}
func main() {
  ch := make(chan int)
  go receiveSample(ch) // ゴルーチン起動!
  // forループでchに向かって整数を送信
  // 数字がずっと出力される
}

// バッファが0か中が空のチャネルから受信、バッファに空きがないチャネルへの送信でエラー
// Fatal error: all goroutines are asleep - deadlock!

chFruits = make(chan string, 2)
chFruits <- "Apple"
chFruits <- "Cherry"
len(chFruits) // 2 データの個数が入る。cap(chFruits)でバッファサイズが取れる
ch1 := make(chan int, 2)
close(ch1)
//クローズ済みのチャネルに送信はできない。
//ch <- 1 // panic: send on closed channel 
fmt.Println(<- ch1) // クローズ済みでもチャネルからの受信はできる。
i, ok := <- ch1
// チャネル内のバッファが空 && クローズ済みだと戻り値2つめがfalseになる。
fmt.Println(i, ok) // 0 false 

ch4for := make(chan int, 2)
ch4for <- 1
ch4for <- 2
close(ch4for) // クローズしていないと最後の受信分の後で deadlock!
for i := range ch4for {
  fmt Println(i) // 1つづつ取り出せる
}
ch4select1 = make(chan int, 1)
ch4select2 = make(chan string, 1)
ch4select1 <- 123
ch4select2 <- "もじれつ"
// このselect文を実行すると、Goランタイムはどのcase節を実行するか
// ランダムに選択する。実行のたびに変わる。
select {
case v1 := <- ch4select1:
  // 来たらv1には123が入っている。
case v2 := <- ch4select2:
  // 来たらv2には"もじれつ" が入っている。
default:
  // このコードではどちらにも当てはまらない例は来ない。
}

 非同期処理はどのような道具を選んでも本質的に難しい領域なので、後からチャレンジしてもよいですよと本書に書いてあり安心しました(笑)。確かに使うシーンは限られてはいそうです。

 並行してUdemyの講座で実際のコードを書いたのですが、チャネルに入っていった数値データが順番にぽんぽん出てきたり、複数のゴルーチンが非同期で並行動作して標準出力をダダダ~っと流すのを見るとなんかすご~い!(語彙力...)と、新たなパラダイムに触れたキモチになりました。
 クラウドでいうとAWSのSQSに順々に非同期でメッセージが格納され、取り出されたメッセージがひとつづつ出て行って順番に各サービスを回って非同期で処理をしていくのと似たようなイメージですね。(キューなんだからそりゃそうじゃろという話ですが...)

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

Chapter 5:構造体とインターフェース ~ポインタ・構造体・メソッド・タグ・インターフェース

ポインタ
  • Goではポインタが用意されている。Cとの互換性を重んじていると思われる。
  • なお文字列やチャネルなど元から参照型の型にもポインタ型は使えるが、使うシーンがあまりない。
//変数宣言では型の前に*をつける。*int型となる。ポインタ型の変数初期値はnil。
var p *int
var p4array *[3]int 

var n int = 100
//アドレス演算子"&"をつけると実際の番地を指す。
fmt.Println(n, &n) // n: 100 &n: 0xな番地
DoubleFunc(n) //引数を2倍する関数を呼んでも、基本型は値渡しなので変わらない。
var p *int = &n
//p := &n // 暗黙的な宣言で同じ
//変数の前に*をつけると番地が示すデータの実体になる。「デリファレンス」という。
fmt.Println(p, *p) // p:元から&nが入っているので、0xな番地nのまま *p:実体なので100 
*p = 300 //ポインタでデリファレンスし、指している実体を書き換える
fmt.Println(n, &n) // n:300 &n:0xな番地 // 値が書き換わる。番地は同じ。
// 引数にポインタ、関数内でもポインタでアクセスする関数を設定
DoubleFunc4Pointer(p *int) {
 *i = *i * 2
}

// nには300が入っているので、アドレスを渡せば実体を書き換えられる。
DoubleFunc4Pointer(&n)
fmt.Println(n, &n) // n:600 &n:0xな番地のまま // 値が書き換わる。番地は同じ。
fmt.Println(*p, p) // *p:600 p:0xな番地のまま // pはnを指しているので値、番地は同じ。

// ポインター変数を渡すときは変数名を渡すだけ。
DoubleFunc4Pointer(p)
fmt.Println(n, &n) // n:1200 &n:0xな番地のまま // pが指しているnが書き換わる。番地は同じ。
fmt.Println(*p, p) // *p:1200 p:0xな番地のまま // nを指しているので同じ。
// 配列へのポインタ型
drinks := [2]string{"Milk", "Juice"}
pd := &drinks
fmt.Println(drinks[0]) // "Milk"
fmt.Println(pd[0]) // C的には(*pd)[0]だが、コンパイラが検知して展開してくれる
// 文字列
s := "ABCDE"
&s // ポインタで番地が出る
s[0] // インデックス参照だがbyte型、マルチバイト文字には使えない
&s[0] // ポインタ参照するとコンパイルエラー。
// Goでは文字列は不変(immutable)で、破壊的変更できないようになっている。
// また関数の引数にstring型を使って操作しても、元から参照型で文字列の実体は新しい領域にコピーされない。
// したがって、Goで*string型で文字列のポインターを使うシーンは基本的にない。

 本書でもポインタはC言語の学習障壁として「悪名高い」と記述されています。ワタクシも駆け出しエンジニアの頃にそこで挫折して完全死亡した訳ですが、いま実際に書きながらGoを学ぶと前よりは分かった気がする...!
 数値型とか数値が入った配列とか、値渡しのデータ構造もポインタをうまく使えば参照渡しにして別関数に渡すことができ、その関数内では戻り値でまったく同じデータ構造を新たにアドレスの番地を消費して作ってから返さない。渡ってきたデータを直接操作することで番地の消費、メモリの消費を抑えられる...というわけですね。

構造体
  • Goに備わっているデータ構造。クラスメソッドやオブジェクトメソッドがないことを除けば、オブジェクト指向のクラスと大体同じ。
type MyInt int // エイリアスが定義できる。
// 元のint型にも型変換できる。なおエイリアス型同士では変換できない。
var mi1 MyInt = 5 
type Strings []string //String{"aaa", "bbb"}
type AreaMap map[string][2]float64 // AreaMap{"SomeCity", {12.345, 23.456}}
type Point struct {
  X int
  Y int
}
pt1 := Point{1, 2} // フィールドの順番に代入される
pt2 := Point{X: 1, Y: 2} // こちらの方式がよい
pt1.X = 100 // ドットで代入も普通にできる

type P struct {
  desc string
  Point // "Point Point" となる場合は型名を省略できる
}
pSample := P{desc: "説明", Point: Point{X: 1, Y: 2}}
// 下はpSample.Point.X の途中のPoint.が省略できる。
pSample.X // これでアクセス可能、VSCodeでも補完が出る。

func swap(p *Point) {
  // Point構造体の中身を入れ替える処理
}
swap(pt1) // 構造体は値渡しなのでpt1は変化しない、この呼び方はほぼ使わない
swap(&pt1) // ポインタを渡すと参照渡し、構造体の中が変わる
pByPointer := &Point{X:100, y:200} // 構造体は最初からポインタを生成して使うのが良い。
pByPointer2 := new(Point) // new演算子を使って書くやり方
swap(pByPointer2)
  • "func""+(変数名 ポインタ型)からなる「レシーバー」+関数名で「メソッド」を定義すると構造体とその関数が関連付けられ、オブジェクトのインスタンスメソッドのように使える。
  • レシーバーにポインタを使わないと構造体の中が更新されないので、普通はポインター型を使う。
  • この「メソッド」も、書き方が違うだけで実体はGoの関数である。
// Point構造体用のメソッドの定義
func (p *Point) showValues() {
  // レシーバーがp Point でも p *Pointでも、関数内ではp.で呼び出せる。
  // C/C++の世界では . と -> 両方が出てくるのより分かりやすい。
  fmt.Println("X:::" p.X, "Y:::", p.x)
}

pt1.showValues() // インスタンスメソッド的に呼び出せる。
// Point型でも*Point型でも呼び出し側では同じ。ドットで補完が出る。
pByPointer2.showValues()

// 慣例で"New"+構造体名で「型のコンストラクタ」という関数を作る。
// 戻り値は型のポインタ型を使う。
func NewPoint(x int, y int) *Pointer {
  return &Pointer{X: x, Y: y}
}
pFromConstructor := newPoint(500, 600) // *Point型で生成される

 構造体は関数に渡すと値渡しなので、レシーバーも型のコンストラクタもふつうポインタ型を使って参照渡しにするんですね。
呼び出し側はエイリアスを使ったりドットを使ったりで、渡す変数が通常の型かポインタ型かをあまり意識しないで使えるようになっているあたりは言語として工夫されています。その一方で自分的には慣れていないので、あれここ&だっけ?*だっけ?と一瞬理解で迷ってしまうのはありました。

 またGoの「メソッド」と元の構造体が関連づくのはメソッド定義のレシーバーの型のところだけで、構造体そのものの定義には「メソッド」は出てきません。このへんコードを読む際に理解の切り替えが必要ですね。

  • 構造体の中のフィールド、メソッドも可視性は同じで、大文字始まりなら外部から参照可能になる。
  • スライスやマップの中に構造体を持つのもよく使われる。構造体をキーにすることもできる。
type User struct {
  Name string
  Age int
}
type Users []*User

user1 := User{Name: "user1", Age: 10}
user2 := User{Name: "user2", Age: 20}
users1 := Users[]
// 複数追加できる。ポインタを使うのに注意
users.append(users, &user1, &user2) 
for _1, u := range users1 {
  fmt.println(u) // &{user1, 10} ...
}

users2 := make([]*User, 0) // 中身0件で作れる
users3 := make(Users, 0) // 同じこと

// キーが数値、値が構造体のマップ
userMap := map[int]User {
  1: {Name: "user1", Age: 10},
  1: {Name: "user2", Age: 20},
}
  • 構造体のフィールドには「タグ」といって文字列かRAW文字列リテラルでメタ情報を付与できる。RAW文字列リテラルのほうが好まれる。
  • reflectパッケージを使うと取り出せる。
type User struct {
  Name string `名前でウィス`
  Age int `年でウィス`
}
インターフェース
  • ある型がどのメソッドを実装すべきかを定義し、振る舞いを共通化できるインターフェースがGoにもある。
type error interface {
  Error() string
}
  • 代表的なのは上の組み込みのerror型で、Error() string メソッドを定義。そのため、実際は様々なエラー型が返ってくるシーンでも一括してerror型と扱い、その変数のError()関数を呼び出せる。
// 独自定義のエラー型
type MyError struct {
  Message string
  ErrorCode string
}
// errorインターフェースで指定された「メソッド」を実装
func (e *MyError) Error() string {
  return "特製のエラーメッセージを返すでウィス" + e.Message
}
// 独自定義のエラー型を返す関数 しかし戻り値の型はerrorインターフェース
func RaiseMyError() error {
  return &MyError{Message: "エラーだニャン", ErrorCode: NKB48}
}

err := RaiseMyError() // ここでは変数errはerror型と扱われる
err.Error() // 呼べる。「特製の〜」が返る
// .(ポインタ+型)の型アサーション使うと本来の型に戻る
e, ok := err.(*MyError)
if ok {
  // ここでeは構造体MyError型として扱われるのでフィールドにアクセス可能。
  e.ErrorCode // NKB48 
  // VSCodeだとドットを打つとちゃんと補完が出る。素晴らしい!
}

 やっていることは他の言語のインターフェースと大体同じですが、この例だとerrorインターフェースがMyError型の定義時に関連付けられるわけではないので、慣れるまでちょい難しそうですね。

// スペック表示ができることを示すインターフェース
type SpecShowable interface {
  ShowSpec() string
}

// 構造体パーソンと、
// SpecShowableインターフェースで実装が必須になるメソッドを定義
type Person struct {
  Name string
  Age int
}
func (p *Person) ShowSpec() string {
  return "スペック?普通なんだけど..."
}

// 構造体ロボットと、
// SpecShowableインターフェースで実装が必須になるメソッドを定義
type Robot struct {
  Number string
  Model string
}
func (r *Robot) ShowSpec() string {
  return "ナンバー:" + r.Number + "モデル:" + r.Model
}

// 型が違っても、全てSpecShowableインターフェース型で揃ったスライスとして扱える
// 構造体なのでアドレス演算子&を最初から全部つける
mens := []SpecShowable {
  &Person{Name: "ケイタ", Age: 10}
  &Robot{Number: "ROBONYAN", Model:"F型"}
}

// 1件1件がSpecShowableインターフェース型なので、共通したメソッドが呼べる!
for _, v := range vs {
  // スペックの文字列が返るので表示できる
  fmt.Println(v.ShowSpec())
  // 2件のメソッド実行結果が順に出力:
  // スペック?普通なんだけど...
  // ナンバー:ROBONYAN モデル:F型
}
  • インターフェースも名称の最初の1文字が大文字だと外部から参照可能、普通は大文字で参照可能にする。
  • 何でも入るinterface {}型は、実はメソッドが定義されていない空のインターフェースである。

 上の例は本書のサンプルをちょい変えたものですが、この形式だと自分的にもより理解できるようになりました。メソッドで表された型の性質を抽出したインターフェースを使っていろんなことが柔軟にできるようになるわけですね。
 上の例のSpecShowableインターフェースの名前そのものは構造体の定義にも紐付けられるメソッドにも出てこず、実際に使うときの型にしか出てきません。ShowSpecというメソッドが2つの構造体と紐付いているところから連想するしかありません。

 オブジェクト指向言語に慣れた身からすると、クラス定義の
class Person implements SpecShowable { ...}
class Robot implements SpecShowable { ...}
みたいな形式の方がやっぱり理解しやすいなあと思ってしまうので、頭の切り替えが必要ですね。Goのインターフェースをうまく活用したライブラリ類を見たりすると理解が深まるでしょうか。

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

Chapter 6:Goのツール ~さまざまなGoコマンド

 続いて本章はGo言語自体を入れると一緒についてくるGoツールのコマンド群について。

> go // これだけでもヘルプが出てくる
> go version
go version go1.18 windows/amd64
> go env

環境変数がずらーっと出る。$GOPATHは設定が必要だが、他はあまり気にしなくてよい。Goのインストール先は$GOROOTになる。

> go fmt [パッケージ名]

ソースコード整形がもう基本機能でついてくる。なおVSCodeでGoの拡張機能が動いているとコーディング中に同じことをやってくれる。

> go doc [パッケージ]
> go doc [パッケージ]/[パッケージ].[定数や関数]

ドキュメントを出してくれる。フォーマットはシンプルテキストでLinuxのman的な感じ。自分たちで作ったクラスの分もコメントが抽出されて表示される。JavaDocやPHPDocやJSDocのような書式はなく、単にコメントさえ書けばよい。とてもシンプル!

> go build

app/main.gosome.go がビルドされて(Windowsなら) app.exeができる。

> go build -o app.exe main.go

対象が1ファイルの場合。

> go build foo

src/foo/*.go がビルドされてパッケージのビルド。
他にファイル名を個別に指定したビルドもできるが、あまり使うシーンはない。

> go install [パッケージ名]

パッケージやソースをとってきて、内部で go buildした結果を既定の場所にインストールしてくれる。

$GOPATH
 bin/ // ビルドしてできた実行ファイルの格納先
 pkg/ //ビルドしたパッケージのインストール先
 src/
   installedpkg/ // go installでソースコードの格納先
   foo/ //ほかのパッケージ
   sample_project1/ // プロジェクトのパッケージとか...

$GOPATH配下にOSごとにディレクトリが作られることなど、内部的な話も本書では解説されています。

> go get [オプションやパッケージ名など]

外部パッケージのダウンロードとインストール。
Go環境構築時に最初にGitHubからいろいろ取ってくるのは昔はこのコマンドでしたが、その後 Go1.17から go installに変わっています。

> go test ./foo

fooパッケージ配下のbar.go, baz.go, この2つのファイルのテストコード barbaz_test.go のような構成で*_test.goのテストを実行してくれる。
Goは1ファイル=1クラスなどの決まりがないので、1ファイル=1テストファイルのような縛りもない。

> cd foo
> go test

パッケージ内に移動して実行するとそのパッケージを全テスト。
その他-vで詳細、-coverカバレッジが出せたりする。

> cd ..
> go test ./...

全パッケージの全テスト。(本書でなくUdemy講座に書いてあったコマンド例です。)

コードのコメントはちゃんと書く派としては、Goの関数コメントなどの書式はどうなのかなと思っていたら、なんと引数と戻り値をこう書くとかの細かなルールはなく、// などで書いたものが go doc コマンドで全部吸い上げられてドキュメントになるんですね。こういうところは徹底して合理的でシンプルですね。

 go test の実行結果はテキストベースでかなりシンプルですが、言語自体がもうテストの機能を備えているのはありがたい。
 Javaで本格的にテストコードを書く場合は

app_root/
  src/
   main/
     com/hogehoge/SomeProcess.java
   test/
     com/hogehoge/SomeProcessTest.java

のように本体のクラスとテストクラスを完全に別の場所に置いたりするのですが、Goだと

app_root/
  foo/ // package foo
    foo.go
    foo_test.go

のように一緒に置いちゃうのが文化のようですね。

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

Chapter 7:Goのパッケージ~よく使われるパッケージとコーディング例

 この章は主なパッケージと中身が解説されています。パラパラと拾い読みしました。

  • このへんもGoの特長といわれますが、外部のライブラリに頼らず、Goの基本機能だけで大抵のことは解決できるようになっているという印象です。
  • ファイルやディレクトリ回りなど、関数名がUnix/Linuxコマンドに似ていて、やっぱりそちらの文化と近い位置にあるなあと。
  • net/httpを使って静的なページを表示するWebサーバー機能を実現するコード例がかなり短くて、これだけでサーバーになっちゃうんだ...!と感動しました。別途Webサーバーを立てたりする必要もないんですよね...

巻末付録:Go標準ライブラリオーバービュー

 現在では以下のURLにある、Go標準の Standard Libraryについての翻訳が付録としてついています。

pkg.go.dev

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

まとめ:Go言語が一通り学べる入門用の良著

 2016年刊行と若干古いのですが、

  • ライブラリ類のインストールはツールのgo getでなく今はgo installを使うようになっていること
  • プロジェクト新規作成時に使うパッケージ管理のGo Modulesを使う話の記述がない(2018年2月のGo1.11からの導入なので仕方ない)

の2点ぐらいで入門にはほとんど支障はありませんでした。 あちこちで他の言語の習得者向けに比較やGoの思想を解説してくれているのもありがたいですね。Amazonの書評も比較的良かったのですがその通りの手ごたえでした。プログラミング自体は知っているエンジニアがGoに入門するなら、まずはこの本あたりから始めると良いと思います。

 Go言語が学べる日本語の商業本のまとめは以下の記事でもまとめていますので、こちらもどうぞ。

iwasiman.hatenablog.com