Java や C# のようなクラスベースのオブジェクト指向言語では、クラスの生成はコンストラクタに制限されており、基本的にコンストラクタを使わないとインスタンスを生成できません。
Go にはクラスという概念はなく、オブジェクト指向は構造体、インターフェース、メソッド、関数のみを使って表現されています。コンストラクターという概念も存在しないので、オブジェクトを生成する場合は以下のように、 NewThing
のような関数を個別に用意します。
package thing
type Thing struct {
Foo string
}
func NewThing(foo string) *Thing {
return &Thing{
Foo: foo,
}
}
ただし、この場合の NewThing
はあくまでただの関数なので、 Thing
構造体自体はこの関数を経由しなくても生成することができます。
func main() {
t1 := thing.NewThing("hoge")
fmt.Printf("%q\n", t1.Foo) // Output: "hoge"
// NewThing を使う必要はない
// この場合、各フィールドはゼロ値で初期化される
t2 := new(thing.Thing)
fmt.Printf("%q\n", t2.Foo) // Output: ""
}
Go においても、別パッケージからは New 関数を経由しないと生成できないオブジェクトを扱うにはどうするかを考えます。
Goにおいてオブジェクトの生成を制限するパターン
方法1. unexported な構造体を使う
Go における型の利用可否は、exported かどうかによって決まります。ただし、これは別パッケージから型名を直接参照する場合の話で、unexported な構造体を返す exported な関数、というのは不正にはなりません。
なので、以下のようにすれば privateThing
は別パッケージからは参照できないため直接生成できず、一方で NewPrivateThing
で生成されたオブジェクトは触ることができます。
package thing
type privateThing struct {
Foo string
}
func NewPrivateThing(foo string) *privateThing {
return &privateThing{
Foo: foo,
}
}
func main() {
pt1 := thing.NewPrivateThing("hoge")
fmt.Printf("%q\n", pt1.Foo) // Output: "hoge"
// NewPrivateThing を使わないと生成できない
pt2 := new(thing.privateThing) // compile error
fmt.Printf("%q\n", pt2.Foo)
}
ただし、この方法では別パッケージから privateThing
型を参照できないことによる問題が発生します。
-
privateThing
型を受け取る/返す関数を別パッケージで定義できない -
privateThing
型のフィールドを持つ別構造体を別パッケージで定義できない -
privateThing
型の array/slice/map を別パッケージで定義できない
func printPrivateThing(t *thing.privateThing) { // compile error
fmt.Printf("%q\n", t.Foo)
}
func printPrivateThings() {
pts := []*thing.privateThing{
thing.NewPrivateThing("hoge1"),
thing.NewPrivateThing("hoge2"),
} // compile error
for _, pt := range pts {
fmt.Printf("%q\n", pt.Foo)
}
}
方法2. interface を使う
Go 言語には、interface というオブジェクトの振る舞いを抽象化する型が存在します。この interface に unexported なメソッドを持たせると、パッケージ外では実装できない interface にすることができます。
package thing
type ThingInterface interface {
Foo() string
private() // unexported メソッドなので、他のパッケージから実装できない
}
type thingImpl struct {
foo string
}
func (t *thingImpl) Foo() string {
return t.foo
}
func (t *thingImpl) private() {}
func NewThingInterface(foo string) ThingInterface {
return &thingImpl{
foo: foo,
}
}
type thingImpl2 struct {
foo string
}
func (t *thingImpl2) Foo() string {
return t.foo
}
// このメソッドは thing.ThingInterface.private とは別物
func (t *thingImpl2) private() {}
func main() {
ti1 := thing.NewThingInterface("hoge")
fmt.Printf("%q\n", ti1.Foo())
// thing.ThingInterface は実装できない
var ti2 thing.ThingInterface = new(thingImpl2) // compile error
fmt.Printf("%q\n", ti2.Foo())
}
ThingInterface
は export されている interface なので、この型自体は外部のパッケージから利用できます。すなわち、関数や構造体のフィールド、array/slice/map 型での利用も通常通り行えます。
ただし、interface なので Getter/Setter をすべてメソッドで定義する必要があります。
func printThingInterface(t thing.ThingInterface) {
fmt.Printf("%q\n", t.Foo())
}
func printThingInterfaces() {
pts := []thing.ThingInterface{
thing.NewThingInterface("hoge1"),
thing.NewThingInterface("hoge2"),
}
for _, pt := range pts {
fmt.Printf("%q\n", pt.Foo())
}
// Output:
// "hoge1"
// "hoge2"
}
じゃあ interface を使えば良いのか?
上記の 2 パターンの比較では、外部パッケージでも型を扱えつつ生成のみ制限できる interface を使えばいいように思えます。ただし、Go には ”Accept interfaces, return structs” という流儀があり、生成時に interface を使うこと自体、 interface の使い方を歪めていると言えます。
”Accept interfaces, return structs” とは
”Accept interfaces, return structs” とは、直訳すると「インターフェースを受け入れ、構造体を返す」となります。この言い回しそのままは使われていいませんが、考え方については Go 公式 Wiki の CodeReviewComment で説明されています。
Go interfaces generally belong in the package that uses values of the interface type, not the package that implements those values. The implementing package should return concrete (usually pointer or struct) types: that way, new methods can be added to implementations without requiring extensive refactoring.
Do not define interfaces on the implementor side of an API "for mocking"; instead, design the API so that it can be tested using the public API of the real implementation.
Do not define interfaces before they are used: without a realistic example of usage, it is too difficult to see whether an interface is even necessary, let alone what methods it ought to contain.
つまり、interface とは利用側が振る舞いを要求するものであり、生成側が実装を隠蔽するために使うものではありません。そのため、 New
関数では interface を返すのではなく、構造体を直接返すことが推奨されています。
標準ライブラリで使われる interface として最もわかりやすいのは io.Writer
/ io.Reader
ですね。bytes.Buffer
や os.File
など、実装はあくまでそれぞれ構造体であり、 io.Copy
のような利用側の関数で振る舞いのみを要求します。Go の interface はダックタイピングなので、実装側で明示的に implements するような構文も必要ありません。
(ただし、標準ライブラリでも context.Background()
のように interface を返すメソッドは存在します。)
Go で生成を制限する必要があるのか?
そもそもの話として、Go では標準ライブラリでも不正な構造体を生成できてしまいます。
func main() {
// 不正な構造体
f := new(os.File)
fmt.Println(f.Name()) // panic
}
こうした書き方ができるのも、Go の流儀のひとつかなと思っています。では、なぜ不正な構造体を生成できることが許容されているのか?以下、自分の考えを書き連ねてみます。
- 構造体の生成を防ぐための言語仕様を取り入れることよりも、言語仕様をシンプルに保つことを優先している
- 技術的には、現状の仕様の組み合わせることで (前述の interface のパターンで) 実現はできる
- New などの生成用関数があればそれを使って生成するもの、という意識が強い
- Godoc の出力では、構造体に対して生成する関数も紐づいて表示されるため、生成用の関数も探しやすくなっている (例: os.File)
- 不正な場合 panic や error を返すなどのランタイムチェックができるようになっていれば、テストで防ぐことができる
- 仕組みで防ぐことより、ドキュメントやテストをちゃんと書く、レビューすることを重視している
- どうしてもチェックが必要であれば、言語仕様ではなく Linter などでアドオン的に防ぐ
Go らしい実装をする場合、そもそもコンストラクタという意識を捨て、構造体の生成や interface の実装を制限せず、必要なことはドキュメントをちゃんと書く、という考え方も必要ですね。
別パターン: ランタイムエラーで構造体の生成を制限する
コンパイルエラーではなくランタイムエラーでもいいなら、unexported な構造体を埋め込むことで、フィールドアクセスやメソッド呼び出しを透過的に panic にすることができます。
package thing
type thingBase struct {
Foo string
}
type EmbedThing struct {
*thingBase
}
func NewEmbedThing(foo string) *EmbedThing {
return &EmbedThing{
thingBase: &thingBase{
Foo: foo,
},
}
}
func main() {
et1 := thing.NewEmbedThing("hoge")
fmt.Printf("%q\n", et1.Foo)
// NewEmbedThing を使わないと、正しく動くものが生成できない
et2 := new(thing.EmbedThing)
fmt.Printf("%q\n", et2.Foo) // runtime error
}
この形であれば、 NewEmbedThing
関数では構造体を返すという Go の流儀に則り、かつ NewEmbedThing
関数経由で生成されていない構造体は、フィールドアクセス時にランタイムエラーとなることで検出することができます。
前述した os.File
の実装も、このように unexported な構造体を埋め込む形で実装されています。
(os.File
がこの形になっているのは、ビルドタグによってプラットフォームごとに実装を切り替えるためであり、new での生成を防ぐのは副次的な効果だと思いますが。。)
まとめ
Go には専用のコンストラクタという仕組みはありませんが、既存の仕様の組み合わせで同様の挙動を実現することができます。Go の奥深さ・表現力の高さによるものですね。
一方で、Go らしい実装とは?と考えると、必ずしも厳密にコンパイルエラーにする必要があるのかは検討の余地がありそうです。現実的には、ランタイムエラーになることで十分、あるいは、ドキュメントに記載されていれば十分ということも大いにありそうです。要件やチームの方針に合わせて柔軟に選択したいですね。