Goの公開、非公開フィールドについて
Goにはjavaでいうreadonlyのような、フィールドの変更を制御するような文法が存在しません。そのためGoではフィールドの公開、非公開が非常に重要な役割を持っています。Goで不変を表現したい場合、非公開なフィールドをつくり、それのゲッターを使って値を参照することが推奨されています
この記事では興味本位ですが、フィールドの公開、非公開に注目して、どういった挙動をするのかまとめました。
検証
基本形
それぞれの公開、非公開のプリミティブ型のフィールドを持っている場合は以下のようになります。
package pkg
type Item struct {
E string
p string
}
func NewItem() Item {
return Item{
E: "created",
p: "created",
}
}
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のフィールドを持っている場合はどうでしょうか?以下のように様々なパターンを試してみましょう。
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,
}
}
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
フィールドで可能となります。また、pE
、pP
フィールドはどちらも非公開のため、インスタンス化の際も参照の際もエラーとなります。
公開型の埋め込み
そしてさらにGoには埋め込みという機能があります。これを使うと、フィールドの型をそのままフィールドとして持つことができます。以下のようにしてみましょう。
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,
}
}
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を埋め込んだ場合を試してみましょう。
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,
}
}
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