フローチャート
背景
goはnull安全な言語ではない。どころか、レシーバがnilの時の処理すら書ける1。
package main
import "fmt"
type Name struct {
First string
Last string
}
func (n *Name) String() string {
if n == nil {
return "N/A"
}
return fmt.Sprintf("%s, %s", n.Last, n.First)
}
func main() {
name := &Name{"Taro", "Yamada"}
fmt.Println(name.String()) // => Yamada, Taro
name = nil
fmt.Println(name.String()) // => N/A
}
一方で、interfaceのzero valueがnilであり、それに対してメソッド呼び出ししてしまうと、残念ながらヌルポになってしまう。
package main
import (
"bufio"
"io"
"os"
)
type Command struct {
Input io.Reader
Output io.Writer
}
func (c *Command) Run() {
scanner := bufio.NewScanner(c.Input)
scanner.Scan()
c.Output.Write(scanner.Bytes())
}
func main() {
c := &Command{} // Oops! 初期化し忘れた。
c.Run() // => SEGV
}
さて、この問題にどう対処するべきか。
適切にエラー処理する
当然のことであるが、nilが入る以上、そのケースを考慮した処理にするべきである。
package main
import (
"bufio"
"errors"
"io"
"os"
)
type Command struct {
Input io.Reader
Output io.Writer
}
func (c *Command) Run() error {
if c.Input == nil || c.Output == nil {
return errors.New("both Input and Output should be non-nil")
}
scanner := bufio.NewScanner(c.Input)
scanner.Scan()
c.Output.Write(scanner.Bytes())
return nil
}
func main() {
c := &Command{}
fmt.Println(c.Run())
}
error処理としてpanicを使用するのは副作用が強すぎるので避けた方が良い (see 付録) 。
デフォルト値を用意する
errorに変わる手段として、デフォルト値を用意する手法もよく取られる。
package main
import (
"bufio"
"io"
"os"
)
var DefaultInput io.Reader = os.Stdin
var DefaultOutput io.Writer = os.Stdout
type Command struct {
Input io.Reader
Output io.Writer
}
func (c *Command) input() io.Reader {
if c.Input != nil {
return c.Input
}
return DefaultInput
}
func (c *Command) output() io.Writer {
if c.Output != nil {
return c.Output
}
return DefaultOutput
}
func (c *Command) Run() {
scanner := bufio.NewScanner(c.input())
scanner.Scan()
c.output().Write(scanner.Bytes())
}
func main() {
c := &Command{}
c.Run()
}
例えば http.Server は、 &http.Server{} のように使われる想定であることもあり、デフォルト値に関する記載が多い。
まとめ
Goではzero valueがカジュアルに生成されるので、それらを正しくerror処理するなり、デフォルト値を用意するなりする必要がある。
これは当たり前のようであるが、実際にやってみると、結構泥臭い記述をすることになるため、妥協しがちである。
現在の言語仕様で、この問題をスマートに解決するのは難しそうなので2、筋肉で解決するのが得策だろう3。
付録: panicしても良いケース
errorにする方が間違いないことは前提とした上で、panicが使えるケースを考えてみる。
OK: assersion的用法。例えばMust関数。
有名所で言えば regexp.MustCompile や template.Must 。
これはassersionの考え方に近い。Mustじゃない版も合わせて用意するべき。
許容: 単独の関数で、渡された引数が不正だったケース
例えば template.JSEscape 。
引数チェックを呼び出し元に求めているとも言える。
微妙: 引数により発生するが、内部状態に依存しているケース
例としては、 func (*ServeMux) Handle 。
patternが既に登録済みの時にpanicすることになっているが、少々危うい仕様。
Bad: 内部状態だけで起こるケース
上記「ヌルポの例」が該当。引数関係なく、内部状態だけで引き起こされるケース。
recover前提で組むのであれば許容されるかもしれないが、goらしくない実装になる。
付録: 冒頭の図のソース
@startuml
(*) --> "struct内のZero valueの対処"
if "デフォルト値で処理 vs. エラー処理" then
-left-> [デフォルト値で処理] "デフォルト値を用意する"
if "デフォルトを変更可能にするか?" then
--> [yes] "グローバルにexportedな形で配置する"
if "nilを入れられた時どうするか"
--> [error] "安全 :)"
else
--> [panic] "ベストではないが妥協ラインではある :("
endif
else
--> [no] "exportしない"
endif
else
-right-> [エラー処理] "エラーを適切に処理する"
if "errorを返す vs. panicにする"
--> [error] "安心 :)"
else
--> [panic] "ドキュメントに記載はするだろうが、あまりよくはない :("
endif
endif
@enduml
