🔐

Goでイミュータブルを実現させたい人向けの方法論

2023/02/20に公開

Goは簡単に再代入できるから嫌だ?

そう思ったことが1度くらいはありませんか?
少し現実味を出してみると、「ドメイン層にて完全コンストラクタ風で生成した構造体を他の層で再代入できるのどうにかならないか?」など思ったことはございませんか?

僕はあります。
それに対して、1つ解決策があり、今回Gopherの皆様へ提案したいです。

一言で結論: interfaceを使えば、求めていることを実現できる

実際のコードを見るのが手っ取り早いと思うので、つまり伝えたいことは下記です。

valentine/valentine.go
package valentine

import "fmt"

type numOfChocolatesPride struct {
	numOfRealChocolates      int
	numOfImaginaryChocolates int
}

type Pride interface {
	Blag() string
	Mutter() string
}

func (p numOfChocolatesPride) Blag() string {
	return fmt.Sprintf("俺、%d個しか貰えへんかった😆", p.numOfImaginaryChocolates)
}

func (p numOfChocolatesPride) Mutter() string {
	return fmt.Sprintf("((( %d個とか言えね〜 )))", p.numOfRealChocolates)
}

func NewHirochonOnValentinesDay(value int) (Pride, error) {
	if value > 5 {
		return nil, fmt.Errorf("そんなにチョコはもらえません。きっと、、、")
	}
	return &pride{
		numOfRealChocolates:      value,
		numOfImaginaryChocolates: value * 2,
	}, nil
}
main.go
package main

import (
	"fmt"

	"play.ground/valentine"
)

func main() {
	numOfChocolatesReceived := 1
	hirochonOnValentinesDay, err := valentine.NewHirochonOnValentinesDay(numOfChocolatesReceived)
	if err != nil {
		panic(err)
	}
	fmt.Println(hirochonOnValentinesDay.Blag())
	fmt.Println(hirochonOnValentinesDay.Mutter())
}
出力
俺、2個しか貰えへんかった😆
((( 1個とか言えね〜 )))

コード例(Go Playground)

ポイントは、

  1. New関数の返り値がinterfaceになっていること
  2. 実行時にinterfaceによって抽象化されたメソッドを呼び出していること

これによってNew関数による生成物は、構造体への再代入どころか、構造体に入っている値の参照もできません。

つまり、イミュータブルっぽさを表現することができました。


もしもチョコレートの数を外部パッケージから弄ろうものなら、僕の気持ちを表してくれるようにプラグラムから拒否されます。

例1: 外部パッケージからの構造体の値を書き換え

main.go
package main

import (
	"fmt"

	"play.ground/valentine"
)

func main() {
	numOfChocolatesReceived := 1
	hirochonOnValentinesDay, err := valentine.NewHirochonOnValentinesDay(numOfChocolatesReceived)
	if err != nil {
		panic(err)
	}
	hirochonOnValentinesDay.numOfRealChocolates = 10
	fmt.Println(hirochonOnValentinesDay.Blag())
	fmt.Println(hirochonOnValentinesDay.Mutter())
}
出力
./prog.go:15:26: hirochonOnValentinesDay.numOfRealChocolates undefined (type valentine.Pride has no field or method numOfRealChocolates)

上記はパッケージ外からのプライベートな構造体の値書き換えで弾かれる。

例2: 公開しているメソッドに代入などできない

main.go
package main

import (
	"fmt"

	"play.ground/valentine"
)

func main() {
	numOfChocolatesReceived := 1
	hirochonOnValentinesDay, err := valentine.NewHirochonOnValentinesDay(numOfChocolatesReceived)
	if err != nil {
		panic(err)
	}
	hirochonOnValentinesDay.Blag() = "俺、10個しか貰えへんかった😆"
	fmt.Println(hirochonOnValentinesDay.Blag())
	fmt.Println(hirochonOnValentinesDay.Mutter())
}
出力
./prog.go:15:2: cannot assign to hirochonOnValentinesDay.Blag() (value of type string)

メソッドの呼び出しに代入などできないので、エラーが発生する。

例3: 公開されているinterfaceを使って、無理やり型を作ろうとする

main.go
package main

import (
	"fmt"

	"play.ground/valentine"
)

func main() {
	numOfChocolatesReceived := 1
	hirochonOnValentinesDay, err := valentine.NewHirochonOnValentinesDay(numOfChocolatesReceived)
	if err != nil {
		panic(err)
	}
	var pride valentine.Pride
	hirochonOnValentinesDay = pride
	fmt.Println(hirochonOnValentinesDay.Blag())
	fmt.Println(hirochonOnValentinesDay.Mutter())
}
出力
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x481dd6]

goroutine 1 [running]:
main.main()
	/tmp/sandbox545031492/prog.go:17 +0x36

メソッドを定義していないので、にるぽします。

こちらエラー例で察した方もいると思いますが、仮の構造体にメソッドを定義してやれば、再代入できます

ただし、この手段を使いつつ他の実装へすり替えることができる良さこそ、interfaceの良さです。
なので、Goにおいて、値自体を簡単に再代入させないイミュータブルに近い手段としてはアリなのかなと思っております。

まとめ

というわけで、Goでイミュータブルを実現させたい方へ方法論を提案してみました!

Go自体が完全なイミュータブルを提供している訳ではない且つ、背景としてNoboNoboさんの「Goはなぜイミュータブル修飾がないの?」でも述べられている理由もあると思います。
それでもそれっぽい挙動を作りたいなーと思っている方は少なくないと思い、今回は書かせていただきました。
少しでも実装の役に立てれば幸いです。

チョコレートの数に見栄なんかハッテネーシ♪(´ε` )

Discussion