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

neosnippet でプレースホルダがある場合は展開よりジャンプを優先させる

今週の Vim の細道見てて neosnippet で困った挙動があるのを思い出して、重い腰上げて help 見て設定したら解消した。 <Tab> にこんな感じで割り当ててたけど

imap <expr> <Tab> neosnippet#expandable_or_jumpable() ?
  \ "\<Plug>(neosnippet_expand_or_jump)" : "\<Tab>"
smap <expr> <Tab> neosnippet#expandable_or_jumpable() ?
  \ "\<Plug>(neosnippet_expand_or_jump)" : "\<Tab>"

これだとプレースホルダーがある時に <Tab> を押すとジャンプよりも展開を優先してしまう。 func<Tab> で展開した後、以下で <Tab> を押すと path が現在のファイルのパスに展開されてしまう (まだプレースホルダがある時は次のプレースホルダに移ってほしい)。

function! s:func(path<Tab>)
  <`0:TARGET`>
endfunction

そこで <Plug>(neosnippet_expand_or_jump) の代わりに <Plug>(neosnippet_jump_or_expand) 使うようにしたら次のプレースホルダへのジャンプを優先してくれるようになって便利。

imap <expr> <Tab> neosnippet#expandable_or_jumpable() ?
  \ "\<Plug>(neosnippet_jump_or_expand)" : "\<Tab>"
smap <expr> <Tab> neosnippet#expandable_or_jumpable() ?
  \ "\<Plug>(neosnippet_jump_or_expand)" : "\<Tab>"

Go とかでも同じように困ってて、 ok が定義されてるので以下で <Tab> を押すと

if v, ok := m["key"]; ok<Tab> {
  <`0:TARGET`>
}

以下のように展開されてしまっていた。

if v, ok := m["key"]; if !ok {
    
} {
    <`0`>
}

これは ok<Tab> が以下に展開されるため。

if !ok {
    <カーソル位置>
}

けどこれもちゃんと以下のようにジャンプを優先してくれるようになった。

if v, ok := m["key"]; ok {
    <カーソル位置>
}

panic() するか non-nil error が返ったらロールバック用の処理を実行するユーティリティ構造体 Guard を作った

こんな感じに使う。

package main

import (
    "errors"
    "fmt"
)

func main() {
    fmt.Println("start")
    if err := f(); err != nil {
        fmt.Println(err.Error())
    }
    fmt.Println("end")
}

func f() (err error) {
    fmt.Println("f() start")
    g := &Guard{}
    defer func() { err = g.Rollback(recover()) }()

    g.Add(func() {
        fmt.Println("rollback 1")
    })
    // if true {
    //     panic("panic!")
    // }
    g.Add(func() {
        fmt.Println("rollback 2")
    })

    // oops, invoke all rollback functions in reverse order...
    e := errors.New("something went wrong")
    if e != nil {
        err = g.Rollback(e)
        err = g.Rollback(e) // this won't call rollback functions twice!
        return
    }

    fmt.Println("f() end")
    return
}

// Guard invokes "rollback functions" if Rollback method received non-nil value
// (e.g. recover(), non-nil error).
//
// TODO: use mutex or something if the methods may be invoked in parallel!
type Guard struct {
    // rollback functions
    rbFuncs []func()
}

// Rollback rolls back if v is non-nil.
//
//   defer func() { err = g.Rollback(recover()) }()
//
//   // or
//
//   if e != nil {
//     err = g.Rollback(e)
//     err = g.Rollback(e) // this won't call rollback functions twice!
//     return
//   }
func (g *Guard) Rollback(v interface{}) error {
    var err error
    if e, ok := v.(error); ok {
        err = e
    } else if v != nil {
        err = fmt.Errorf("%s", v)
    }
    if err != nil {
        g.RollbackForcefully()
    }
    return err
}

// RollbackForcefully calls rollback functions in reverse order.
func (g *Guard) RollbackForcefully() {
    // Call rollback functions in reverse order
    for i := len(g.rbFuncs) - 1; i >= 0; i-- {
        g.rbFuncs[i]()
    }
    // Do not rollback twice
    g.rbFuncs = nil
}

// Add adds given rollback functions.
func (g *Guard) Add(f func()) {
    g.rbFuncs = append(g.rbFuncs, f)
}

そんなに行数無いので管理面倒になるしリポジトリは作ってない。 とりあえず Volt でちょうどこんな interface を実装した struct が沢山あって (インタプリタ的な DSL を実装中で、Value は全てのベースとなる型)

type Op interface {
  // ...

  Execute(ctx context.Context, args []Value) (ret Value, rollback func(), err error)
}

いちいちロールバック用の関数のスライス作って error や panic() の処理して… ってするのはかなり面倒になりそうだったので、構造体にまとめた。 本来の処理に注力できて良い感じに関心の分離ができてる感じ。

ちなみに Volt ではちょっと変更したバージョンを使ってて、 Rollback() で error を返す所を pkg/errorsWrap() 使うように変えてる。 エラーメッセージが分かりやすくなっていい感じになった (はず…確認できてないけど)。