Humanity

Edit the world by your favorite way

Go で struct をそのまま公開するのはあんまり良くないんじゃないか

と思った。理由は以下の通り。

ずっと地味に困ってた事があって、ある struct をコンストラクタ的な関数 (例えば Foo って struct だったら NewFoo って名前の関数) 経由で生成したい、と思っても、 Go の言語仕様上普通にこんな感じで struct を作れてしまう。

package main

import "fmt"

type Editor struct {
    name string
}

// ほんとはこっちを使ってほしい
func NewEditor(name string) *editor {
    return &editor{name: name}
}

func (e *Editor) Hello() string {
    return "こんにちは、" + e.name + "です"
}

func main() {
    // NewEditor() 使わずに普通に作れてしまう
    editor := &Editor{name: "ビム"}
    fmt.Println(editor.Hello()) // => "こんにちは、ビムです"

    // こんなのも作れてしまう
    editor = &Editor{}
    fmt.Println(editor.Hello()) // => "こんにちは、です"
}

見ての通り、自分的には必須のつもりが指定してないフィールドがあっても勝手に zero value になってしまってコンパイルエラーとはならない点。 運が悪いと例えば struct に新しいフィールドを追加してもコンパイルエラーにはならずに気付かずに runtime error とかで落ちる事になったりする。 この場合 string の zero value は "" なので runtime error にはならないものの、期待した結果と異なる可能性がある。

これは自分だけのプロジェクトでやってる分には問題ないんだけど、ライブラリを提供する場合、 何か機能を付け足して struct にフィールドを足したくなった時、 すべてのフィールドに対して zero value で指定される可能性を考慮する必要がある。 つまりカジュアルに &Editor{} とかで生成させたくない、できれば struct は unexported にしてコンストラクタ的な関数で生成したいという場合がままある。このように。

package main

import "fmt"

type editor struct {
    name string
}

func NewEditor(name string) *editor {
    return &editor{name: name}
}

func (e *editor) Hello() string {
    return "こんにちは、" + e.name + "です"
}

func main() {
    // 違うパッケージの想定なので直接 editor 構造体を触ることはできない
    // editor := editor{name: "ビム"}
    editor := NewEditor("ビム")
    fmt.Println(editor.Hello()) // => "こんにちは、ビムです"

    // これは引数の数が違うためコンパイルエラーになる (YATTA)
    // editor = NewEditor()
}

けどそれをすると golint に以下のように怒られてしまう (コンパイルは通る)。

exported func NewEditor returns unexported type *main.editor, which can be annoying to use

VimALE を入れてマーカーに表示させてる様子 (うっとうしい)。

f:id:tyru:20180422230412p:plain

Lint の警告が出るのは精神衛生上よくないのでなんとかしたい。 というわけでもやもやしながらも struct を export するという選択をしてたのですが、 これを回避する方法があります。といっても Gopher には当然だろと怒られるかもしれませんが、 interface を定義して、 NewEditor() でそれを返すことです。

type Editor interface {
    Hello() string
}

func NewEditor(name string) Editor {
    return &editor{name: name}
}

NewEditor() の返り値の型を定義した interface のものに変えました。 すると golint も unexported な型を返してるとは言わないですし、副作用として GoDoc を付ける必要のある箇所が減ります。 (本来 export する必要のない struct を export してたんだから当然ですが) golint は GoDoc が付いてない export されてる interface、struct、メソッドに警告を出します。

そのため1つの interface に対して複数の struct (実装) がある場合、 以前は golint を黙らせるために各 Hello() メソッドに GoDoc が必要になりました。

package main

import "fmt"

// NewVim is ...
func NewVim() *Vim {
    return &Vim{}
}

// NewNeoVim is ...
func NewNeoVim() *NeoVim {
    return &NeoVim{}
}

// Vim is ...
type Vim struct{}

// Hello is ...
func (e *Vim) Hello() string {
    return "こんにちは、ビムです"
}

// NeoVim is ...
type NeoVim struct{}

// Hello is ...
func (e *NeoVim) Hello() string {
    return "こんにちは、ネオビムです"
}

func main() {
    // export してるからこれもできてしまう
    // editor := &Vim{}

    fmt.Println(NewVim().Hello())    // => "こんにちは、ビムです"
    fmt.Println(NewNeoVim().Hello()) // => "こんにちは、ネオビムです"
}

しかし interface を定義すれば各 struct とそのメソッドに GoDoc を付ける必要がなくなります。 以下で struct へのコメントがなくなったのが分かると思います。 大量の struct がある場合にこれがかなり苦痛だったのが改善されました。

package main

import "fmt"

// Editor is ...
type Editor interface {
    Hello() string
}

// NewVim is ...
func NewVim() Editor {
    return &vim{}
}

// NewNeoVim is ...
func NewNeoVim() Editor {
    return &neoVim{}
}

type vim struct{}

func (e *vim) Hello() string {
    return "こんにちは、ビムです"
}

type neoVim struct{}

func (e *neoVim) Hello() string {
    return "こんにちは、ネオビムです"
}

func main() {
    fmt.Println(NewVim().Hello())    // => "こんにちは、ビムです"
    fmt.Println(NewNeoVim().Hello()) // => "こんにちは、ネオビムです"
}

結論

というわけで

  • まず interface とコンストラクタ関数を定義することを考える

ことにしました。

なんか Java みたい (ファクトリ関数定義しよう、全てのプロパティに getter を生やそう) ですが、 API の互換性とか考えるならやっぱ interface が一番ですね。 もちろんもっとカジュアルな用途 (個人プロジェクトとかチーム間でノウハウ共有できてるとか) を想定するなら struct をそのまま export してもいいかもしれませんが、個人的には (リファクタリングした時にうっかり必須フィールド付け忘れとか) ミスを減らすために export する API は基本これで行こうかなーと考えてます (そもそも Go の struct 生成の仕様がゆるふわすぎる)。

ちなみに (ALE の Vim 設定)

ALE はデフォルトだとかなり頻繁な間隔で golint とかぶん回してくれてもたついてウザいので、 こんな感じに設定すると :w のタイミングでのみ golint ぶん回すようになってくれて便利。

let g:ale_lint_on_enter = 1
let g:ale_lint_on_filetype_changed = 1
let g:ale_lint_on_save = 1
let g:ale_lint_on_text_changed = 'never'
let g:ale_lint_on_insert_leave = 0