💡

【技術書まとめ】実用 Go言語 ―システム開発の現場で知っておきたいアドバイス

2024/01/31に公開

実際に業務で使われるGoの書き方について学ぶために本書に手を取った。

まえがき

  • Goは言語自体はシンプルなので、それらの組み合わせ方法をどのくらい知っているかがスキルを分けるポイントとなる
  • Goは学びやすい
    • シンプルなためキーワードが少ない
      • 「義務教育で習う漢字と語彙で書かれた、少し冗長だけれどシンプルな文章」のようなもの
    • 実装の効率が高いのにパフォーマンスが高い
      • コンパイルも早い
      • 調整ポイントも少ない
    • クラウドネイティブ言語
      • コンテナと相性がいい

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

  • 基本はCamel, Pascal
    • 略語はそのままとする(HTTPなど)
  • パッケージ名
    • ディレクトリで切っていく(encoding/jsonなど)
  • Goのconstは厳格
    • TSのconstは再代入できないだけで値の書き換えは可能
  • Goは後ろ向きの努力に手間をかけず開発者を信じて性善説で行く姿勢がある
  • iotaでenumのような列挙型が作れる
    • iotaの値はコンパイル時に決まるため、途中追加すると値が変わってしまうことに注意
  • Goのエラーは単なる値
  • panic()を呼び出すことは、ほぼない
  • ガード節を利用して例外フローを早めに処理する
  • 変数は:=形式を使う方が自然
  • 大量の文字列結合はstringパッケージのstring.Builderを使う
  • Goはtime.Timeの1つ型ですべての時間を扱う
    • フロントエンドにはFormat()でRFC339Nanoを指定して渡すと、JSのDateやDate.parse()で処理できる
      • 逆にJSのtoISOString()で生成した文字列はtime.RFC3339Nanoで指定したtime.Parse()でパースできる

2: 定義型

  • Goの世界では型は一級市民ではない
    • 一級市民: 値として扱って変数に格納したりできること
  • 構造体への操作をレシーバーとして実装するのがGoらしい

3: 構造体

  • Goは、オブジェクト指向のクラスから一歩シンプルにした構造体を提供している
  • 値型を指定したレシーバーのフィールド変数を書き換えてもエラーにはならない
  • タグを使ってメタデータを埋め込める
  • 構造体を設計するポイント
    • 使い方
      • ポインターとして使う
        • 内部に参照型要素(map,スライスなど)を持つとき
        • コピーコストの大きな複合型も持つとき
      • 値として使う
        • 参照型要素を持たない
      • 両方を許可する
    • 値として使える場合
      • イミュータブルかどうか
        • イミュータブルは関数型の考え方
        • time.Timeはイミュータブル
          • フィールドの変更を行うメソッドを呼び出すと、その変更を加えた新しいインスタンスを返す
        • イミュータブルなら戻り値を利用するコードとなる
        • ポインターにするか値にするかと似ている
        • エンティティはミュータブルにするといい
      • ゼロ値での動作を保証するか
        • 初期化せずにインスタンス化したときは、すべてのフィールドがゼロ値となる
        • ファクトリー関数以外での動作を保証しないという手段もある
    • 困ったらまずは「ポインターで扱う前提」「ミュータブルなAPIセットを提供」「特定のファクトリー関数でのみ動作(ゼロ値動作を保証しない)」がいちばんお手軽で問題も少ない
  • 空の構造体を使ってゴルーチン間での通知を行う
    • 何かが起きたことだけを伝える
    • 空の構造体であれば占有バイト数がゼロだから、消費メモリの削減になる
  • 構造体とオブジェクト指向の違い
    • Javaはフレームワークからメソッドが呼び出されるという「ハリウッド法則」になっている
      • 仕組みや開発技術を学ぶよりもフレームワークの作法を学ぶことが要求される
    • 構造体は責務がはっきりとしたやや小さなもの
    • 埋め込みは継承ではない
      • 単なるメンバーフィールドで、あくまでIs-AではなくHas-Aを便利にしたもの
        • オーバーライドはできない
  • Goはオブジェクト指向言語ではない

4: インタフェース

  • 文法定義をいくらボトムアップで学んでも、その先にどのような世界が広がるかは、他のさまざまな言語機能やその言語を使うユーザーを含んだエコシステムとの相互作用によって決まる
  • Goのインタフェースは特定のメソッドを実装しているかどうか、という点にフォーカスしている

5: エラーハンドリング

  • もっともシンプルなのがerrors.New()でエラー宣言する方法

6: パッケージ、モジュール

  • Goでは、すべてのソースコードがパッケージに属する
    • フォルダ=パッケージ
  • Go Modulesによってパッケージを管理する
    • go modコマンド
    • npmのような中央集権型のモジュール管理サービスではなく、Gitリポジトリを指定してモジュールをダウンロードする
  • 後方互換生がない場合にメジャーバージョンが上げられる
    • Goの中では名前が似ているだけの完全に別のパッケージとして扱われる
  • 相対パスでのimportはできない
  • ._で始まる名前と、testdataと言う名前のフォルダは、コンパイル対象から除外される
  • Standard Go Project Layoutは必要以上に複雑なので、標準ではない
  • Goはコンパイルすると外部依存の少ない静的にリンクされたバイナリを生成する
  • Goは利用順序を考慮して並び替えてから実行する
    • 初期化の宣言の順序に頭を使う必要がなくなる

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

8: さまざまなデータフォーマット

  • JSONファイル
    • encoding/jsonパッケージを使用する
  • 構造体の変数が意図せず非公開(小文字始まり)とならないように気をつける
  • 動的に決まるフィールドはデコードせずにjson.RawMessage型としてバイト列のまま保持しておくのがおすすめ
  • CSVファイル
    • RFC4180という仕様がある
    • encoding/csvパッケージを使う
    • BOM(Byte Order Mark)付きファイル
      • UTF-8なら0xEF 0xBB 0xBFという3バイトが付与される
      • spkg/bombom.NewReader()で回避できる
    • Shift-JISデータ
      • encoding/japanesetransformパッケージを利用する
    • 構造体で表現する
      • gocarina/gocsv

9: Goとリレーショナルデータベース

  • ORM -> database/sqlパッケージ -> DBドライバ -> DB という流れ
  • Goはすべての型がゼロ値を持つ
    • NullStringには、NUllかどうかを判別するValidフィールドがある
  • 共通カラムをアプリケーションの業務ロジックに利用するのはアンチパターン

10: HTTPサーバー

  • net/httpパッケージは、HTTPリクエストを受け取るたびに内部でクライアントとのコネクションを生成し、通信している
    • 並行して処理できる仕組みとなっている
    • ゴルーチンセーフ
  • net/httpのインターフェースと型
    • Handlerインターフェース
    • HandlerFunc型
    • ServeMux型
  • リクエストのバリデーション
    • タグで定義する
    • ゼロ値とrequiredを区別する方法
      • 必須属性をポインターに変更する
  • クエリーのパース
    • http.RequestのFormValue()メソッドで取得できる
  • ファイルのアップロード処理
    • ファイル名などはmultipart/form-data形式で送信される
    • ファイルの受信はhttp.RequestのFormFile()を使う
  • ルーター
    • マルチプレクサーとも呼ばれる
    • 標準ライブラリはhttp.ServeMux
  • 共通して実行したいい処理をミドルウェアで行う
    • 認証チェック、ロギング、トランザクション制御、タイムアウト設定など
  • APIドキュメントはスキーマ駆動でOAS(OpenAPI Specification)ファイルから生成することもできる

11: HTTPクライアント

  • SDKがない場合などに、リトライ機構などをHTTPクライアントとして実装する
    • もっとも基本的なのでhttp.Get(),http.Post()を使う方法

12: ログとオブサーバビリティ

  • 明示的にerrorを呼び出し、元に返すのがGoの基本的なエラー戦略
  • 自分でログを読む必要がある場合は、Fatal()よりもpanic()の方がいい
  • 分散システムの動作はテレメトリー手法で観測する

13: テスト

  • ファイル名の末尾が_test.goで終わる
  • 関数はTestから始まる名前にする
    • 引数は*testing.Tとする
      • tはテスト結果を記録するインターフェース
  • t.Error()t.Errorf()で失敗を知らせる
  • Table Driven Test
    • TDTは、テストの入力値と期待値をまとめて定義し、テストの実行箇所を1箇所にまとめる形式
    • {
          name: "足し算",
          args: args{
              a: 10,
          b: 2,
          operator: "+",
        },
        want: 12,
        wantErr: false,
      }
      
  • t.Run()で実行する
  • ヘルパー関数はhelper_test.goなどに書く
  • テストの順序依存はgo test -shuffle=onで排除する
  • 外界とつながるコードをテストするときは、外部との入出力をメモリへの読み書きに置き換える
    • テストが簡単になる

14: クラウドとGo

  • dockerでGoの開発環境を作る
    • $ docker image pull golang:1.17-bullseye
      $ docker container run --rm -it golang:1.17-bullseye bash root@b63a6923cfa8:/go#
      

15: クラウドのストレージ

  • S3やDynamoDBなどの説明

16: エンタープライズなGoアプリケーションと並行処理

  • Goは並行処理に強い
    • 組み込みの文法だけで並行化できる
      • goroutine
      • channel
      • select
    • ただ、80%のタスクで実装する必要がない
  • ゴルーチン
    • メソッド呼び出しの前にgoをつけるだけ
  • チャネル
    • 複数のゴルーチンから送受信しても安全が保証されているキュー
  • I/Oのコストが高い領域は、Goとの相性が良い領域
  • ゴルーチンセーフ
    • スレッドセーフと同じ意味
      • 複数のゴルーチンから同時にアクセスしてもおかしな結果にならないことを保証する
      • go test -raceでエラーを検知してくれる
  • ゴルーチンリーク
    • 中にforループがあり、無限ループしている
    • チャネルの送受信によってブロックしている

Discussion