LoginSignup
5
4

More than 3 years have passed since last update.

GoでRAIIを満たしたい!

Last updated at Posted at 2019-08-30

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 = &MyFile{}    // 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を守れたんじゃないかな?

5
4
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
5
4