いろいろ備忘録日記

主に .NET とか Go とか Flutter とか Python絡みのメモを公開しています。

Goメモ-29 (空構造体, Empty struct)

概要

ちょっと前に空インターフェースについて書いたので

devlights.hatenablog.com

ついでに、今回は空構造体についてもメモ。

空のインターフェースという型が存在するので、実は空の構造体という型もあります。

空インターフェースが 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

参考情報

dave.cheney.net


過去の記事については、以下のページからご参照下さい。

  • いろいろ備忘録日記まとめ

devlights.github.io

サンプルコードは、以下の場所で公開しています。

  • いろいろ備忘録日記サンプルソース置き場

github.com

github.com

github.com