【Go】ログライブラリzapを使う時はglobal loggerが便利！

# はじめに
Goのログライブラリで人気なzap。
私も使っているのですが、インスタンスを引数で受け渡す必要があるのが少し面倒な気がします。

```go:main.go
package main

import "go.uber.org/zap"

func main() {
	logger, _ := zap.NewProduction()
	defer logger.Sync()
	sugar := logger.Sugar()

	// 引数で渡す
	hoge.Hoge(sugar)
}
```

```go:hoge.go
package hoge

// ログ出力したい
func Hoge(s *zap.SugaredLogger) {
	url := "http://example.com"
	s.Infof("error: %s", url)
}
```

規模が小さい時はそこまで気になりませんでしたが、大きくなるにつれて徐々に気になるようになりました。
ネストされたところでログ出力したいがために、そこへ引数リレーしていくのがちょっと辛いです。

別の使い方がないかと思って調べてみると、`global logger`を使うと解決できそうだったので、この記事で解説していきたいと思います。

# 解決策
2つありますが、私は前者推しです。

## global loggerを使う
```go:main.go
package main

import "go.uber.org/zap"

func main() {
	logger, _ := zap.NewProduction()
	defer logger.Sync()
	// こいつがキモ
	zap.ReplaceGlobals(logger)

	hoge.Hoge()
}
```

```go:hoge.go
package hoge

import "go.uber.org/zap"

// ログ出力したい
func Hoge() {
	url := "http://example.com"
	zap.S().Infof("error: %s", url)
}
```

引数渡しなしで、いい感じにログ出力を実装することができました！

### zap.ReplaceGlobalsとは
公式より引用
https://pkg.go.dev/go.uber.org/zap#example-ReplaceGlobals

> func ReplaceGlobals(logger *Logger) func()

> ReplaceGlobals replaces the global Logger and SugaredLogger, and returns a function to restore the original values. It's safe for concurrent use.

zapには`L()`や`S()`といった`global logger`を返す関数が用意されており、`ReplaceGlobals`で`global logger`を登録する形になります。

以下のzapのソースコード読んだ方が話早いかもです。
https://github.com/uber-go/zap/blob/master/global.go

```go:global.go抜粋
var (
	_globalMu sync.RWMutex
	_globalL  = NewNop()
	_globalS  = _globalL.Sugar()
)

// L returns the global Logger, which can be reconfigured with ReplaceGlobals.
// It's safe for concurrent use.
func L() *Logger {
	_globalMu.RLock()
	l := _globalL
	_globalMu.RUnlock()
	return l
}

// S returns the global SugaredLogger, which can be reconfigured with
// ReplaceGlobals. It's safe for concurrent use.
func S() *SugaredLogger {
	_globalMu.RLock()
	s := _globalS
	_globalMu.RUnlock()
	return s
}

// ReplaceGlobals replaces the global Logger and SugaredLogger, and returns a
// function to restore the original values. It's safe for concurrent use.
func ReplaceGlobals(logger *Logger) func() {
	_globalMu.Lock()
	prev := _globalL
	_globalL = logger
	_globalS = logger.Sugar()
	_globalMu.Unlock()
	return func() { ReplaceGlobals(prev) }
}
```

## パッケージスコープで変数宣言する
```go:logger.go
// zapをwrapしたようなパッケージ
package logger

import "go.uber.org/zap"

func initLogger() *zap.SugaredLogger {
	logger, _ := zap.NewProduction()
	defer logger.Sync()
	sugar := logger.Sugar()
	return sugar
}

var Sugar = initLogger()
```

```go:hoge.go
package hoge

imoprt logger

// ログ出力したい
func hoge() {
	url := "http://example.com"
	logger.Sugar.Infof("error: %s", url)
}
```

上記のように、パッケージスコープにexportedな変数`Sugar`を宣言して、それをよそのパッケージでも使うという手法です。

ただ個人的には、変数`Sugar`がどこでも上書きできてしまうのであまり好みません。
const定義もできないですしね。

というかやっていること自体は`ReplaceGlobals`とほぼ一緒で、自前で用意するかパッケージで用意されているものを使うかの違いですね。

# 公式の推しは引数リレーっぽい
この問題にぶち当たって色々調べている時に、以下のissueがとても参考になりました。

https://github.com/uber-go/zap/issues/717

contributerの方が以下のように発言されていました。

> Zap prefers not to provide implicit default global behavior, favoring explicit
configuration in your main().

> The default global logger used by zap.L() and zap.S() is a no-op logger.
To configure the global loggers, you must use ReplaceGlobals.

> The approach we recommend is to not rely on global loggers at all. Instead,
build a logger in your main() and pass it down to the components that need
to perform logging operations explicitly.

> we believe that it's cleaner software design to not rely on any
global state in our applications. We believe that the verbosity trade-off is
worth it. The trade-off of Zap may not be worth it for simple systems.

> By "pass it down" I meant to instantiate the logger in your main(), not in
init(), and then passing it explicitly as an argument to all components that
need it, without ever using zap.ReplaceGlobals.

ソフトウェア設計の観点から、loggerを必要とする関数に明示的な引数として渡してあげた方が良い、といった趣旨のコメントです。

まあわかるけどちょっと規模大きくなるとめんどうだな。。というのが私見です。
もちろんPJの状況に合わせて選定していくのが一番なので、一概にどちらが良い悪いはないと思います。

# さいごに
Twitterの方でも、モダンな技術習得やサービス開発の様子を発信したりしているので良かったらチェックしてみてください！

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">Goの人気ログライブラリzapに関する記事書いた！<br><br>⬇️【Go】ログライブラリzapを使う時はglobal loggerが便利！<a href="https://t.co/UGKbW9qZ7t">https://t.co/UGKbW9qZ7t</a><br><br>global loggerの日本語情報があまりなかったので書いてみた<br>zapの使い方って、READMEにはすごいシンプルにしか書いてないから参考になるかもしれない😇</p>&mdash; やぎぬ😇QOL追求エンジニア (@yagi_eng) <a href="https://twitter.com/yagi_eng/status/1391526215471624196?ref_src=twsrc%5Etfw">May 9, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

また、BOT開発を通じてGoとLINE BOTにまとめて入門する記事をZennに掲載していますので、良かったらそちらもご覧ください！

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">ZennでGoとLINE BOTの記事を書いてみました<br><br>⬇️BOT開発を通じてGoとLINE BOTにまとめて入門する<a href="https://t.co/QqsEESJMKa">https://t.co/QqsEESJMKa</a><br><br>5ステップに分け、「Hello Worldから始めて、飲食店検索ができるLINE BOTの実装まで」を解説しています<br>GoやLINE BOTに興味のある人は是非読んでみてください😇<br>24,000字超え😇</p>&mdash; やぎぬ😇行動力エンジニア (@yagi_eng) <a href="https://twitter.com/yagi_eng/status/1325027213784326152?ref_src=twsrc%5Etfw">November 7, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

# 参考
- [github.com/uber-go/zap](https://github.com/uber-go/zap)
- [zap - ReplaceGlobals](https://pkg.go.dev/go.uber.org/zap#example-ReplaceGlobals)