Posted at

GoでRAIIを満たしたい!


RAIIとは

Resource Acquisition Is Initialization

リソースの確保は初期化時に

という意味であるが、コンストラクタ、デストラクタのある言語では具体的に

コンストラクタでリソースを確保し、デストラクタで開放

という事になる


RAIIしていない例

エラーチェックとか無視してるけど

#include <string>

#include <stdio.h>

class Hoge{
private:
FILE *fp;
public:
void Open(const char *file){
fp = fopen(file, "r");
}

void Close(){
fclose(fp);
}
};

int main()
{
auto hoge = new Hoge();
// x ここはリソースが確保されていないのでReadWriteするとエラー
hoge->Open("xxx.txt");
// o ここはリソースが確保されている
hoge->Close(); // x Close忘れるとリソースがリークする
// x Close後にアクセスするとエラー
delete hoge;
}

色々とバグが潜む隙があるので、RAIIを行うには下記のようにします

(本当は unique_ptrなどを使うが、今回はC++については 細かく扱わない)

#include <string>

#include <stdio.h>

class Hoge{
private:
FILE *fp;
Hoge();
public:
Hoge(const char *file){
fp = fopen(file, "r");
}

virtual ~Hoge(){
fclose(fp);
}
};

int main()
{
// auto hhh = new Hoge(); // o デフォルトコンストラクタは禁止
auto hoge = new Hoge("xxx.txt");
// o ここはリソースが確保されている
delete hoge; // o デストラクタで確実に開放される
}

これで、クラスの生存期間とリソースの確保期間が一致し、不具合の潜む箇所が減りました


Goでは?

Goにはコンストラクタもデストラクタもありません


New関数

Goでは NewHoge という関数をコンストラクタとする習わしがあります


type MyFile struct {
fp *os.File
filename string
}

func NewMyFile(filename string) *MyFile{
fp, _ := os.Open(filename)
return &MyFile{filename: filename, fp: fp}
}

func (s *MyFile) Read(len int) {
// Read処理
}

func (s *MyFile) Close() {
s.fp.Close()
}

func main() {
{
mf = NewMyFile("hoge.txt") // o コンストラクタでリソース取得
mf.Read(10) // o リソース確保されている
mf.Close() // x Close忘れるとリークする
// x Closeしているのでここでアクセスしたらエラー
}
}

何個かの問題が解決したように思えますが、実は全然解決していない

例えばメイン関数でNewを使わず直接構造体生成された場合

func main() {

{
mf = &NewMyFile{} // x 直接生成された
// x リソースが確保されていない
mf.Open("xxx.txt") // o リソース確保されている
mf.Read(10) // o リソース確保されている
mf.Close() // x Close忘れるとリークする
// x Closeしているのでここでアクセスしたらエラー
}
}

と、Newを使わずに構造体を直接生成されるとアウトだ


Private構造体にする

構造体をPrivate(頭文字を小文字)にすると、別のパッケージからは直接生成不可能になる


MyFile/myFile.go

type myFile struct {

fp *os.File
filename string
}

func NewMyFile(filename string) *myFile{
fp, _ = os.Open(filename)
return &myFile{filename: filename, fp: fp}
}

func (s *myFile) Read(len int) {
// Read処理
}

func (s *myFile) Close() {
s.fp.Close()
}



main.go

func main() {

{
mf = MyFile.NewMyFile("hoge.txt") // o コンストラクタでリソース取得
mf.Read(10) // o リソース確保されている
mf.Close() // x Close忘れるとリークする
// x Closeしているのでここでアクセスしたらエラー
}
{
// mf = &MyFile.myFile{} // o Private構造体は直接生成できない
}
}



コールバックを使い開放をする

Private構造体にする前のものに改造を行う

Newxxx関数に、リソース確保したら呼び出すコールバック関数を渡す

これでNewxxx内部でリソースの確保と開放が制御できる

ここでは deferを使う

deferは、その関数の終了時に実行される関数を指定できる


type MyFile struct {
fp *os.File
filename string
}

func NewMyFile(filename string, cb func(*MyFile) error) error {
fp, err := os.Open(filename)
s := &MyFile{filename: filename, fp: fp}
if err != nil {
return err
}
defer s.fp.close()

err = cb(s)

return err
}

func (s *MyFile) Read(len int) {
// Read処理
}

func (s *MyFile) close() {
s.fp.Close()
}

func main() {
_ = NewMyFile("hoge.txt", func(mf *MyFile) error {
mf.Read(10)
return nil
})
}


private構造体とコールバックを組み合わせる

main関数から *myFile が参照できずコンパイルエラー

func main() {

_ = NewMyFile("hoge.txt", func(mf *myFile) error { // ここがエラー
mf.Read(10)
return nil
})
}


結論、Interfaceを使う

上記は *myFile がコールバック型で参照できないので、Interfaceにする


MyFile/myFile.go


type ImyFile interface {
Read(len int)
}

type myFile struct {
fp *os.File
filename string
}

func NewMyFile(filename string, cb func(ImyFile) error) error {
fp, err := os.Open(filename)
s := &myFile{filename: filename, fp: fp}
if err != nil {
return err
}
defer s.fp.Close()

err = cb(s)

return err
}

func (s *myFile) Read(len int) {
// Read処理
}



main.go

func main() {

_ = MyFile.NewMyFile("hoge.txt", func(mf MyFile.ImyFile) error { // o 初期化時にリソース確保
mf.Read(10)
return err
// o コールバック抜けた後、New関数を抜け、deferにてCloseが呼ばれるので開放忘れなし
})

// mf := &MyFile.ImyFile{} // o Interfaceは生成不可能
// mf := &MyFile.myFile{} // o Privateなので構造体を直接生成不可能
}


冗長なコードになったけど、これでGoでも RAIIを守れたんじゃないかな?