概要
ちょっと前に空インターフェースについて書いたので
ついでに、今回は空構造体についてもメモ。
空のインターフェースという型が存在するので、実は空の構造体という型もあります。
空インターフェースが interface{}
という形なので、予想通り空構造体は struct{}
で表します。
これも、理屈は同じで何の属性も定義されていない構造体という意味です。
これ、何に使うんだ?って最初思うんですが、何気に便利です。
Goにて、空の構造体は以下の特徴を持ちます。
- 空の構造体はメモリサイズが 0
- 空の構造体は 属性 を持たない
- 空の構造体は 同じアドレス を示す
- なので、型の別名を付与してメソッドを作ることで簡易的な utilクラス や singleton みたいに出来る
- メモリサイズが 0 なので、終了通知だけを送る done チャネルなどに便利
struct{}
は、メモリサイズが0です。余計なスペースを取らない。
unsafe パッケージに Sizeof という関数があって、それにより型自身のメモリサイズを取得できるのですが、結果が0となります。
また、何回生成しても同じアドレス
を示します。実体は同じものになるということですね。
なので、singleton のような使い方もできます。
(なんでもかんでもすぐに singleton にしてしまうのは良くないパターンですが、、、使い所によりけりですね)
Goでは、型の別名を付与すると完全に別の型として扱われるので、同じアドレスを示すものでも別名付けてしまえば、それぞれに個別にメソッド定義できます。
こんな感じ。
package main import "fmt" type ( config struct{} util struct{} ) func (config) count() int { return 10 } func (u util) addr() string { return fmt.Sprintf("%p", &u) } func main() { var ( c1 = config{} u1 = util{} u2 = util{} u3 = util{} ) // c1 から addr() は呼べない. u1 から count() は呼べない. fmt.Println(c1.count()) fmt.Println(u1.addr(), u2.addr(), u3.addr()) }
以下のようになります。
10 0x596c18 0x596c18 0x596c18
また、メモリ使わないので終了管理だけするチャネル用意するときに
type nop struct{} var done = make(chan nop) go func() { defer close(done) }() <-done
とかすると分かりやすいので、個人的にはよく使います。
サンプル
package struct_ import ( "fmt" "log" "time" "unsafe" ) type ( es3 struct{} es4 struct{} ) // f1 は、 es3に紐づくメソッド // レシーバーを利用することがない場合は 以下のように (型名) or (*型名) とすることができる // struct{} なので、属性が一切ない。なのでレシーバーを使うことがない。 func (es3) f1() string { return "hello" } // f2 は、 es4に紐づくメソッド func (es4) f2() string { return "world" } // EmptyStruct は、空の構造体についサンプルです. func EmptyStruct() error { // -------------------------------------------------- // 空の構造体 (Empty struct) // - 空の構造体は struct{} で表す // - 空の構造体はメモリサイズが 0 // - 型のメモリサイズは Unsafe.sizeof() で調べられる // - 空の構造体は 属性 を持たない // - 空の構造体は 同じアドレス を示す // - なので、型の別名を付与してメソッドを作ることで簡易的な utilクラス みたいに出来る // - メモリサイズが 0 なので、終了通知だけを送る done チャネルなどに便利 // // ref: https://dave.cheney.net/2014/03/25/the-empty-struct // -------------------------------------------------- var ( emptyStruct struct{} emptyInterface interface{} ) emptyStruct = struct{}{} emptyInterface = emptyStruct // メモリサイズを見てみる // struct{} は 0 となり、 interface{} は 16 となる memsize1 := unsafe.Sizeof(emptyStruct) memsize2 := unsafe.Sizeof(emptyInterface) fmt.Printf("EmptyStruct[%d] EmptyInterface[%d]\n", memsize1, memsize2) var ( v1 = struct{}{} v2 = struct{}{} ) // 同じアドレスを示すか? fmt.Printf("v1[%p]\tv2[%p]\taddr_equal? [%v]\n", &v1, &v2, v1 == v2) type ( es1 struct{} es2 struct{} ) var ( v3 = es1{} v4 = es2{} ) // 型の別名を付与した状態でも、アドレスは同じか? // (補足) v3 == v4 は、型が異なるためコンパイルエラーとなる fmt.Printf("v3[%p(%T)]\tv4[%p(%T)]\n", &v3, v3, &v4, v4) // 型 es3 と es4 の定義は、本ソースコードの上部を参照。 var ( v5 = es3{} v6 = es4{} ) // 型の別名を付与して、さらにメソッドを付与しても、アドレスは同じか? // (補足) v5 == v6 は、型が異なるためコンパイルエラーとなる fmt.Printf("v5[%p(%T)]\tv6[%p(%T)]\n", &v5, v5, &v6, v6) // es3とes4は、元々は同じ struct{} だが、異なる型別名がついているので、Go内部では全く別の型として扱われる // なので、それぞれに定義したメソッドも、ちゃんと対象となる型の方に紐づく fmt.Println(v5.f1(), v6.f2()) // done チャネルと空構造体の組合せ // done チャネルは、処理終了を通知したいだけなので close しかしない type ( nop struct{} ) var ( done = make(chan nop) ) go func() { defer close(done) log.Println("\t==> gorouine begin") time.Sleep(2 * time.Second) log.Println("\t==> gorouine end") }() log.Println("main goroutine wait start") <-done log.Println("main goroutine wait done") return nil }
try-golang/struct_empty_struct.go at master · devlights/try-golang · GitHub
実行すると以下な感じ。
[Name] "struct_empty_struct" EmptyStruct[0] EmptyInterface[16] v1[0x68d5a0] v2[0x68d5a0] addr_equal? [true] v3[0x68d5a0(struct_.es1)] v4[0x68d5a0(struct_.es2)] v5[0x68d5a0(struct_.es3)] v6[0x68d5a0(struct_.es4)] hello world 2019/12/02 23:08:02 main goroutine wait start 2019/12/02 23:08:02 ==> gorouine begin 2019/12/02 23:08:04 ==> gorouine end 2019/12/02 23:08:04 main goroutine wait done
参考情報
過去の記事については、以下のページからご参照下さい。
- いろいろ備忘録日記まとめ
サンプルコードは、以下の場所で公開しています。
- いろいろ備忘録日記サンプルソース置き場