🥖

Go中級者におすすめしたい『Go言語100Tips』

2024/01/05に公開
1

年末年始を利用して、『Go言語100Tips』を読みました。結論とっても良かったので、ぜひおすすめです。
https://book.impress.co.jp/books/1122101133

本記事では、本書を読んで特に良かったところや、その一部を紹介しています。

おすすめ読者

本書は、Go言語を利用している方なら誰でも読んだら楽しいものだとは思うのですが、特に(特に!)おすすめの方はこんな方!というのを自分なりに定義してみました。

それはズバリ、わたし!みたいな人です(わたしが読んでめちゃ良かったんだから、そりゃそうだよね)。

わたし自身は、1年ほど前から業務でGoをはじめています。Goの入り方としてはA Tour of Goでライトに学びはじめつつ、あとは実践的にゴリゴリ書いていたという感じです。今思うと結構雑な入り方だったかも…でもこんなものなのかな。

ちなみにそれまでメイン業務はフロントエンドを担当していたため、以降もフロントエンドとバックエンド半々で開発に入ることが多く、ずーっとGoを書き続けていたというわけではありません。ただ、1年の間に比較的に大きめの機能も体験させてもらっていたりして、一人で一通りの開発はできるのでは?という感じのステータスです。もちろん先に開発していた先輩たちのコードがあるので、そのコードをかなーり参考にしつつという感じではありますが。

で、そういうレベルのわたしにとって本書が素晴らしいなと思ったことは、実践経験を積むにつれて誰もが一度は引っかかりそうな誤認系題材を集めているところや、中級・上級へとステップアップするために必要な中核の知識を教えてくれるところの2点です。

実際に自分がハマったことのある内容や、リードエンジニアからレビューで指摘を受けたところ、改めての復習になったところ、全く全然知らなかったところ(つまり、基本的に先人のコードをパクる形なのでまだちょっと"知っている"の幅が狭かった…)などたくさんありました。

また、他言語と比べた際のGoの言語特性について触れてくれているのも、面白いなと思います。

単純であれば容易であるとは限らない

本書の冒頭部分で、Goはこのように説明されています。Goは他言語に比べて覚える機能が少なく、平易に書き始めることが可能ですが、実は多くのGoの側面を知らなければ、真に習得することはできない言語、だそうです。わたしもそれは日々書いていて感じることだったので(いわゆる雰囲気で書いてます的なやつ)、なるほどなと思ったりしました。

記事タイトルにもありますが、この書籍はGoを学びはじめた初学者におすすめなのはもちろんですが、Goをバリバリに書いている中級者が上級者への道を進み始めるのにとても勉強になる一冊かなという感じです!

印象的だったTips

ここからは、印象的だったTipsを少しだけ紹介していきます(全部紹介すると80個くらいになりますので)。本書のネタバレになっちゃうので、あまり詳しい説明は書きません。自分の感想がメインです。

No.10 型埋め込みで起こりうる問題点を意識していない

埋め込みフィールドとは、以下のこと。いわゆるembeddedというやつです。structの値で重複させたくないものがある場合に便利なので、よく使われているのを見かけます。しかし、この型埋め込みには注意が必要らしい。

struct Foo {
  Bar
}
struct Bar {
  Baz int
}

この型埋め込みをすると、外部からこのデータにアクセスする際にFoo.Bazでアクセスできることになります。つまりすべて公開されてしまうのです。もしもBazが外部に向けて公開したくないデータだったらどうするべきでしょうか? 答えは以下です。

struct Foo {
  bar Bar
}
struct Bar {
  Baz int
}

埋め込みをやめて、プライベートなものとして定義するということです。本書ではsync.Mutexを例にしてより詳しく解説をしていますが、基本的に型埋め込みは利便性のためだけに選択されるべきではない、ということが書かれていました。

No.17 8進数リテラルで混乱を招く

これは業務中に、実際に誰かが直してるのを見かけたコードであり、その時に意味を知っていたのですが、本書でも紹介されていました。

// これは0o10という意味であり、8進数の10を表してしまう
hoge := 010
// こう書くべき
hoge := 10

Goでは10進数の他にも、2進数、8進数、16進数などを扱うのですが、その表現が独特なので注意が必要です。

No.20 スライスの長さと容量を理解していない

このTipsから始まるスライスやマップのTipsについて、実は既知のものも結構あったのですが、「事前に長さがわかってたら、makeして第一引数にlenをつっこんで〜そしたらパフォーマンス的に良いらしいよネ」程度でした。スライスやマップは実際に業務で利用することも多いので。

ただ、スライスやマップの内部的な仕組みの解説をしたのち、どのくらい性能に違いがあるのかまでを綺麗に解説してもらい、めちゃくちゃに納得!という感じでした。今度から、事前に長さや容量がわかる時には絶対にこのTipsを思い出して利用するようにしようと思えました。一度定義したスライスを利用して別の変数に格納した時の参照の挙動なども、なるほど、という感じです。

No.32 rangeループでポインタ要素を使う影響を無視する

たまーにコードを見ていると、以下のように一見意味のわからないコードに遭遇することがあります。わたしは特にテーブルテストあたりで見かけていました。

type Customer struct {
  ID string
  Balance float64
}

type Store struct {
  m map[string]*Customer
}

// ある処理の中で
customers := []Customer{ ... }
for _, customer := range customers {
  // 待って、何してんのこれ…
  customer := customer
  s.m[customer.ID] = &costomer
}

本書ではなぜこのcustomer := customerという処理を入れないといけないのかを丁寧に解説してくれています。理解することにより、実装時にいつこのような処理がつかいどきになるのかがわかってくるはずなので、とってもハッピーになると思います。

No.47 deferの引数やレシーバの評価方法を無視している

わたしは元々deferの挙動理解がかなり怪しかったというのもあり、『No.54 deferでエラーを処理しない』と並んで非常に勉強になったTipsでした。実際に業務でも引っかかっており、deferのエラーをうまく扱えておらず、実際テストを走らせて「なんでこうなる…?」となった記憶があります。deferの挙動について、わたしと同じく「あれ?」と思ったことがある方であれば、めちゃなるほどTipsかなと思います。

No.47について簡単に紹介しておくと、以下の実装でdefer内の関数に割り当てられた変数はどう違いそうでしょう?という感じです。

// パターン1
func f() error {
  status := "ok"
  defer func() {
    notify(status) // statusの変数には何が格納されている?
  }
  
  if err := foo(); err != nil {
    status = "NG"
    return
  }
  
  return nil
}

// パターン2
func f() error {
  status := "ok"
  defer func() {
    notify(status) // statusの変数には何が格納されている?
  }()
  
  if err := foo(); err != nil {
    status = "NG"
    return
  }
  
  return nil
}

答えは、1は"OK"で2は"NG"です。deferにクロージャを渡すかどうかで、defer内で定義した変数がいつ定義するかが決まります。脳死でdefer notify(status)等をしていると、エラーになっているのになぜかステータスが絶対okで通知されるんだよな〜みたいな罠に出くわすわけですね。わたしは出会ったことがありますね!当時は本当に意味がわかりませんでした。

第8章〜第9章 並列処理:基本編〜実践編

ここでは紹介しきれないのですが、めちゃくちゃのめちゃで勉強になりました!!!!
わたしはGoの開発をするようになってからも、実開発であまりgoroutineや並行処理に出会うことが数えるほどだったため、あまり並列処理については得意ではありませんでした。が、本書を機会にガッと理解させてもらいました。

本書は並行処理の使い所についてはもちろんですが、使うにあたって知らなければいけない並列処理や並行処理の基礎的な概念をまるっと教えてくれました。全部すごくよかったので、ここだけ章でまとめておすすめしておきます。

並列処理の基礎的な概念から、並列処理はパフォーマンスの検証を行うことの大切さ、goroutineやcontextなど周辺機能の解説、どのような場合に競合が発生するかやその発見方法など盛りだくさんでした。

No.52 エラーを2度処理する

エラーがあると絶対debugしたいので、特に神経質にいろんなところにlogを吐きつつreturnする時にもいろんな情報をくっつけつつ…みたいなことしてないですか?わたしはしてました。

本書では、同じエラーが何度もあると逆にdebugしにくいから、きちんと設計しなさいという話をされてました。ごもっともです…。まだコード残ってるので、年明けに消しておこうと思いました。

No.79 一時的な資源をクローズしない

インターフェースでio.Closerを実装しているものと出会った時には、基本的にはクローズしないと、資源のリークになるという話でした。Goのioインターフェースが優秀なので、なんとなく「これ閉じそうだな」と思ったら閉じてたのですが、もれなく閉じるように意識しないとなと思いました。

また、それにあたり、利用する機能のインターフェースを参照することで、閉じられそうかどうかもすぐわかると思いました。このへんはGoの優れた表現力ですよね。io.Closerインターフェースは高確率でエラーを返してくるので、そのエラーも潰さないようにしないと、とかも思ったりしました。ついつい、defer a.Close()ってしてしまいますよね。

No.82 テストを分類しない

テストの章もあつめに解説されていましたが、知らないことの方が多かったです。テストについては、自分で調べるよりも自分が開発しているコードベースから学ぶ方が多かったので、自分で色々と積極的に調べることが少なかったせいだろうなと反省しています。

本書で取り扱っている題材について知らないことを知れたことが大きいのはもちろんですが、やり方によってテストの性能が何倍も上がることや、遅いテストにはスキップ方法が存在すること、テスト結果を鵜呑みにするのではなく何か別観点で影響している気配がないかを観察することの大切さを学びました。

Goのテストについては、もう少しガッと勉強したいなと思っていたりします。

おわりに

本書にはこの他にも、Goを書くにあたって知っておかなければならないことやハマりがちな罠などが多く紹介されています。100Tipsというだけあって、1Tipsずつ単独で書かれているのも、読みやすくて良いですよね。

ちなみにこんなにおすすめしておりますが、わたしは『第12章 最適化』において、CPUやメモリなどの低レイヤーががっつり入ってくる最後の章で、ちょっと理解が追いつかず思わずパラ読みになりました…。低レイヤーの前提知識がまだまだ足りないのを感じてるので、時間をおいてまた再読しようと思います。泣

Discussion

Daichi TakahashiDaichi Takahashi

No.47 deferの引数やレシーバの評価方法を無視している

上記のコード例について、2点修正が必要そうなところを見つけました。

  1. パターン1,2ともに、if分中でスコープの異なる新たなstatus変数を初期化しており、status := "ok"で初期化した変数への代入になっていない
  2. パターン1のdeferが関数呼び出しになっていない

『Go言語100Tips』はまだ読めていないので、読んでみようと思いました!