Go言語にデストラクタがないので何とかしたい
Goの真実
Goにはコンストラクタもデストラクタもない
ので、便宜上 コンストラクタに当たる関数は 構造体名の頭にNewのプリフィクスを入れた関数である
C言語だと init_hoge とするのが NewHogeになっただけだ
そして、そのNew関数は構造体のポインタを返す(Cでも呼び方わすれたが構造体をClassっぽく書くときに同じことをする)
結局C言語なのだ(念のため、C++ではない)
そしてデストラクタがないので、呼び出し側が終了関数を明示的に呼ばなければならない
Cだと delete_hoge とか release_hoge、teardown_hoge・・・ 呼び名が統一されてる覚えがない
Goにもデストラクタとして呼び名が統一されている気がしない
つまり、よく呼び忘れてメモリリークする!!
https://play.golang.org/p/i6-vTaTVx9Z
package main import ( "fmt" ) type File struct{ filename string } func NewFile(f string) *File{ fmt.Printf("File{%s} new&open\n", f) return &File{f} } func(s *File)Read(){ fmt.Printf("File{%s} read\n", s.filename) } func(s *File)Close(){ fmt.Printf("File{%s} close\n", s.filename) } func main() { fmt.Println("main start") file := NewFile("test") file.Read() fmt.Println("main finish") } main start File{test} new&open File{test} read main finish
上記のコードはRAII原則にのっとり、New時にリソースをOpenしているが
残念なことにライブラリを使う人がClose()を呼び忘れたため、リソースリークしている
defer
いちおうGoにはdeferがあり、関数が終了した時に実行されるファイナライザを書くことができる
https://play.golang.org/p/LPEm7oDSf2M
... file := NewFile("test") defer file.Close() file.Read() ... main start File{test} new&open File{test} read main finish File{test} close
これは便利だが、このdeferはライブラリを使う側の人間が忘れたらCloseしてくれない
構造体の方でdeferを入れたい
そう考えるかもしれないが、deferはあくまで、関数の終了時のファイナライザだ
コンストラクタにdeferを入れると当然コンストラクトした後にすぐCloseが走る
https://play.golang.org/p/aav2psenFxF
... func NewFile(f string) *File{ // file Open処理をする fmt.Printf("File{%s} new&open\n", f) file := File{f} defer file.Close() return &file } ... main start File{test} new&open File{test} close File{test} read main finish
runtime.SetFinalizer
見るからに悪手だが、ランタイムライブラリに、ガーベージコレクションが走った時に実行されるコールバックがある
x:= "hoge" runtime.SetFinalizer(&x, func(x *string){fmt.Println("SetFinalizer")}) // 上記だと即終了しGCが走らないので故意にGCを走らせ待機させる
と、上記では xがGCされる時に 次のラムダ式が呼ばれる。ラムダ式の引数はGCされるオブジェクトのポインタだ
https://play.golang.org/p/QYkVhC1uQKP
これを構造体に当てはめられないか?
コンストラクタでSetFinalizerする
コンストラクタで構造体を作成し、ポインタを使う側に返すが、このポインタにSetFinalizerを設定し
構造体がGCされるときにCloseしてみよう
念のためインスタンスを複数作る
https://play.golang.org/p/LL3ejYYcowe
package main import ( "fmt" "runtime" "time" ) type File struct{ filename string } func NewFile(f string) *File{ // file Open処理をする fmt.Printf("File{%s} new&open\n", f) file := File{f} runtime.SetFinalizer(&file, func(f *File){f.Close()}) return &file } func(s *File)Read(){ fmt.Printf("File{%s} read\n", s.filename) } func(s *File)Close(){ fmt.Printf("File{%s} close\n", s.filename) } func(s *File)Nop(){ fmt.Printf("File{%s} nop\n", s.filename) } func hoge(){ file := NewFile("test") file.Read() file2 := NewFile("test2") file2.Read() file3 := NewFile("test3") file3.Nop() } func main() { fmt.Println("main start") hoge() runtime.GC() time.Sleep(1 * time.Second) fmt.Println("main finish") } main start File{test} new&open File{test} read File{test2} new&open File{test2} read File{test3} new&open File{test3} nop File{test3} close File{test2} close File{test} close main finish
綺麗に動いてしまった・・・
レシーバーにSetFinalizerする
先ほどはコンストラクタにSetFinalizerをしたので、たぶん確実にFinalizerが走るが
特定メソッドで、レシーバーに対してSetFinalizerしたらどうなるか?
たとえばReadメソッドでやってみる
func(s *File)Read(){ runtime.SetFinalizer(s, func(f *File){f.Close()}) fmt.Printf("File{%s} read\n", s.filename) } main start File{test} new&open File{test} read File{test2} new&open File{test2} read File{test3} new&open File{test3} nop File{test2} close File{test} close main finish
すばらしい、意図したとおりに動いた(file3はReadメソッドを読んでないのでFinalizeされてない)
使うシーンがあるか不明だが、例えばOpenメソッドを呼んだ時だけCloseのFinalizerを設定
という使い方も不可能ではなさそうだ
でもね
GCされるまでは開放されないので、もし長時間GCされずに存在していたら
その間ファイルを開きっぱなしだったり、ネットワークリソースを開放しない事になる
だから、SetFinalizerでデストラクターをするのは、よくない事は明確である・・
どこを間違えたかと思ったが、Goはオブジェクト指向言語ではないというのが答えだ
本当は関数型のように書くべきなんだろうか?
Callback にすべきではないか?
コンストラクタでdeferしても、コールバックで続きの処理を行えば
コンストラクタのコンテキスト内なので問題ないはずだ・・・
https://play.golang.org/p/1ZBphVFMHKM
package main import ( "fmt" "runtime" "time" ) type File struct{ filename string } func NewFile(f string, cb func(file *File)*File) *File{ // file Open処理をする fmt.Printf("File{%s} new&open\n", f) file := File{f} defer file.Close(nil) if(cb != nil){ return cb(&file) } return &file } func(s *File)Read(cb func(file *File)*File)*File{ fmt.Printf("File{%s} read\n", s.filename) if(cb != nil){ return cb(s) } return s } func(s *File)Close(cb func(file *File)*File)*File{ fmt.Printf("File{%s} close\n", s.filename) if(cb != nil){ return cb(s) } return s } func(s *File)Nop(cb func(file *File)*File)*File{ fmt.Printf("File{%s} nop\n", s.filename) if(cb != nil){ return cb(s) } return s } func hoge(){ _ = NewFile("test", func(file *File)*File{ file.Read(func(file *File)*File{ fmt.Println("callback hell") return file }) return file }) } func main() { fmt.Println("main start") hoge() runtime.GC() time.Sleep(1 * time.Second) fmt.Println("main finish") } main start File{test} new&open File{test} read callback hell File{test} close main finish
やったぜ!!(やりたくない
もう少し実用的に
ReadやWriteの引数ちゃんと作って、もう少し実用的にします
エラーも返さないとね(エラー処理は省略
https://play.golang.org/p/XbAWauNUNyZ
package main import ( "fmt" "runtime" "time" ) type File struct { filename string } func NewFile(f string, cb func(file *File) (*File, error)) (*File, error) { // file Open処理をする fmt.Printf("File{%s} new&open\n", f) file := File{f} defer Close(&file, nil) if cb != nil { return cb(&file) } return &file, nil } func Read(file *File, len int, cb func(file *File, readed *string) (*File, error)) (*File, error) { fmt.Printf("File{%s} read\n", file.filename) data := "readed" if cb != nil { return cb(file, &data) } return file, nil } func Write(file *File, data *string, cb func(file *File, written int) (*File, error)) (*File, error) { written := len(*data) fmt.Printf("File{%s} write {%s} size={%d}\n", file.filename, *data, written) if cb != nil { return cb(file, written) } return file, nil } func Close(file *File, cb func(file *File) (*File, error)) (*File, error) { fmt.Printf("File{%s} close\n", file.filename) if cb != nil { return cb(file) } return file, nil } func Nop(file *File, cb func(file *File) (*File, error)) (*File, error) { fmt.Printf("File{%s} nop\n", file.filename) if cb != nil { return cb(file) } return file, nil } func hoge() { _, _ = NewFile("test", func(file *File) (*File, error) { Read(file, 5, func(file *File, readed *string) (*File, error) { fmt.Printf("READ{%s}\n", *readed) data :="hogehoge" Write(file, &data, func(file *File, written int) (*File, error) { fmt.Printf("WRITE [%d]bytes\n", written) return file, nil }) return file, nil }) return file, nil }) } func main() { fmt.Println("main start") hoge() runtime.GC() time.Sleep(1 * time.Second) fmt.Println("main finish") } main start File{test} new&open File{test} read READ{readed} File{test} write {hogehoge} size={8} WRITE [8]bytes File{test} close main finish
ライブラリを使う側からはもうCloseの事考えなくてよくなりました
やったね!!
しかし、コールバック地獄、エラー処理の場所・・・もっと考えなければならない事が増えてしまいました
しかもGoはJavaScriptやC#のような、クロージャを簡単に書く書式もないので、まいかいfunc ()と大変
コールバック必要なのはNewだけだったんや!
別に非同期プログラミングのようなコールバックする必要はない
GoはGoroutineという素晴らしい仕組みがあるので、同期っぽく書けばいい
わざわざ人間に不親切なコールバック地獄にする必要がない
コールバックを使った理由はNew関数を抜けるときにdeferdでCloseし忘れを防ぎたい
処理が終わるまでNew関数を抜けないためだ
ReadやWriteまでコールバックする必要がない。Newだけでよかったんだ
https://play.golang.org/p/WtixVskDI7R
package main import ( "fmt" ) type File struct { filename string } func NewFile(f string, cb func(file *File)error) ( error) { // file Open処理をする fmt.Printf("File{%s} new&open\n", f) file := File{f} defer file.Close() err := cb(&file) return err } func (s *File) Read(){ fmt.Printf("File{%s} read\n", s.filename) } func (s *File) Close() { fmt.Printf("File{%s} close\n", s.filename) } func (s *File) Nop() { fmt.Printf("File{%s} nop\n", s.filename) } func hoge() { _ = NewFile("test", func(file *File)error{ file.Read() return nil }) } func main() { fmt.Println("main start") hoge() fmt.Println("main finish") } main start File{test} new&open File{test} read File{test} close main finish
当然ちゃんとクローズされるし、コールバックは初回のNewだけなので、コードの見通しも悪くない
ただ、コールバックとして全てを中にいれなければいけない
オブジェクト指向脳だと自由に構造体を使い回したいと思うだろう
コールバックとオブジェクト指向のハイブリッド
Goにはメソッドのオーバーライドやデフォルト引数がない
可変長引数で処理できるが今回はそこは手抜きで、CallbackがnilだとClose管理はライブラリでは行わない
https://play.golang.org/p/M6g-dqUz86P
package main import ( "fmt" ) type File struct { filename string } func NewFile(f string, cb func(file *File) error) (*File, error) { // file Open処理をする fmt.Printf("File{%s} new&open\n", f) file := File{f} var err error = nil if cb != nil { defer file.Close() err = cb(&file) } return &file, err } func (s *File) Read() { fmt.Printf("File{%s} read\n", s.filename) } func (s *File) Close() { fmt.Printf("File{%s} close\n", s.filename) } func (s *File) Nop() { fmt.Printf("File{%s} nop\n", s.filename) } func hoge() { // クロージャで自動的にCloseする _,_ = NewFile("test", func(file *File) error { file.Read() return nil }) // クローズは使う側が責任持つ file2, _ := NewFile("test2", nil) defer file2.Close() file2.Read() // 忘れると当然リークします file3, _ := NewFile("test3", nil) // defer file3.Close() file3.Read() } func main() { fmt.Println("main start") hoge() fmt.Println("main finish") } main start File{test} new&open File{test} read File{test} close File{test2} new&open File{test2} read File{test3} new&open File{test3} read File{test2} close main finish
結局どれがいいのか?
Finalizerは動作はするが、GCが走るまで開放が行われないので実用的ではない
コールバック地獄はダメ!絶対!(おそらくGoはGoroutineで同期的に書く設計なので、promise/futureやasync/awaitのようなコールバック解決ライブラリはあまり出ない)
Newのコールバックは場合によっては使えると思う
でも基本は、ライブラリを使う側が責任もって後始末しなければいけないっぽい
ハイブリッドは良さそう