Open13

実用 Go言語 を読んでみる

tkg216tkg216

1章 「Goらしさに触れる」

パッケージ名

パッケージ名は小文字で構成される1つの単語、bytes, http, listなどとすることが慣例。

変数名

短い名前が好まれる、ただし宣言している箇所と利用箇所が離れすぎていない場合。
contextをcとしたり、短いfor文の中だと1文字の変数が使われている印象なので、納得。

panic()は使わない

基本的にerror値のチェックでエラーハンドリングする。
httpサーバーを起動するのに失敗するときはpanicを使ったりしている印象(ポートが空いていなくて処理を続けても仕方ないからだという認識)

使えるならvarよりも短縮形式:=

関数のオプション引数

すっと頭に入ってこなかったので、別記事を当たった。
https://tech.uzabase.com/entry/2022/12/19/130006
が丁寧に書かれていて良かった。他言語の例としてTypeScriptの記事の紹介も良かった(https://qiita.com/k-penguin-sato/items/e2791d7a57e96f6144e5)

  • 使われそうなパターン分のファクトリー関数を作る
    • →ユースケースが限られているなら有用
  • オプション構造体を用意する
    • →ポインタを取るためのUtility関数が必要
  • ビルダーを利用する
    • →zerologなどのロガーライブラリなどでよく見る(らしい)
  • Functional Optionパターン
    • func NewCoffee(opts ...CoffeeOptionFunc) *Coffee {} のように任意の数関数を渡せるようにする
tkg216tkg216

付録A 駆け足で学ぶGoの基礎

  • 20ページくらいで要点がまとまっていて良かった、ここから読むといいかも
tkg216tkg216

付録B 情報源

Effective Goはコードも多そうで良さそう。

tkg216tkg216

2章 定義型


  • Goでは「型」であれば何でもメソッドを持てる

アプリケーションを作成する作業を分割していくと、次の要素に分けられる

  • 提供されている基本的なツール(標準ライブラリ、文法)を組み合わせる
  • ライブラリやフレームワークを使うためのグルー(糊付け)コードを書く

Goは前者に比重があるらしい。標準ライブラリのインセンティブが高く、この辺りがコードの書きっぷりにばらつきが少なく学習コストが低い、ことにつながっているのかもしれない。


オブジェクト指向言語のようにコンストラクタが用意されていないので、「この型・構造体の機能」どうすればいいのか」を判断することが難しいことがある。
→シンプルで良いので、値を定義した型のインスタンスを返すファクトリー関数を提供しておくといい。

参考:
https://qiita.com/marurusan/items/b75ed3a48ea352670a5c

// ユーザー情報を格納する構造体
type User struct {
    Name string
    Age  int
}

// ファクトリー関数
func NewUser(name string, age int) (*User, error) {
    if name == "" {
        return nil, errors.New("name is required")
    }
    if age < 0 {
        return nil, errors.New("age must be positive")
    }

    return &User{Name: name, Age: age}, nil
}
tkg216tkg216

スライスへの型定義

type Cunsumers []Consumer

func (c Cunsumers) ActiveCunsumer() Cosumers {}

レシーバーで実装すると、実装箇所を極小化でき、テストも書きやすくなる!!
戻り値を元の型に揃えておくとメソッドチェーンで記述できるので良い感じ。

※配列とスライスを言葉上混同しているケースもある気がするけど、違うもの
https://zenn.dev/spiegel/articles/20210315-array-and-slice

tkg216tkg216

構造体への型定義
構造体を関数にそのまま渡すならレシーバーにしたほうがいい、テストもしやすくなり、再利用性も高⁨⁩まるらしい。なるべくレシーバーで実装することがGoらしいプログラミングの方法。

そうなのか?

ChatGPTに聞いてみるとわかりやすかった。

Goらしいスタイル:

Go言語では、メソッドを構造体(またはインターフェース)のレシーバーとして定義することが一般的です。これにより、その構造体に関連する機能がメソッドとしてまとまり、コードの可読性と整合性が高まります。
テストのしやすさ:

レシーバーメソッドは、構造体のインスタンスに対して直接呼び出すことができるため、テストの際にモックやスタブを使いやすくなります。特定の構造体のメソッドをテストすることで、関連するデータと処理を一元管理しやすくなります。
再利用性の向上:

構造体のメソッドとして定義することで、そのメソッドは構造体の一部として再利用されやすくなります。複数の箇所で同じ機能を使う際にも、一度定義したメソッドを使い回せるため、コードの重複を避けることができます。
tkg216tkg216

Stringer, GoStringerの2つのインターフェースを満たすことで機密情報のマスキングができる、なるほど。

tkg216tkg216

12章 ログとオブザーバビリティ

ちょっとSREっぽい話題に寄り道。

  • データ分析を前提に、構造かログが用いられるようになった
  • ログの収集方法もFluentdなどのエージェントを経由したり多様化
  • LambdaなどのサーバーレスだとCloudWatch Logsで標準出力・エラー出力を自動で集められる
  • コンテナ・マイクロサービスではログを追いかけるのが大変なのでTraceIDなどセッションで一意に決まるIDを使った共有分散トレーシングが出てくる

Goのパッケージ:

観点:

  • 必要な情報
  • セキュリティ(監査、マスキング)
  • クラウド費用

その他:

  • log.SetFlags(LstdFlags) で日時の出力
tkg216tkg216

構造化ログ

先述のzapが有名らしい。

zerologという速度を売りにしたものもある。

import (
  "github.com/rs/zerolog"
  "github.com/rs/zerolog/log"
)
  log.Printf("Hello")

だけで標準パッケージのlogの代わりになるし、構造化ログになる。良さそう。

tkg216tkg216

log.Fatalとpanic()は終了の仕方が大きく違う。
とはいえ使うのは多くなくて以下のシーンくらい

  • main()関数
  • init()関数 = main()の前に呼ばれる
  • Must接頭辞がついた関数 = errorが返ってきた時用のラップ関数
  • その他の初期化処理 (OSSで公開するならmainに返すべきとあるのであまり使わないかも)

Must知らなかったな、panicにするからあまり使うことないかもしれないけど
https://future-architect.github.io/articles/20190713/

tkg216tkg216

14章 クラウドとGo

慣れ親しんだ話が多くて安心感がある章。

  • ベースイメージの選択の表が最初わからなかったが、ビルド用とデプロイ用で最適化できる意味だった
    • →マルチステージビルドでビルドをgolang:1.x-bullseye, デプロイをgolang:1.x-bullseye-slimなどのように組み合わせられる
tkg216tkg216

13章 テスト

基礎

  • ファイル名: xx_test.go
  • 関数: Testから始まる名前、引数は t *testing.T
  • 検証
  • 失敗を知らせる: t.Error()やt.Errorf()

Table Driven Test
この辺りがわかりやすかった。 https://zenn.dev/urakawa_jinsei/articles/8de654bb2c1649

時間がかかるテスト

  • デフォルトではGOMAXPROCS値(Go1.15以降はCPUコア数が設定)だけ並列に動かせる

testify

  • sliceを含む構造体の比較に==を使うとコンパイルエラーになる
  • reflect.DeepEqual()で比較できる
  • どこに差分があるかわかりにくい→stretchr/testify

構造体の比較にgo-cmp

  • フィールドの除外などができるのが便利そう