🤕

Goのつらいところ

2023/09/11に公開

僕のTwitterでは数ヶ月おきにGoでアプリケーション開発するのつらい話をしていて、そのまとめになります

僕は一定Goのパワーを認めていつつも、特にアプリケーションの開発ではそれよりもつらい点がかなりあると思っています
今回はその理由をまとめていきます もし今あなたがGoを選定する候補の一つとしているなら参考にして頂けると嬉しいです
ちなみに、Go自体は仕事でかれこれ5年程メインで使っています

できるだけ公平な言葉で書くつもりですが不適切な表現があれば教えてください

6 reasons why

JSONの T | null | undefined 問題

Goでアプリケーションを書く時は大抵JSONを返すWebAPIになると思いますが、その時によく出くわす問題です
T | null | undefined のJSONのフィールドを生成する時に、意図的にこれをコントロールできません
以下の記事で詳しく話されています
https://zenn.dev/ngicks/articles/go-json-that-can-be-t-null-or-undefined

nil の扱いをポインターにせざるを得ない

Goの値で T | null を表現する時は、その値をポインターにすることで表現します
しかし、プリミティブな値をNullableとして扱おうとした時は一度変数に入れて処理する必要があり大変です

var maybeFour *int
// これでは代入できない
maybeFour = &4

four := 4
// 一度変数に代入しポインタを取ってくる必要がある
maybeFour = &four

めんどくさいです
time になってくると nil を値として入れられないため、sql.NullTime を使って事なきを得ています
(余談ですが time のゼロ値は様子がおかしいのでnullとは扱わないように気をつけてます https://www.m3tech.blog/entry/2020/12/29/120000)

var maybeNow sql.NullTime
// これがnull扱い
maybeNow = sql.NullTime{
  Valid: false,
}

// null じゃない方
maybeNow = sql.NullTime{
  Time: time.Now(),
  Valid: true,
}

ヌルポ

Goはnull安全じゃないです
1ヶ月に数回は見てます

並列化は結構ちゃんとやる必要がある

async/await のようなシンタックスを実装している言語に比べて、GoではGoRoutineという仕組みを使用して並列化します
これは結構ちゃんと書く必要があります

// `go` を置くことで非同期のような形で実行できる
go func() {
  // ...処理
}()

ある関数を単純な非同期関数として実行する場合はこれでいいのですが、複数の非同期を待つ実装をする場合は

eg, ctx := errgroup.WithContext(context.TODO())
eg.Go(func() error {
  // ...処理
})
eg.Go(func() error {
  // ...処理
})

if err := eg.Wait(); err != nil {
  // どちらかの処理がエラーを返却した場合
}

このように書く必要があり、かなり明示的なシンタックスになっています
(これでも errgroup が出て簡単になった方ですが)
また、動的なリストを処理する場合はリソースのリミットを設定する必要があり

users := getUsers()

eg, ctx := errgroup.WithContext(context.TODO())
// 並列での同時実行数を制限
eg.SetLimit(20)
for _, user := range users {
  user := user
  eg.Go(func() error {
    // ...処理
  })
}

if err := eg.Wait(); err != nil {
  // ...エラー処理
}

GoRoutineは簡単のような言葉を巷で見かけますが、僕はC/C++などに比べたら簡単みたいな話だと思っていて、エンジニア側で気にすべき点があることをしっかり意識して使う必要があります
僕は宣言的なシンタックスの方が好きなので async/await の方が好きです

interface から実装に飛べなくなる問題

IDEの機能で interface の定義から実装に飛ぶことができますが、Goではinterfaceに対して何か1つでも変更を加えると実装に飛べなくなります
他言語では起こらないのですが、Goは implements 句のような明示的なinterface実装であるということを書かなくて良いので、全文検索などでも引っ掛けられません

type HogeService interface {
  GetHoges(ctx context.Context, hogeID string) ([]Hoge, error)
}

type InMemoryHogeService struct {
  hoges []Hoge
}

func (s *InMemoryHogeService) GetHoges(ctx context.Context, hogeID string) ([]Hoge, error) {
  // ...実装
}

例えば↑のような実装があり、 interfacestruct が別々のファイルにあった時に interface にメソッドを追加したとします

type HogeService interface {
  GetHoges(ctx context.Context, hogeID string) ([]Hoge, error)
+ SaveHoge(ctx context.Context, hogeID string) error
}

この次は InMemoryHogeService の実装を追加しに行きたいのですが、すぐにそこへ飛ぶことが出来ません
interface にメソッドを追加した時点で InMemoryHogeServiceHogeService の実装では無くなってしまっているからです
泣く泣く InMemoryHogeService で全文検索しますが、これも implements 相当のものがないのでヒットしません
そのプロジェクトに慣れてる人であればすぐに勘で飛べるかもしれませんが、入りたての人はどこに何の実装がされているのかわからないので困ります

これの対策として implements 相当の実装を追加しておくハックがあります

var _ HogeService = (*InMemoryHogeService)(nil)

これをしておくと全文検索や使用箇所の検索などでジャンプできるようになります

細かいGo特有のハックを知らないとめちゃくちゃ困る

https://zenn.dev/mpyw/articles/go-dont-inject-logger
先日はロガーDIの話で盛り上がってましたが、こういったGo特有のハックを知らないとめちゃくちゃ困る場面がたくさん出てきます

  • ctx は最初から色んな関数の引数に入れておかないとめちゃくちゃ困る
  • struct のフィールドメンバーをNullableにしてると if Hoge.MaybeName == nil のように参照しようとした瞬間にパニックになる
  • error は返却するときに errors.WithStack() してないとめちゃくちゃ困る
  • table driven test はほどほどにしておかないとめちゃくちゃ困る(読みづらいので)
  • Generics はみんなが求めてるようなものじゃなくておまけ程度
  • 一部のライブラリの様子がおかしい(つらい)
    • oapi-codegen
    • gorm
      • v2になって改善したみたい :tada:
    • grpc
      • 前はもっとすごかった気がするけど最近まともになった?
    • go-migrate
      • dirty にしたことがない人だけが石を投げなさい
    • samber/lo
      • https://github.com/samber/lo
      • 存在がgoへのカウンターパンチ Starが13.2kある皮肉
      • P.S. forのシンタックスについては一番荒れそうなので触れないことにします

ポインタの配列を for range で回すとバグる

https://qiita.com/uchiko/items/1c611f0db618ce9dc0a9

クールなポイントもご紹介

  • 速い
  • go func()
  • コミュニティがすごいやる気
  • 初学者が書けるようになるまでが本当に早い
  • 適当にビルドしてどこでも動く
  • go mod まともなパッケージマネージャ
  • iffor しか使わない
  • dd-trace がある(超大事)

さいごに

なんやかんや文句言いながら毎日Goを書いています
世の中まともに小数点以下の計算もできない言語もある中で、これだけの不満で済んでいるのは良い言語の証なのかもしれません
しかし、僕にとっては結構ストレスに感じるポイントが多く、皆様にはそういった部分もあることを理解した上で使っていってほしいなと思います
最後に、僕はアプリケーション開発での最適言語を探っていますので良いものがあれば是非教えてください

Discussion