Goのつらいところ
僕のTwitterでは数ヶ月おきにGoでアプリケーション開発するのつらい話をしていて、そのまとめになります
僕は一定Goのパワーを認めていつつも、特にアプリケーションの開発ではそれよりもつらい点がかなりあると思っています
今回はその理由をまとめていきます もし今あなたがGoを選定する候補の一つとしているなら参考にして頂けると嬉しいです
ちなみに、Go自体は仕事でかれこれ5年程メインで使っています
できるだけ公平な言葉で書くつもりですが不適切な表現があれば教えてください
6 reasons why
JSONの T | null | undefined 問題
Goでアプリケーションを書く時は大抵JSONを返すWebAPIになると思いますが、その時によく出くわす問題です
T | null | undefined
のJSONのフィールドを生成する時に、意図的にこれをコントロールできません
以下の記事で詳しく話されています
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) {
// ...実装
}
例えば↑のような実装があり、 interface
と struct
が別々のファイルにあった時に interface
にメソッドを追加したとします
type HogeService interface {
GetHoges(ctx context.Context, hogeID string) ([]Hoge, error)
+ SaveHoge(ctx context.Context, hogeID string) error
}
この次は InMemoryHogeService
の実装を追加しに行きたいのですが、すぐにそこへ飛ぶことが出来ません
interface
にメソッドを追加した時点で InMemoryHogeService
は HogeService
の実装では無くなってしまっているからです
泣く泣く InMemoryHogeService
で全文検索しますが、これも implements
相当のものがないのでヒットしません
そのプロジェクトに慣れてる人であればすぐに勘で飛べるかもしれませんが、入りたての人はどこに何の実装がされているのかわからないので困ります
これの対策として implements
相当の実装を追加しておくハックがあります
var _ HogeService = (*InMemoryHogeService)(nil)
これをしておくと全文検索や使用箇所の検索などでジャンプできるようになります
細かいGo特有のハックを知らないとめちゃくちゃ困る
先日はロガーDIの話で盛り上がってましたが、こういったGo特有のハックを知らないとめちゃくちゃ困る場面がたくさん出てきます
-
ctx
は最初から色んな関数の引数に入れておかないとめちゃくちゃ困る -
struct
のフィールドメンバーをNullableにしてるとif Hoge.MaybeName == nil
のように参照しようとした瞬間にパニックになる -
error
は返却するときにerrors.WithStack()
してないとめちゃくちゃ困る - table driven test はほどほどにしておかないとめちゃくちゃ困る(読みづらいので)
- Generics はみんなが求めてるようなものじゃなくておまけ程度
- 一部のライブラリの様子がおかしい(つらい)
-
oapi-codegen
- https://twitter.com/suzuesa/status/1529419630171017216?s=20
-
interface{}
って書いてあるのにmap[string]interface{}
で入ってくる箇所がある 思い出しただけでイライラしてきた 治ってたらごめん
-
gorm
- v2になって改善したみたい :tada:
-
grpc
- 前はもっとすごかった気がするけど最近まともになった?
-
go-migrate
-
dirty
にしたことがない人だけが石を投げなさい
-
-
samber/lo
- https://github.com/samber/lo
- 存在がgoへのカウンターパンチ Starが13.2kある皮肉
- P.S. forのシンタックスについては一番荒れそうなので触れないことにします
-
ポインタの配列を for range で回すとバグる
クールなポイントもご紹介
- 速い
go func()
- コミュニティがすごいやる気
- 初学者が書けるようになるまでが本当に早い
- 適当にビルドしてどこでも動く
-
go mod
まともなパッケージマネージャ -
if
とfor
しか使わない -
dd-trace
がある(超大事)
さいごに
なんやかんや文句言いながら毎日Goを書いています
世の中まともに小数点以下の計算もできない言語もある中で、これだけの不満で済んでいるのは良い言語の証なのかもしれません
しかし、僕にとっては結構ストレスに感じるポイントが多く、皆様にはそういった部分もあることを理解した上で使っていってほしいなと思います
最後に、僕はアプリケーション開発での最適言語を探っていますので良いものがあれば是非教えてください
Discussion