はじめに
ジョインしているPJ内で golang+gPRC
なAPIを構築することになりGoを3ヶ月程齧ってみたので
Goの開発時に気をつければ良さそうなポイントをまとめておきます。
この記事はこれからGo開発する人、もしくは私と同じくGoで開発をし始めしばらく経った人を対象としています。
Goをしばらく使ってみて感じたところとして
Goは良い意味でも悪い意味でも自由度が少なく主張の激しい言語という印象です。
ジェネリクス型がないせいで型が違うだけの同じようなコピペコードを複数書く必要があったり
スペースの代わりにタブを強要される所も場合によっては人をイラつかせる要素ではありますが
郷(Go)に入っては郷(Go)に従え (鵜飼文敏氏) とも言われていますしそういうもんなのかなと思ってます。
開発環境について
主に開発環境やツールに関するポイント
Go Modules を使用する
Go では GOPATH
という特殊な概念がありgoのすべてのコードはサードパーティ製の
ライブラリ等も含めて $GOPATH/src
直下に置かなくてはいけません。
コードはモジュール名毎ではなくPJ毎や顧客毎にディレクトリを
分けて置きたいと思うのは職業エンジニアの常だと思うのですが
それを実現するのが Go 1.11 より追加された Go Modules
モード です。
Go Modules
モードを有効にすることでコードを置く場所を縛られなくなります。
有効にするには環境変数に GO111MODULE=on
の設定を加えます。
$ export GO111MODULE=on
ちなみに Go Module
モードはGo 1.13ではデフォルト有効になっているそうです1。
上記の環境変数は不要かもしれません。
Go Modules を使用するためには init コマンドで go.mod
ファイルを生成します。
$ go mod init <module名>
module名には 実際にそのモジュールを配置しているdomainを入力しましょう。
例えばgithub上で開発するなら github.com/tomtwinkle/go-sample
のようなパスになります。
Go Modules でローカルのリポジトリを参照する
Go Modules モードでGoland等のIDEで開発していると go.mod
の中身を見て
masterブランチにある最新のモジュールを勝手に go get
してきます。
それでは依存するlibraryもセットで開発していたりすると困ることになるので
ローカルで開発しているディレクトリを参照させたいことがあると思います。
その場合は go.mod
に replace <module> => <local directory path>
の設定を追記することで
ローカルのディレクトリを参照させることが出来ます。
module github.com/tomtwinkle/go-sample
go 1.13
replace github.com/tomtwinkle/go-lib => ../go-lib
require (
github.com/tomtwinkle/go-lib v1.0.0
)
go fmt
go vet
をpush前に行う
これは開発環境についてというより、遵守しておくと自分と他のメンバーが幸せになるルールみたいなものです。
goには標準ツールの中に go fmt
go vet
というコードフォーマット、Lintツールがあります。
リポジトリにpushする際に必ず上記2コマンドでコードをフォーマットしつつ構文誤り等を修正しておきましょう。
go fmt
はローカルで行うのが吉。
go vet
に関してはCIに組み込んでしまっても良いかもしれません。
毎度叩くのを忘れる人は ↓ の記事のようにgitのpre-commitにhookスクリプトを作っておくのが良さそうです。
Go言語のプログラムをgitで管理する時に便利なこと
内部でしか使わないパッケージは internal
の内部に置く
internal
パッケージがどのようなものかは以下サイトが参考になります。
https://mattn.kaoriya.net/software/lang/go/20150820102400.htm
- internal/hoge
- internal/fuga
のように配置すると
internal
内の hoge
fuga
パッケージは private になり外部から参照を行うとエラーになります。
Goland を使う
身も蓋もない話ですが、Goで開発するならJetBrainsのGoLandを使いましょう。
Golandそのものも使いやすいですがGo開発のためのpluginが揃っているのが魅力です。
File Watchers
pluginを使い go fmt
go vet
をファイル保存時に
自動実行してもらうだけで生産性が1.2倍くらいは向上します(個人談)。
※この記事は個人的なものです。 私の雇用者とは全く関係はありません。
開発時のコード & レビュー観点について
パッケージ構成
現在のプロジェクトは以下を参考にした上でクリーンアーキテクチャの概念を元にパッケージ構成を構築しています。
https://github.com/golang-standards/project-layout
クリーンアーキテクチャについてはここで書くには長すぎるのでまたいずれ。
上記の中で vendor
フォルダは Go Modules
を使用する場合不要です。
エラーハンドリング
Goには例外がないため他言語で書かれる try/catch
のように
その関数内で起きたErrorを全て拾うようなことは出来……なくはないが(panic/recover機構)
panicは本当にシステムを止めなければ行けないような深刻なError以外では使うべきではないと考えています。
少なくとも現段階で recover
しなきゃいけないようなコード書く機会ありませんでした。
Goのエラーハンドリングについては散々他のサイトで言われている通りですが
以下のポイントを抑えておくと良いでしょう。
- error発生後に処理を行う余地を与えず即座に
return
させる。 - errorを作る際にはその関数内の情報をなるべくWrapする。
- errorをreturnさせる際には必ずそのerrorをStackさせる。
- 上記を楽に処理する pkg/errors を使用する。
- エラーログは中央集権的(centralized error handling) になるべくmainに近い場所で出力する。
NGパターン
func main() {
ctx := context.Background()
sample(ctx)
}
func sample(ctx context.Context) {
logger := ctxzap.Extract(ctx)
user, err := getUser()
if err == nil && user != nil {
logger.Info(fmt.Sprintf("I'm %s !", user.firstName))
}
if err != nil {
logger.Error(err.Error())
}
}
getUser()
でErrorを拾った後に即座に返さずに何かしらの処理を入れている。
実際にはerror時にその処理には入らないかもしれないが
機能追加などによりそれは担保されなくなるかもしれない。
OKパターン
func main() {
ctx := context.Background()
logger := ctxzap.Extract(ctx)
err := sample(ctx)
if err != nil {
logger.Error(fmt.Sprintf("%+v", err))
}
}
func sample(ctx context.Context) error {
hoge := "sample"
err := sampleChild(ctx, hoge)
if err != nil {
return errors.WithStack(err)
}
return nil
}
func sampleChild(ctx context.Context, hoge string) error {
logger := ctxzap.Extract(ctx)
user, err := getUser(hoge)
if err != nil {
return errors.Wrapf(err, "user not found. arg=%s", hoge)
}
logger.Info(fmt.Sprintf("I'm %s !", user.firstName))
return nil
}
error を返す関数を呼んだ直後に if err != nil {}
で error の有無をチェックし
errorだった場合には即座に return しています。
この if err != nil {}
構文はGoを書くようになるとよく見かけるようになります。
エラーログ出力
エラーログ出力に関しては絶賛模索中なので、こうした方が良いみたいな意見募集中です。
ちなみに上記パターンではmainでエラーログを拾っていますが
gRPC Serverの場合は go-grpc-middleware を使用し WithUnaryServerChain
の中で
(もしくはserver-side streamを使用する場合は WithStreamServerChain
の中で)
custom interceptor を chain させその中で拾うのが良さそうです。
gRPC Serverのエラーログ出力
package main
import (
"context"
"fmt"
grpcMiddleware "github.com/grpc-ecosystem/go-grpc-middleware"
grpcZap "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap"
"github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap"
grpcCtxtags "github.com/grpc-ecosystem/go-grpc-middleware/tags"
"github.com/pkg/errors"
"go.uber.org/zap"
"google.golang.org/grpc"
"log"
"net"
customInterceptor "sample"
)
func main() {
c := zap.NewProductionConfig()
c.Level = zap.NewAtomicLevelAt(zap.DebugLevel)
logger, err := c.Build()
if err != nil {
log.Fatal(err)
}
lis, err := net.Listen("tcp", fmt.Sprintf(":%s", "50051"))
s := grpc.NewServer(
grpcMiddleware.WithUnaryServerChain(
customInterceptor.UnaryServerInterceptor(),
grpcCtxtags.UnaryServerInterceptor(grpcCtxtags.WithFieldExtractor(grpcCtxtags.CodeGenRequestFieldExtractor)),
grpcZap.UnaryServerInterceptor(logger),
),
grpcMiddleware.WithStreamServerChain(
customInterceptor.StreamServerInterceptor(),
grpcCtxtags.StreamServerInterceptor(grpcCtxtags.WithFieldExtractor(grpcCtxtags.CodeGenRequestFieldExtractor)),
grpcZap.StreamServerInterceptor(logger),
),
)
{ // user handler
// gRPC api handler
server := handler.NewUser()
// protobuf generate gRPC server
protobufInterface.RegisterUserServer(s, server)
}
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
package custom_interceptor
import (
"context"
"fmt"
"github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap"
"github.com/pkg/errors"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"google.golang.org/grpc"
)
type wrapServerStream struct {
grpc.ServerStream
c context.Context
}
type wrapCore struct {
zapcore.Core
}
func UnaryServerInterceptor() grpc.UnaryServerInterceptor {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (_ interface{}, err error) {
logger := ctxzap.Extract(ctx)
logger = logger.WithOptions(zap.WrapCore(func(c zapcore.Core) zapcore.Core {
return &wrapCore{Core: c}
}))
ctx = ctxzap.ToContext(ctx, logger)
resp, err := handler(ctx, req)
if err != nil {
logger.Error(fmt.Sprintf("%+v\n", err))
return resp, errors.WithStack(err)
}
return resp, nil
}
}
func StreamServerInterceptor() grpc.StreamServerInterceptor {
return func(srv interface{}, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
ctx := stream.Context()
logger := ctxzap.Extract(stream.Context())
logger = logger.WithOptions(zap.WrapCore(func(c zapcore.Core) zapcore.Core {
return &wrapCore{Core: c}
}))
ctx = ctxzap.ToContext(ctx, logger)
err := handler(srv, &wrapServerStream{ServerStream: stream, c: ctx})
if err != nil {
logger.Error(fmt.Sprintf("%+v\n", err))
return errors.WithStack(err)
}
return nil
}
}
関数の引数や戻り値に大きい構造体を使う場合はポインターを使う
こういうのですね。
// 引数:ポインター、戻り値:ポインターの場合
func GetSample(req *RequestSample) (*Sample, error) {}
// 引数:実体、戻り値:実体の場合
func GetSample(req RequestSample) (Sample, error) {}
構造体の大きさによる所もありますが毎回構造体をコピーするのはコストが高いです。
大きい構造体を関数に渡したり戻したりする場合はポインターで返すのが良さそうです。
特に戻り値で返す構造体はmodelになる事が多くほぼポインターで良いかなと思っています。
何でもかんでもポインターにしてしまうとシンプルではなくなるので
int
, string
などのプリミティブな値の場合はそのまま
time.Time
みたいな小さな構造体も実体のまま渡した方が使いやすいかなと思っていたり。
[]T
ではなく []*T
を使用する
どっちが良いのかなと悩んで以下の記事にたどり着きました。
https://www.reddit.com/r/golang/comments/5lheyg/returning_t_vs_t/
[]T:
Pro: Contiguous in memory with respect to T (the runtime will allocate sizeof(T) * cap), which increases cache locality. For example, a loop over []int can be very efficient and potentially even be vectorized.
Con: To access a slice element, it has to be copied, which is more expensive than passing a pointer around. Similarly, modifying an element requires copying the element, modifying it and then copying it back.
[]*T:
Pro: No copying needed in order to read/write elements.
Con: Requires an indirection to dereference the stored pointer, which can point anywhere in RAM and will be unlikely to take advantage of cache locality.
Cache locality also includes RAM prefetching; modern CPU architectures are complicated, but sequential access is generally faster than random access.
[]T
では slice の要素をコピーするため T
の構造体が大きければ大きいほど
ループ内で処理する際のコストが高くなりそうです。
[]int
と []*int
だともしかしたら前者の方が良いのかもしれません。
Context.Value
にはリクエストスコープで完結するものを入れる
皆さんは Context.Value
使ってますか?
私は未だにどこまでの範囲で使えばいいのか迷っていますが
レビューの観点で見る場合
取り敢えずリクエストスコープ内で作られ、リクエストスコープが
終わったタイミングで消えるものなら入れてもいいかなと一応定義しています。
入れてもよさそうなもの
- request の metadata (ユーザID や ユーザ名 や browser情報など)
- request 時刻
微妙だけど入れてるもの
- データベースのtransactionオブジェクト
入れちゃダメそうなもの
- アプリのバージョン情報
- データベースのconnectionオブジェクト
- ステートフルなユーザのsession情報
testify/assert.Equal()
ではなくて cmd.Diff()
を使う
testify/assert.Equal()
は 内部で reflect.DeepEqual
を使用しています。
reflect.DeepEqual
は 構造体 の unexport value (private) まで参照するため
time.Time
型が含まれた構造体同士を比較する場合など
テストを書く際に考慮しなくてはいけない事柄が増えてしまいます。
- 取得した構造体の unexport value を参照するようなケースはない
- Getter で取得するならその構造体の Getter のテストを書けばよい
- ドメイン駆動なモデルで unexport value により export value が変化するなら
そのモデルのテストを書けばよい
以上からテストのassertの観点では cmd.Diff()
を使用した方が良さそうです。
assert.Equal
import (
"github.com/stretchr/testify/assert"
"testing"
)
type Sample struct {
Public string
private int64
}
func GetSample() (*Sample, error) {
return &Sample{
Public: "a",
private: int64(1),
}, nil
}
func TestSample_GetSample(t *testing.T) {
t.Run("assert.Equal", func(t *testing.T) {
expected := &Sample{
Public: "a",
}
actual, err := GetSample()
if err != nil {
t.Error(err.Error())
}
assert.Equal(t, expected, actual)
})
}
--- FAIL: TestSample_GetSample (0.00s)
--- FAIL: TestSample_GetSample/assert.Equal (0.00s)
main_test.go:25:
Error Trace: main_test.go:25
Error: Not equal:
expected: &main.Sample{Public:"a", private:0}
actual : &main.Sample{Public:"a", private:1}
Diff:
--- Expected
+++ Actual
@@ -2,3 +2,3 @@
Public: (string) (len=1) "a",
- private: (int64) 0
+ private: (int64) 1
})
Test: TestSample_GetSample/AssertEqual
Expected :&main.Sample{Public:"a", private:0}
Actual :&main.Sample{Public:"a", private:1}
cmp.Diff()
unexport value があった場合にどう動くか option によります。
デフォルトでは unexport value があるとpanicして使いにくいため
DeepEqual
utilを作って使いやすくしています。
import (
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"reflect"
"testing"
)
type Sample struct {
Public string
private int64
}
func GetSample() (*Sample, error) {
return &Sample{
Public: "a",
private: int64(1),
}, nil
}
func TestSample_GetSample(t *testing.T) {
t.Run("cmp.Diff", func(t *testing.T) {
expected := &Sample{
Public: "a",
}
actual, err := GetSample()
if err != nil {
t.Error(err.Error())
}
DeepEqual(t, expected, actual)
})
}
func DeepEqual(t *testing.T, expected, actual interface{}, opts ...cmp.Option) bool {
t.Helper()
var opt cmp.Option
e := reflect.ValueOf(expected)
// cmpopts.IgnoreUnexported() オプション で Unexported な attribute を無視する
if e.Kind() == reflect.Ptr {
// cmpopts.IgnoreUnexported() の引数には interface の実体を指定する必要がある
opt = cmpopts.IgnoreUnexported(e.Elem().Interface())
} else {
opt = cmpopts.IgnoreUnexported(expected)
}
opts = append(opts, opt)
if diff := cmp.Diff(expected, actual, opts...); diff != "" {
t.Errorf("Diff: (-expected +actual)\n%s", diff)
return false
}
return true
}
=== RUN TestSample_GetSample/cmp.Diff
--- PASS: TestSample_GetSample (0.00s)
--- PASS: TestSample_GetSample/cmp.Diff (0.00s)
PASS