実用 Go言語-システム開発の現場...読書メモ

⭐は引用が多すぎて結局書籍の内容を書き写してしまうことへの対策。⭐が多いほど重要なのではないか、という気持ちの表れ。

「Goらしさ」に触れる
1.1 変数やパッケージ、メソッドなどに名前を付けるには
変数名
頭文字はUrlやIdなどはせずにURL, IDとするかurl, idのようにする、とのこと。
エラーの変数見栄は接尾辞Errorをつけるのが慣例とのこと。
パッケージ名
priority_queue のようなスネークケースや、 ArrayList のようなキャメルケースを用いたパッケージ名はGoらしくありません。複数の名詞で構成したくなった場合は、encoding/xml, encoding/jsonのようにフォルダを分ける、とのこと
インターフェース
接尾辞にerをつけるのが多い。
1.2 定数の使い方
1.2.1 型のない定数を定義する
1.2.2 型付きconst変数を定義する
関数の返り値をconstキーワードで定数にできない。
constは構造体のインスタンス、マップ、スライスなどの複合型を扱うことはできない
1.2.3 他言語との違い
1.2.4 定数でerror型のインスタンスを提供する
1.2.2にあるように以下はコンパイルエラー
const (
// New()関数の返り値は実行時まで決まらないのでconstにできない
ErrDatabase = errors.New("Database Error")
)
⭐
1.3 iotaを用いて列挙型を実現する
1.3.5 文字列として出力可能にする
以下のライブラリを紹介
golang.org/x/tools/cmd/stringer
github.com/alvaroloes/enumer
1.4 Goのエラーを扱う
1.5 変数は短縮形式:=とvarのどちらを使うべきか
結論から言うと、使える場所ではどんどん短縮記法を使うべきです。
とのこと。
1.6 関数のオプション引数
⭐
1.6.1 別名の関数によるオプション引数
必要な数だけ初期化処理のバリエーションを増やす方法
os.OpenFile()に対するos.Open()とos.Create()、WithContextを紹介
1.6.2 構造体を利用したオプション引数
構造体を利用する方法です。http.Clientの初期化などで使われます。
1.6.3 ビルダーを利用したオプション引数
1.6.4 Functional Optionパターンを使ったオプション引数
1.6.5 どの実装方法を選択すべきか
おすすめはコード量の少ない構造体パターンをまず実装して提供することです
1.7 プログラムを制御する引数
1.7.1 コマンドライン引数
Go標準ライブラリflagパッケージの紹介
kingpin.v2を使ったサンプルを紹介
1.7.2 環境変数
1.8 メモリ起因のパフォーマンス低下を解消する
1.8.3 deferの落とし穴
Close()など、エラー処理のメソッドによってはエラーを返す可能性があるケースもあります。この場合、普通にdeferを呼ぶだけではそのエラーを取りこぼしてしまうため、無名関数で括ってそのエラーを名前付きの返り値に代入すると、呼び出し元に返すことができます。
func deferReturnSample(fname string) (err error) {
var f *os.File
f, err = os.Create(fname)
if err != nil {
return fmt.Errorf("ファイルオープンのエラー %w", err)
}
defer func() {
// Closeのエラーを拾って名前付き返り値に代入
// すでにerrに別のものが入る可能性があるときは
// さらに要注意
err = f.Close()
}()
io.WriteString(f, "deferのエラーを拾うサンプル")
return
}
1.9 文字列の結合方法
大量に文字列を結合するときは、stringsパッケージのstrings.Builderを使うのが良いでしょう
1.10 日時の取り扱い
1.10.1 日時のtime.Timeの取得
⭐
1.10.2 時間を表すtime.Duration
⭐
time.Durationには、いくつかの作成方法があります。
time.Time同士の差をSub()メソッドで計算
time.Secondなどの既存の時間のインスタンスの積により作成
1.10.3 ウェブフロントエンドとのデータの交換
⭐
GoではFormat()メソッドを使って時刻を文字列にし、time.Parse()やtime.ParseInLocation()で文字列からtime.Timeインスタンスを作成します。
既存のフォーマットがいくつか用意されています。timeパッケージの定数として定義されているtime.RFC822Zやtime.RFC3339などが利用可能です。
どれを使うかは悩みどころですが、JavaScriptとのデータ交換を考えると、time.RFC3339Nanoを使うのが良いでしょう
1.10.4 翌月を計算するときのはまりどころ
2021年5月31日の翌月を取得するために AddDate(0, 1, 0) とすると翌月の2021年6月とはなりません。正規化されて2021年7月となります。

2 定義型
2.1 型を定義してデータの不整合を防止する
2.2 既存のデータ型を拡張する
- メソッドを追加して組み込み型を拡張する
- 拡張した組み込み型で、元となる基底型の挙動を利用する
- ファクトリー関数を用意して定義した型を使いやすくする
2.3 定義型を作成してアプリケーションドメインに対応する
2.3.1 スライスへの型定義
Goではスライスに対しても定義型を作成できます。
これにより、従来スライスで取得していたデータベースなどへの問い合わせ結果を、レシーバー側に移譲できます。
type Consumers []Consumer
func (c Consumers) ActiveConsumer() Consumers {
resp := make([]Consumer, 0, len(c))
for _, v := range c {
if v.ActiveFlg {
resp = append(resp, v)
}
}
return resp
}
gets, err := GetConsumers(ctx, key)
activeConsumers := gets.ActiveConsumer()
// 契約が有効で、1ヶ月後に契約が切れる予定で、昇順にソートし、ユーザーを取得
consumers := gets.ActiveConsumer().Expires(time.Now().AddDate(0, 1, 0)).SortByExpiredAt()
2.3.2 値への型定義
2.3.3 列挙への型定義
2.3.4 構造体への型定義
引数としている構造体をなるべくレシーバーにすることを検討しよう、という紹介
メソッドにするかどうかは自分の値をつかっているかどうか、という判断をしたらいいよ、というアドバイスをどこかで見た
2.4 型の変換
Goでキャストに分類される機能には2種類あります。
型変換(type conversion)
型アサーション(type assertion)
型アサーションはインタフェースからのダウンキャストに使います。ダウンキャストというのは、抽象的な型(インタフェース)から具象型への変換です。抽象的な型に入っている実態によって成功したりしなかったりするもので、動的な実行時の型の確認が必要な機能です。これには複数への型とのマッチングを一度に行う型スイッチ(type switch)も含まれます。
2.4.1 型変換(type conversion)によって型をキャストする
2.5 機密情報を扱うフィールドを定義して出力書式をカスタマイズする
Goの独自型を利用することで機密情報を含む項目を扱いやすくする方法を紹介します。

3 構造体
3.1 構造体の基本的な使い方
構造体名の後ろに、フィールドと値のペアが書かれた波かっこを書くと(これを複合リテラル、Composite Literalと呼びます)、メモリが確保されてインスタンスが生成されます。
使い捨て構造体
func main() {
jst, _ := time.LoadLocation("Asia/Tokyo")
book := struct {
Title string
Publisher string
ISBN string
ReleasedAt time.Time
}{
Title: "Some book",
Publisher: "オライリー・ジャパン",
ISBN: "123456",
ReleasedAt: time.Date(2017, time.June, 14, 0, 0, 0, 0, jst),
}
fmt.Println(book)
}
Goの構造体の裏側ではポインター(値が格納されているメモリのアドレス)が使われています。ピリオドを使った構造体のメンバーアクセスではデリファレンス(ポインターから値の取り出し)を自動でおこなう
3.2 構造体をインスタンス化する3つの方法
// newで作成(あまり使わない)
p1 := new(Person)
// var変数宣言で作成
var p2 Person
// 複合リテラルで初期化
p3 := &Person{
FirstName: "三成",
LastName: "石田",
}
⭐ それぞれの初期値
3.2.1 ファクトリー関数
Effective Goではコンストラクタと呼ばれています
次の関数のように「New」を最初に付けた名前にするのが一般的です。
⭐メリット
// NewPerson ファクトリー関数
func NewPerson(first, last string) *Person {
return &Person{
FirstName: first,
LastName: last,
}
}
3.3 構造体にメソッドを定義する
3.3.1 値レシーバーとポインターレシーバーのどちらを使えば良いか
3.3.2 レシーバーはnilでもメソッドは呼べる
3.3.3 インスタンスからメソッドを取り出して関数型として使う
func register(h *Handler) http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("/value", h.Get) // ここのこと
return mux
}
3.3.4 クロージャを使ってメソッドを再現する
3.3.5 ジェネリクスとメソッド
メソッドではジェネリクス(型パラメータ)は使えない
参考
3.4 構造体の埋め込みで共通部分を使いまわす
⭐
type OreillyBook struct {
Book
ISBN13 string
}
ob := OreillyBook{
ISBN13: "9784873119038",
Book: Book{
Title: "Real World HTTP",
},
}
3.5 タグを使って構造体にメタデータを埋め込む
3.6 構造体を設計するポイント
3.6.1 ポインター型として扱う必要があるケース
⭐
内部にスライスやmap、ポインターなど参照型の要素を持っている場合には、基本的にポインター型でのみ扱う構造体にします。
コピーしてもフィールドのポインタを共有していることになるので、影響範囲が大きいため。
構造体をポインター型として扱う場合、フィールドの値を取り出してコピーするという組み込みの文法では問題がおきるため、コピーが必要な場合は明示的なCopy()メソッドを用意すべきです。
3.6.2 値として扱える場合
ポインター型として扱う必要があるケースの逆になりますが、値として扱う場合には、ポインターやmap、スライスなどをその構造体のメンバーにはできません。文法上エラーにはなりませんが、わかりにくいバグを誘発します。
値として扱える構造体はポインターで扱っても問題ありません。ポインターにnilを入れることで無効な値であることが表現できます。値でも「IsZero()」メソッドを用意することで、同じように表現することもあります。
3.6.3 ミュータブルな構造体とイミュータブルな構造体
Goの場合エンティティと呼ばれるような構造体はミュータブルにするのが良いでしょう。関数型にかぶれると全部イミュータブルにしたくなりますが、time.Timeのようにほぼプリミティブのようなデータやバリューオブジェクトに限って利用した方が、Goの標準ライブラリなどのエコシステムと粒度を合わせやすいと思います。
3.6.4 ゼロ値の動作を保証するかどうか
3.6.5 実装方法を選択するポイント
3.7 空の構造体を使ってゴルーチン間での通知を行う
3.8 構造体のメモリ割り当てを高速化する
sync.Poolの紹介
3.9 構造体とオブジェクト指向の違いを知る
3.9.1 構造体の用途
3.9.2 構造体の埋め込みは継承ではない
3.9.3 テンプレートメソッドパターンではなく、ストラテジーパターン
3.9.4 あえてオーバーライドを実装する

4 インタフェース
4.1 柔軟なコードを書くインタフェースの利用法
(NormalizeFileのNormalizeの引数逆な気が)
ジェネリクスとインターフェース
func Stringify[T Stringer](s []T) (ret []string) {
for _, v := range s {
ret = append(ret, v.String())
}
return ret
}
4.2 あらゆる型のデータを格納するany
any型の変数は、そのままの状態では利用できません。必ず型のキャストが必要です。
4.3 インタフェースのキャスト
4.3.1 型アサーション・型スイッチの基本の書き方
型アサーション
// ctx.Value()はinterface{]なので変換が必要
// 必ず、okで成功可否を確認すること
if s, ok := ctx.Value("favorite").(string); ok {
// sはstring型
log.Printf("私の好きなものは%sです\n", s)
}
型スイッチ
// 同じvだが、case節ごとにvの型が変わる
switch v := ctx.Value("favorite").(type) {
case string:
log.Printf("好きなものは: %s\n", v)
case int:
log.Printf("好きな数値は: %d\n", v)
case complex128:
log.Printf("好きな複素数は: %f\n", v)
default: // どれにもマッチしない場合
log.Printf("好きなものは: %v\n", v)
}
4.3.2 他のメソッドを持っているかの問い合わせ
次のコードは、渡されたオブジェクトがio.Closerインタフェースを満たしていて、満たしている場合にはClose()メソッドを呼び出すコードです。
if c, ok := r.(io.Closer); ok {
c.Close()
}
インターフェース定義を直接書くこともできるとのこと
if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) {
return true
}
4.3.3 スライスのキャスト
一括での変換はできない。
スライスの場合、要素ごとスライスに入れ直す必要がある。
4.4 インタフェースの合成
⭐
type ReadWriter interface {
Reader
Writer
}
この場合はio.Readerが満たすべきインタフェース(Read()メソッド)と、io.Writerが満たすべきインタフェース(Write()メソッド)の両方を満たさなければならないインタフェースになります。
4.5 実装を切り替えるためのさまざまな方法
if、インターフェース、DIを紹介
あらかじめ、テストを考えてすべてモック化できるようにインタフェースにしておく、という考えをしているGoユーザーは少数派です。
へー

5 エラーハンドリング
5.1 エラーの書き方
errors.New
var EOF = errors.New("EOF")
fmt.Errorf
fmt.Errorf("length must be greater than 0, lentgh = %d", length)
独自のエラー
生成するときなぜポインターである必要がある?
エラーのラップ、アンラップ
上記の loadConfigError のような他の詳細なエラーに対して情報を付与して抽象度の高いエラーを扱う構造体などを作りたい場合には、 Unwrap() メソッドを用意するのが、エラー実装者のお作法になります。
エラーの色々な比較
errors.Is(): エラーを値として比較する
独自にfmt.Errorf(... %w", err)で返してもsql.ErrNoRowsになるの?
%wがwrapすることになるから?
errors.As(): エラーを型として比較する
エラーを文字列として比較する
比較したいエラーが外部パッケージに非公開のエラーとして宣言されている場合があります。非公開のエラーの場合、アプリケーションから利用することができず、やむを得ずエラーを文字列として比較することがあります。
スタックトレースをどう出すのか
golang.org/x/xerrors を使うのが良いでしょう。
5.2 エラーハンドリングの基本テクニック
5.2.1 呼び出し元に関数の引数などの情報を付与してエラーを返す
上ではなく、下のようにしましょうという紹介。
user, err := getInvitedUserWithEmail(ctx, email)
if err != nil {
// 呼び出し先で発生したエラーをそのまま呼び出し元に返却
return err
}
user, err := getInvitedUserWithEmail(ctx, email)
if err != nil {
// 呼び出し先で発生したエラーをラップし、付加情報を付与して呼び出し元に返却
return fmt.Errorf("fail to get invited user with email(%s): %w", email, err)
}
5.2.2 ログを出力して処理を継続する
5.2.3 リトライを実施する
Songmu/retry を使用したリトライの紹介
5.2.4 リソースをクローズする
return err しても後片付けが必要な場合は defer で後処理しましょうね、という紹介。
log.Fatal() などを呼び出すとプログラムが即座に終了するため defer でリソースがクローズされません。関数の実装の中で log.Fatal() が登場したら8、9割は間違いなので、コードレビューでしっかりと指摘しましょう。
へー
5.2.5 複数のエラーをまとめる方法
https://go.dev/blog/errors-are-values の紹介
func (ew *errWriter) write(buf []byte) {
if ew.err != nil {
return
}
_, ew.err = ew.w.Write(buf)
}
ew := &errWriter{w: fd}
ew.write(p0[a:b])
ew.write(p1[c:d])
ew.write(p2[e:f])
// and so on
if ew.err != nil {
return ew.err
}
go.uber.org/multierr の紹介
複数回エラーが発生した場合は何回目のエラーでどのようなエラーが発生したかわかります
5.3 エラーのチェック忘れを予防する
5.3.1 kisielk/errcheckの利用
エラーのチェック忘れをLinterで検知する紹介
5.3.2 %w利用箇所の制限
fmt.Printf() や fmt.Sprintf()では%wは使用できない
go vet でエラーが検知できる、とのこと

6 パッケージ、モジュール
6.1 プロジェクト構成の事前知識
パッケージの命名規則は変数などと同じで、英数字とアンダースコアを使います。ただし先頭はアルファベットです。
慣習として、パッケージ名とフォルダ名と同一にすることが多いですが、変えることもあります。たとえばSQLite用パッケージのフォルダ名(リポジトリ名)はgo-sqlite3ですが、パッケージ名はsqlite3です
6.1.2 パッケージのimportが循環してはいけない
6.1.3 親パッケージと子パッケージは独立したパッケージ
6.1.4 モジュール
go mod
6.1.5 セマンティックバージョニング
6.1.6 相対パスでのimportはできない
6.1.7 internal
6.1.8 無視されるフォルダ
.と_で始まる名前と、testdataと言う名前のフォルダは、Goの処理系がコンパイル対象から除外します。たとえば静的解析ツールなどを実装していて、goのコードをテストデータとして置いておきたい場合や、テストの失敗ケースに用いる処理系でコンパイルエラーになるコードなどは、testdataフォルダ以下に置きます。
6.1.9 go gettable
6.1.10 GOPATH
「GOPATHは過去のもの」、と考えて問題ありません。
GOPATHを設定したりもされていましたが、Go Modulesが導入されてからはそのような必要はなくなりました。
6.2 Go Modulesで開発環境のパッケージを管理する
6.2.1 Go Modulesの概要
6.2.1.1 モジュールへのファイルの追加
go get
$ go get -u github.com/rs/zerolog/log
-u オプションを付けると取得しようとしているライブラリ(この場合は github.com/rs/zerolog )が依存しているライブラリについても最新のバージョンを取得できます。
更新する場合は go mod tidy
6.2.1.2 ライブラリの削除/更新
go のソースコードで不要になったライブラリ( import に現れなくなったライブラリ)の依存関係は、 go mod tidy を実行することで go.mod と go.sum から削除されます。
同様にソースコードで import して使っているけれど go.mod や go.sum には追加されていない依存関係がある場合は追加されます。
6.3 Goプロジェクトのライフサイクル
6.4 Goプロジェクトのモジュール内のパッケージ構成
1つの参考として
6.4.1 最低限のパッケージ構成
Goのパッケージングにおける最低限のルールは次の2つです。
同一フォルダ内のファイルは同一パッケージとなる
実行ファイルのエントリーポイントはmainパッケージ1となる
6.4.2 パッケージを階層化する
6.4.3 ドメイン/レイヤー vs レイヤー/ドメイン
⭐
6.4.4 開発時に使うツールの追加
ソフトウェアのビルドには不要なものの、コード生成にGo製のツールを使うことがあります。特に、go generateで使うようなstringerやgithub.com/alvaroloes/enumer、github.com/Songmu/gocreditsなどがあります。これらのツールはgo installコマンドでインストールできます。
この方法でインストールすると、go.mod に影響を与えることなく、実行バイナリをインストールできます。
実行バイナリのインストール先は環境変数 GOBIN で指定されたディレクトリ、または GOPATH/bin または HOME/go/bin となります。
⭐
6.5 Go Modulesの実践的な使い方
6.5.1 1リポジトリ、マルチモジュール構成
内部モジュールを見るための記述
module github.com/myorg/app/entrypoint
require github.com/myorg/app/storage v0.0.0
replace github.com/myorg/app/storage => ../storage
6.5.2 プライベートリポジトリのモジュールを参照する方法
GOSUMDB を off にする代わりに、環境変数 GOPRIVATE を用いて go のコマンドにプライベートなモジュールだと認識させる方法があります。
GitHubのプライベートリポジトリに直接アクセスしてモジュールをダウンロードするには git config でGitHubのアクセストークンを設定しておく必要があります。
git config --global url."https://${access_token}:x-oauth-basic@github.com/oreilly-japan/".insteadOf "https://github.com/oreilly-japan/"
6.5.3 フォークしたモジュールを参照する方法
module forkedsample
go 1.17
require github.com/rs/zerolog v1.26.1
replace github.com/rs/zerolog => github.com/myname/zerolog v1.26.1
6.5.4 モジュールのキャッシュ
GOMODCACHE
6.5.5 依存するモジュールの可用性の考慮
6.6 静的なプラグイン機構を実現する
よく意義が分からなかった
6.7 初期化の順序を制御する
6.7.1 パッケージの読み込み順
6.7.2 パッケージ内部の初期化の順序
上から順々に実行ではなく、
Goは利用順序を考慮して並び替えてから実行する
なお、パッケージレベルの変数を初期化する際に関数呼び出しなどを使うと、暗黙的にinit()関数が作られ、その中で初期化処理が実行されます。

7 Goプログラミングの環境を整備する

8 さまざまなデータフォーマット
8.1 JSONファイルを扱う
エンコードとは一般に、データをある規則に基づいて別の形式に変換することを指します。逆に変換された形式のデータを元に戻すことをデコードと言います。
Javaの文化だと、Javaのオブジェクトからバイト列に変換して出力することをシリアライズと呼び、シリアライズされた入力をJavaオブジェクトに復元することをデシリアライズと呼びます。
デコード
json.Unmarshalとjson.NewDecoderの使いわけ
io.Reader インタフェースを満たしている型のデータを元にデコードできます。io.Reader インタフェースを満たしているストリーミングなJSONデータを扱う場合は Decoder() メソッド 、そうではなく []byte 型を扱う場合は json.Unmarshal() 関数を使う、とおぼえておけば良いでしょう。
⭐
エンコード
/ "-" とすることでエンコードの対象から除いておくことができる
X func() json:"-"
JSONからGoの構造体を自動作成
GoLandにもJSONをエディタに貼り付けることで構造体を自動生成してくれる便利な機能があります。
https://www.jetbrains.com/help/go/working-with-json.html
omitempty: ゼロ値の場合にJSONのフィールドに項目を表示させないようにする
ゼロ値と区別して omitempty する -> ポインター型を使う
デコード時に未知のフィールドがある場合にエラーにする
UnmarshalJSONやMarshalJSONを使ったJSONエンコード/デコードの拡張
フィールドの値によってJSONの中身が動的に変化する場合のテクニック
動的に決まるフィールドは json.RawMessage 型としてデコードせずにバイト列のまま保持しておきます
8.2 CSVファイルを扱う
Comma-Separated Valuesという意味でのCSVには、RFC 4180(Common Format and MIME Type for Comma-Separated Values (CSV) Files)と呼ばれる仕様があります。 encoding/csv はそれにしたがって実装されています。
8.2.1 CSV形式のファイルを読み込む
8.2.2 CSV形式でファイルに書き込む
8.2.3 BOM付きファイルの扱い
BOM(ボム)とはByte Order Markの略で、通常はテキストの先頭につける数バイトのデータです。BOMを元にどのように符号化されたUnicodeかを判定します。
⭐
r := csv.NewReader(bom.NewReader(f)) // BOMの回避
8.2.4 Shift-JIS(Windows-31J)を扱うには
r := csv.NewReader(transform.NewReader(f, japanese.ShiftJIS.NewDecoder()))
8.2.5 コメントアウトされた行をスキップしたい
r := csv.NewReader(f)
r.Comment = '#' // # で始まる行をコメントとみなし、取り込みをスキップ
8.2.6 CSV行を構造体で表現する
gocsv
8.2.7 encoding/csvの設定をgocarina/gocsvに引き継ぐ
8.2.8 CSVのエンコード/デコードを拡張する
8.2.9 巨大なCSVファイルを扱いたい場合(逐次処理で書き込みたい場合)
read
write
8.2.10 マルチレイアウトCSVを処理したい
8.3 Microsoft Excelファイルを扱う
8.3.1 Excelファイルに対する書き込み・読み込み
8.3.2 複数レコードを書き込む方法
8.3.3 Excelファイルを読み取る
8.3.4 構造体へのマッピング付きで読み取る
8.4 固定長データを扱う
ianlopshire/go-fixedwidth

9 Goとリレーショナルデータベース
9.1 データベースの基本的な利用法
9.2 トランザクションを扱うには
9.3 コネクションプールのパラメーターをチューニングする
⭐
SetMaxOpenConns: 同時にデータベースへ接続できるコネクション数
SetMaxIdleConns: アイドルのコネクションとしてコネクションを保持する最大の数
SetConnMaxLifetime: 下記に説明
SetConnMaxIdleTime: コネクションがアイドル状態でいられる最大時間
新規にコネクションを確立されてからのタイムアウト時間です。アクティブなコネクションは SetConnMaxLifetime の時間が経過しても切断されるわけではありません。
いずれのパラメーターもアプリケーションのワークロードやデータベースの設定に合わせて値を決める必要があります。
9.4 クエリーをキャンセルする
9.5 アプリケーションでクエリーをロギングする
9.5.1 ドライバーを使ってクエリーをロギング
9.5.2 ラッパーのドライバーを使用してクエリーをロギング
フック(gchaincl/sqlhooks)を使った例を紹介
9.6 大量のデータをバッチインサート
9.6.1 プリペアードステートメントを使う
9.6.2 バッチインサートを使う
9.6.3 データベース組み込みの関数を使う
9.7 共通カラムをうまく扱うには
9.7.3 共通カラムを構造体の埋め込みで扱う
9.8 データベースアクセスをともなう実装のテスト
9.9 サードパーティーのライブラリを使ったあれこれ
9.9.2 スキーマドリブンでアプリケーションを開発
スキーマ情報からGoのコードを自動生成する方法
volatiletech/sqlboiler
クエリードリブンでアプリケーションを開発
kyleconroy/sqlc
9.9.4 クエリーのロギング
gorm.v2のロギングを紹介
他、アプリケーションロギング

10 HTTPサーバー
10.2 ウェブプログラミングの基本
10.2.1 HTTPサーバーを実装する
GoのHTTPサーバーを知るには net/http パッケージの主要な型、インタフェースを押さえると見通しが良くなります。大きく以下の3つです。
Handler インタフェース
HandlerFunc 型
ServeMux 型
⭐
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
type HandlerFunc func(ResponseWriter, *Request)
// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
他参考
- https://journal.lampetty.net/entry/understanding-http-handler-in-go
- https://github.com/oreilly-japan/practical-go-programming/blob/master/ch10/intro/handle.go
10.2.2 JSONデータの読み書き
10.2.3 リクエストのバリデーション
10.2.4 必須チェックのハマりどころ
これを避けるためには、必須チェックを行いたいフィールドをポインター型で定義します。
10.3 HTTPのリクエストのパース
10.3.1 クエリーのパース
10.3.2 ファイルのアップロードの処理
⭐
10.4 ルーター
10.4.1 標準ライブラリのhttp.ServeMux
10.4.2 サードパーティー製のルーター
go-chi/chiの紹介
10.5 Middlewareパターンを使って処理を分離する
10.5.1 Middlewareの仕組み
10.5.2 ステータスコードのキャプチャ
10.5.3 ハンドラー内部でのpanicの防御
10.5.4 DBのトランザクション制御
10.5.5 タイムアウト設定
h := MiddlewareLogging(http.HandlerFunc(Healthz))
http.Handle("/healthz", http.TimeoutHandler(h, 5, "request timeout"))
http.ListenAndServe(":8888", nil)
10.5.6 レートリミット(速度制限)
1秒間に1回まで
IP制限で回数制限
middleware実装参考
- https://github.com/gorilla/handlers
- https://github.com/go-chi/chi/tree/master/middleware
- https://echo.labstack.com/middleware/
10.6 シングルページアプリケーションの静的ファイルを配信する
10.7 APIドキュメントを生成する

11 HTTPクライアント
11.1 net/httpを使ったHTTPクライアントの基本
simple get - https://github.com/oreilly-japan/practical-go-programming/blob/master/ch11/intro/client/defaultclient/main.go
simple post - https://github.com/oreilly-japan/practical-go-programming/blob/master/ch11/intro/client/post/main.go
http.Client
11.2 RoundTripperインタフェースによって処理を分離する
リトライなどのリクエストやレスポンスにおける細かい制御は、http.Client の http.RoundTripper インタフェースである Transport フィールドを使って行います。
net/http
type Client struct {
Transport RoundTripper
CheckRedirect func(req *Request, via []*Request) error
Jar CookieJar
Timeout time.Duration
}
type RoundTripper interface {
RoundTrip(*Request) (*Response, error)
}
basicな拡張
11.2.1 RoundTripperでロギングする
11.2.2 RoundTripperで認証認可
11.2.3 RoundTripperでリトライ
11.3 リトライ時に考慮するべき点
11.3.1 すべてをリトライしない
11.3.2 Exponential backoff
github.com/hashicorp/go-retryablehttpを使用した例
11.3.3 Retry-Afterヘッダーによる待機時間
11.3.4 リトライするための待機処理にtime.Sleepを使わない
Context().Done() と time.After を select で待ち受けるのが良い作法です。
11.4 プロキシサーバーを突破する
11.4.1 net/httpのプロキシサーバー設定
net/http/proxy.go -> FromEnvironment
net/http/transport.go -> ProxyFromEnvironment
11.4.2 SSL証明書エラーがでる場合のプロキシサーバー対応

12 ログとオブザーバビリティ
12.1 ログをめぐる、出力の仕組みの変化
12.2 ログに出力すべき内容を決めるには
12.3 標準ライブラリでログを出力する
12.3.1 ユニットテストの中のログ出力
12.3.2 ログ出力のカスタマイズ
log.SetOutput()は出力先を設定します
file, _ := os.Create("log.txt")
log.SetOutput(io.MultiWriter(file, os.Stderr))
log.Println("ファイルと標準エラー出力に同時に出力します")
SetFlags()とSetPrefix()関数を使うことで、ファイル名を付与したり、目立つような接頭辞を付与できます。
LstdFlags: 日時を出力します(デフォルト)
Lshortfile: ファイル名と行番号を出力します
Llongfile: ファイルのフルパス(ビルドオプションで-trimpath
基本的に、logパッケージは開発の途中でのみ利用し、最終的にリポジトリにコミットする段階では削除してからコミットする方針が良いでしょう。情報の収集などに利用する、仕様書に記述するログ出力には構造化ログを使いましょう。
12.4 構造化ログを出力する
rs/zerologを紹介とのこと
12.4.1 zerologの基本とログレベル
⭐
12.4.2 zerologの基本: さまざまな情報
12.4.3 ログにエラーコードを埋め込む
12.4.4 ウェブサービスのミドルウェアでログを出力する
12.5 エラーとログ出力
12.5.1 log.Fatal()とpanic()の違い
12.5.2 これらの関数で強制終了をしても良い場所
main()関数
init()関数
Must接頭辞がついた関数
その他のアプリケーションの初期化処理
12.6 net/httpのエラーログをカスタマイズする
zerolog
io.Writeを満たす
zap
io.Writerを満たさない
常にErrorレベルとして出力されるコード例?
12.7 分散システムの動作を確認するには
テレメトリーを行うソフトウェアとして、OSSのOpenTelemetry
OpenTelemetryは次の3分野から構成されています。
複数のシステムにまたがった処理を見える化する「分散トレース」
サービスの負荷や使用しているリソース量を把握する「メトリクス」
分散トレースと連動した、キーと値で複雑な情報も表現可能な「構造化ログ」
12.7.1 Instrumentation/Exporter
12.7.2 分散トレース

13 テスト
13.1 Goのテストの書き方の基礎
13.2 Table Driven Testを実装する
TDTでは、まずはテストケースを作成します。主に以下の項目を定義します。
テスト名
関数の引数
関数の戻り値
関数がエラーを返しているかどうか
13.3 テストに事前事後の処理を追加する
テスト共通の実行前後
TestMain() 関数を利用します。 go test を実行した際、テストファイル内に TestMain() があればこれを経由して各テスト関数が順次 m.Run() で呼び出されます。ここに事前事後の処理を追加します
13.4 ヘルパー関数
testutil や testonly といった名称でテストコードとは別パッケージを作成し、 _test.go が末尾に付かない通常のGoファイルでヘルパー関数を定義します。この時、テストコードからしかimportしなければ、プロダクションコードのビルドには含まれないため問題にはなりません。
あるパッケージの機能をモックするようなヘルパーの場合は、 パッケージ名test といった名称も良く見かけます。 httptest パッケージが代表的な例でしょう。
13.5 ウェブサーバーのハンドラーをテストする
13.5.1 サーバー全体のテスト
13.5.2 ハンドラー単体のテスト
httptestパッケージのNewRequest()とNewRecorder()関数を使い、リクエストとレスポンスのインスタンスを使い、テスト対象のハンドラーを直接起動
ミドルウェアの影響を避けて呼び出すなど、ユニットテストには最適です。
13.6 テストの落とし穴の回避
13.6.2 テストの順序依存の排除
テストの順番をシャッフル
go test -shuffle=on
13.6.3 キャッシュの削除
-count=1を付与するとキャッシュがクリアされます
go test . -count=1
13.6.4 時間がかかるようになったテストへの対処
「 Model 」が含まれている関数名のテストのみが実行される
go test -run Model
go test -short
タイムアウト設定(デフォルト10分)
go test . -timeout 30m
並列実行
並列数を変更したい場合は -parallel オプション
これとは別に -p フラグが存在
-parallel は同一パッケージ内のテストに対して、 -p は複数パッケージ単位のテストに対しての並列数を指定するためのオプションです
13.7 testifyを使う
構造体の値は、構造体のすべてのフィールドが比較可能、つまり map 型や slice 型、 func 型の値を含んでいない場合に比較できます。
reflect.DeepEqual() を用いると、単純に等価演算子 == では比較できない複合型のデータも、中身をたどって判定できます
assert.Equalを使うと何が等しくないか視認しやすい、とのこと
13.8 構造体の比較にgo-cmpを使う
13.8.1 go-cmpを使う理由
13.8.2 go-cmpのTips
公開されていないフィールドの取り扱い
一方で公開されていないフィールドを比較対象から除くときは
構造体のあるフィールドを比較対象から除く
複数のオプションを指定する
go-cmp 側でソートして比較するオプション cmpopts.SortSlices()
その他
float64 型の差分を加味して、差分が一定の範囲内であれば等価とみなすような cmpopts.EquateApprox()
math.NaN() と math.NaN() の比較を等価とみなせるような cmpopts.EquateNaNs()
time に関する差分が一定の範囲内であれば等価とみなすような cmpopts.EquateApproxTime()
13.9 テストが書きにくいものをテストする
13.9.1 シンプルな入出力のテスト
コアとなるロジックはなるべく抽象化された入出力のインタフェース(io.Readerとio.Writer)に対して実装するようにしておくと、テストしやすくなります
の紹介
13.9.2 変換が必要な入出力のテスト
13.9.3 さらに複雑な入出力のテスト
13.9.4 時刻をともなうテスト
13.10 品質を保証するテストを実装する
13.10.1 ペアワイズ法を使ったテストパターンの考慮もれ防止
13.10.2 カバレッジ
go test -cover
-coverの代わりに-coverprofileオプションで出力し、 go tool cover でHTMLファイルを生成すれば、どの行が実行されていてどこが実行されていないかが一目瞭然です。
13.10.3 プロパティベーステスト
「入力は文字列である」「整数である」といった入力の特性を与えると、その特性にあった引数の組み合わせを自動的に作り出してエッジケースの問題を見つけてくれるのが「プロパティベーステスト」です。
leanovate/gopter
13.11 go testでベンチマークを取る
ベンチマークは、関数名を Benchmark ではじめて、引数に *testing.B を取ります。b.Nはベンチマーク結果が安定するまで自動的に増加する数値です。ベンチマークの試行回数を定義することなく、結果が安定するまで自動で複数回実行してくれます。
go test -benchmem -bench Benchmark cookbooktest
ベンチマーク関数名
試行回数( op )
1回の実行にかかる時間( ns/op )
1回の実行で発生するメモリアロケーションサイズ( B/op )
1回の実行で発生するメモリアロケーション回数 ( allocs/op )
ループの前にテストデータのロードなど時間のかかる初期化処理を行う場合、ループの直前で b.ResetTimer() を呼び、経過時間とメモリのアロケーション回数をリセットすることで、ループ外の処理が結果に影響を与えないようにします。
b.ResetTimer()
13.12 ドキュメントに出力するExampleをコードに記述する

14 クラウドとGo
14.1 コンテナの起動
14.2 コンテナ用イメージの作成
14.2.1 Dockerを使ったイメージのビルド
golang:1.x-bullseye と golang:1.x-bullseye-slim のように、ビルド用とデプロイ用でイメージの系統をそろえるのが無難です。また情報の多い、標準的なLinuxディストリビューションに基づくイメージの方が、トラブル発生時の問題は少なくなります。DebianとAlpineであれば、各ライブラリの実装など今までテストされてきた回数は圧倒的に前者の方が上でしょう。
latestを選択した結果、いつの間にかメジャーバージョンが変わってビルドに失敗するようになるという話も聞きます。OSのメジャーバージョンやGoのバージョンが勝手に変わると困るので、 1.17-bullseye あたりを選んでおくのが無難でしょう。Goの場合、3桁目のバージョンはセキュリティの修正が入ることが多いため、なるべくアップデートするのがおすすめです。ただし、完全なバージョン固定をしないのであれば、CIテストを設定すべきでしょう。
14.2.2 Cloud Native Buildpacksとkoによるイメージの作成
14.3 クラウドサービスにデプロイする
14.3.1 Amazon ECSへのデプロイ
copilot
$ copilot init --app ecssample --name helloapi \
--type "Load Balanced Web Service" \
--dockerfile "./Dockerfile" --deploy
14.3.2 AWS LambdaでWebサーバーを動かす
$ CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -trimpath -o lambda ./sampleapp/cmd/main.go
$ zip -j lambda.zip lambda
$ aws lambda update-function-code --profile my_profile_dev --region ap-northeast-1 --function-name dev-example-api --zip-file fileb://lambda.zip
14.3.3 Cloud RunでWebサーバーを動かす

15 クラウドのストレージ
15.1 AWS S3
15.1.1 AWS SDK for Go v2
# 15.1.2 Go CDK
Go CDK(Go Cloud Development Kit)は2018年のGoogle Cloud Nextで発表された、各クラウドサービスに対して統一的なコードでアクセスすることを目指し、Goのアプリケーションをクラウド間でポータブルにするためのライブラリです。これに類似するものとして database/sql パッケージがあるでしょう。RDBにもさまざまな製品が存在しますが、 database/sql で抽象化することで開発生産性や移植性を高めています。これをクラウドの世界で、オブジェクトストレージ、ドキュメントストア、Pub/Subなどに適用したのがGo CDKです。
⭐
15.2 Amazon DynamoDB

16 エンタープライズなGoアプリケーションと並行処理
16.1 並行処理の基本を知る
16.2 同時に動作するスレッドを1つに制限する
16.3 ゴルーチンプールを使って複数のタスクを実行
16.4 チャネルのブロッキングを中断する(何も起きていないことを検知する)
16.5 ゴルーチン間のイベント伝達
16.6 処理の分岐と待ち合わせをしたい(ファンアウト・ファンイン)
16.7 処理の待ち合わせをしたい(Future/Promiseパターン)