👏

Goの公開、非公開フィールドについて

2024/10/20に公開

Goにはjavaでいうreadonlyのような、フィールドの変更を制御するような文法が存在しません。そのためGoではフィールドの公開、非公開が非常に重要な役割を持っています。Goで不変を表現したい場合、非公開なフィールドをつくり、それのゲッターを使って値を参照することが推奨されています

この記事では興味本位ですが、フィールドの公開、非公開に注目して、どういった挙動をするのかまとめました。

検証

基本形

それぞれの公開、非公開のプリミティブ型のフィールドを持っている場合は以下のようになります。

pkg/item.gp
package pkg

type Item struct {
	E string
	p string
}

func NewItem() Item {
	return Item{
		E: "created",
		p: "created",
	}
}
main.go
func main() {
	// constructor
	item := pkg.Item{
		E: "constructor",
	}
	fmt.Printf("%#v\n", item)

	// change the value of the field
	item = pkg.NewItem()
	item.E = "changed"
	fmt.Printf("%#v\n", item)

	// reference the field
	fmt.Println(item.E)
	// fmt.Println(item.p) // error
}
pkg.Item{E:"constructor", p:""}
pkg.Item{E:"changed", p:"created"}
changed

この場合、Item型のEフィールドは公開されているため、他のパッケージからアクセス可能ですが、pフィールドは非公開のため、他のパッケージからアクセスできません。インスタンス化したItemを使ってpフィールドを変更したり、参照したりはできません。当然の挙動ですね。

struct

ではstructのフィールドを持っている場合はどうでしょうか?以下のように様々なパターンを試してみましょう。

pkg/item.gp
package pkg

type Item struct {
	EE Exp
	EP prv
	pE Exp
	pP prv
}

type Exp struct {
	EField string
	pField string
}

type prv struct {
	EField string
	pField string
}

func NewItem() Item {
	e := Exp{
		EField: "created",
		pField: "created",
	}
	p := prv{
		EField: "created",
		pField: "created",
	}
	return Item{
		EE: e,
		EP: p,
		pE: e,
		pP: p,
	}
}
main.go
func main() {
	// constructor
	item := pkg.Item{
		EE: pkg.Exp{
			EField: "constructor",
		},
	}
	fmt.Printf("%#v\n", item)

	// change the value of the field
	item = pkg.NewItem()
	item.EE = pkg.Exp{
		EField: "changed",
	}
    item.EP.EField = "chaged"
	fmt.Printf("%#v\n", item)

	// reference the field
	fmt.Println(item.EE)
	fmt.Println(item.EP)
	// fmt.Println(item.pE) // error
	// fmt.Println(item.pP) // error
}
pkg.Item{EE:pkg.Exp{EField:"constructor", pField:""}, EP:pkg.prv{EField:"", pField:""}, pE:pkg.Exp{EField:"", pField:""}, pP:pkg.prv{EField:"", pField:""}}
pkg.Item{EE:pkg.Exp{EField:"changed", pField:""}, EP:pkg.prv{EField:"changed", pField:"created"}, pE:pkg.Exp{EField:"created", pField:"created"}, pP:pkg.prv{EField:"created", pField:"created"}}
{changed }
{changed created}

フィールドとして公開されているのと、フィールドの型が公開されている場合で挙動が変わります。インスタンス化の際に利用できるのは、フィールドとして公開されているかつ、その型が公開されているEEフィールドのみです。しかし参照、更新の際は型が非公開でも、フィールドが公開されているEPフィールドで可能となります。また、pEpPフィールドはどちらも非公開のため、インスタンス化の際も参照の際もエラーとなります。

公開型の埋め込み

そしてさらにGoには埋め込みという機能があります。これを使うと、フィールドの型をそのままフィールドとして持つことができます。以下のようにしてみましょう。

pkg/item.gp
package pkg

type Item struct {
	Exp
}

type Exp struct {
	EField string
	pField string
}

func NewItem() Item {
	e := Exp{
		EField: "created",
		pField: "created",
	}
	return Item{
		Exp: e,
	}
}
main.go
package main

import (
	"fmt"
	"playground/pkg"
)

func main() {
	// constructor
	item := pkg.Item{
		Exp: pkg.Exp{
			EField: "constructor",
		},
	}
	fmt.Printf("%#v\n", item)

	// change the value of the field
	item = pkg.NewItem()
	item.Exp = pkg.Exp{
		EField: "changed",
	}
	item.Exp.EField = "changed" // 上と同じ
	item.EField = "changed"     // 上と同じ
	fmt.Printf("%#v\n", item)

	// reference the field
	fmt.Println(item.Exp)
	fmt.Println(item.EField)
	// fmt.Println(item.pField) // error
	// fmt.Println(item.Exp.pField) // error
}
pkg.Item{Exp:pkg.Exp{EField:"constructor", pField:""}}
pkg.Item{Exp:pkg.Exp{EField:"changed", pField:""}}
{changed }
changed

Exp型をItem型に埋め込むことで、Exp型のフィールドをそのままItem型のフィールドとして利用できるようになります。そのため、Exp型のフィールドはItem型のフィールドとして参照、更新ができます。しかしconstructする際はEFieldを直接指定することはできず、pkgをimportする必要があります。

非公開型の埋め込み

次にprivateなフィールドを持つstructを埋め込んだ場合を試してみましょう。

pkg/item.gp
package pkg

type Item struct {
	prv
}

type prv struct {
	EField string
	pField string
}

func NewItem() Item {
	p := prv{
		EField: "created",
		pField: "created",
	}
	return Item{
		prv: p,
	}
}
main.go
func main() {
	// constructor
	item := pkg.Item{}
	fmt.Printf("%#v\n", item)

	// change the value of the field
	item = pkg.NewItem()
	item.EField = "changed" // 書き換えができる !!!
	fmt.Printf("%#v\n", item)

	// reference the field
	fmt.Println(item.EField)
	// fmt.Println(item.prv) // error
	// fmt.Println(item.pField) // error
	// fmt.Println(item.prv.pField) // error
}
pkg.Item{prv:pkg.prv{EField:"", pField:""}}
pkg.Item{prv:pkg.prv{EField:"changed", pField:"created"}}
changed

この場合の挙動が一番不思議かと思います。埋め込んでいるのは非公開のprv型であるため、コンストラクト時にはprv型のフィールドを直接指定することはできません。しかし、Item型のフィールドとしてprv型のフィールドを参照、更新することができます。

考えたこと

コンストラクトの際は利用できないのに参照や変更はできてしまうような、非公開型の埋め込みはあまり使いたくないなと思いました。型による変更の制御は、結局はフィールド自体の公開、非公開の動作に落ち着くなと思いました。

うまく使えそうだな、と思ったのは公開フィールドに非公開型をセットする方法で、非公開オブジェクト自体にはアクセスができますがその詳細には全く影響を与えられない用になっています。依然置き換えはできてしまいますが、セットする非公開型に公開コンストラクタを用意しなければ、ポインタ型でない限りは不用意な変更はされにくそうです。
あまり自由に変更はされたくないが、ポインタでも扱わない、Entityの中のValueObjectをこういう扱いにするのは一種ありなのかなと思いました。

Discussion