3
2

More than 5 years have passed since last update.

struct内のzero valueの対処法

Last updated at Posted at 2017-06-03

フローチャート

a.png

背景

goはnull安全な言語ではない。どころか、レシーバがnilの時の処理すら書ける1

nilレシーバの活用例
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.MustCompiletemplate.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

  1. とはいえ分かり辛いのでなるべく避けたほうが良いと思う。ちなみにObjective-cではnilレシーバへのメッセージ送信は無視される。Rubyでは(method_misingでハックしなければ)NoMethodError。 

  2. 完全にnull安全である必要はないと思うが、部分的なnon-nil保証は欲しい。 func (r io.Reader non-nil)のようなイメージ。 

  3. 依存の注入について、structのembedを利用する腹案があるので、いずれまとめてみたいと思っている。 

3
2
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
3
2