7
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

BeeXAdvent Calendar 2019

Day 9

Golang開発のすすめ

Last updated at Posted at 2019-12-08

はじめに

ジョインしている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.modreplace <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のエラーハンドリングについては散々他のサイトで言われている通りですが
以下のポイントを抑えておくと良いでしょう。

  1. error発生後に処理を行う余地を与えず即座に return させる。
  2. errorを作る際にはその関数内の情報をなるべくWrapする。
  3. errorをreturnさせる際には必ずそのerrorをStackさせる。
  4. 上記を楽に処理する pkg/errors を使用する。
  5. エラーログは中央集権的(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

参考サイト

  1. https://budougumi0617.github.io/2019/02/15/go-modules-on-go112/

7
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?