見出し画像

Go構造体の徹底解剖

Go 言語では、`struct` はデータを定義し、カプセル化するための集約型です。異なる型のフィールドを組み合わせることができ、他の言語のクラスに似ていますが、継承はサポートしていません。メソッドは特定の型(通常は構造体)に関連付けられた関数であり、その型のインスタンスを通じて呼び出すことができます。

struct の定義と初期化

構造体の定義

構造体は `type` キーワードと `struct` キーワードを使用して定義します。以下はシンプルな構造体定義の例です:

type User struct {
  Username    string
  Email       string
  SignInCount int
  IsActive    bool
}

構造体の初期化

構造体はさまざまな方法で初期化できます。

フィールド名を使用した初期化

user1 := User{
  Username:    "alice",
  Email:       "alice@example.com",
  SignInCount: 1,
  IsActive:    true,
}

デフォルト値での初期化

特定のフィールドを指定しない場合、それらはその型のゼロ値に初期化されます。

user2 := User{
  Username: "bob",
}

この例では、`Email` は空文字列 `""` に、`SignInCount` は `0` に、`IsActive` は `false` に初期化されます。

ポインタを使った初期化

構造体をポインタで初期化することもできます。

user3 := &User{
  Username: "charlie",
  Email:    "charlie@example.com",
}

構造体のメソッドと動作

Go 言語では、構造体はデータを格納するだけでなく、メソッドを定義してそのデータに関連する動作をカプセル化することもできます。以下に、構造体メソッドとその動作について詳しく説明します。

構造体メソッドの定義

構造体にメソッドを定義するには、レシーバ(receiver)を使用します。レシーバはメソッドの最初の引数で、そのメソッドがどの型に属しているかを指定します。レシーバには値レシーバとポインタレシーバがあります。

値レシーバ

値レシーバは、メソッド呼び出し時に構造体のコピーを作成するため、フィールドの変更は元の構造体に影響を与えません。

type User struct {
  Username string
  Email    string
}

func (u User) PrintInfo() {
  fmt.Printf("Username: %s, Email: %s\n", u.Username, u.Email)
}

ポインタレシーバ

ポインタレシーバを使用すると、元の構造体のフィールドを直接変更することができます。

func (u *User) UpdateEmail(newEmail string) {
  u.Email = newEmail
}

メソッドセット

Go では、構造体のすべてのメソッドが「メソッドセット」を構成します。値レシーバのメソッドセットは値型のすべてのメソッドを含み、ポインタレシーバのメソッドセットはポインタ型とその基になる値型のすべてのメソッドを含みます。

インターフェースと構造体メソッド

構造体メソッドは通常、インターフェースと組み合わせて使用され、多態性を実現します。インターフェースを定義する際、構造体が実装すべきメソッドを指定できます。

type UserInfo interface {
  PrintInfo()
}

// User は UserInfo インターフェースを実装
func (u User) PrintInfo() {
  fmt.Printf("Username: %s, Email: %s\n", u.Username, u.Email)
}

func ShowInfo(ui UserInfo) {
  ui.PrintInfo()
}

まとめ

構造体にメソッドを定義することで、データに関連する動作をより良く整理し、カプセル化することができます。構造体メソッドを使用することで、オブジェクト指向プログラミングの原則(カプセル化や多態性など)に従ったコードを書くことができ、コードのモジュール性と保守性が向上します。

ネスト構造体とコンポジション

Go 言語では、ネスト構造体とコンポジションは、コードの再利用と複雑なデータの整理において重要な手段です。ネスト構造体を使うことで、ある構造体を別の構造体のフィールドとして定義でき、複雑なデータモデルを構築できます。コンポジションは、他の構造体を組み込むことで新しい構造体を作成する方法です。

ネスト構造体

ネスト構造体は、1 つの構造体を別の構造体のフィールドとして使用することを可能にします。これにより、データ構造を柔軟かつ整理された形で表現できます。以下はネスト構造体の例です:

package main

import "fmt"

// Address 構造体を定義
type Address struct {
  City    string
  Country string
}

// User 構造体を定義し、Address 構造体をフィールドとして持つ
type User struct {
  Username string
  Email    string
  Address  Address // ネスト構造体
}

func main() {
  // ネスト構造体を初期化
  user := User{
    Username: "alice",
    Email:    "alice@example.com",
    Address: Address{
      City:    "New York",
      Country: "USA",
    },
  }

  // ネスト構造体のフィールドにアクセス
  fmt.Printf("User: %s, Email: %s, City: %s, Country: %s\n", user.Username, user.Email, user.Address.City, user.Address.Country)
}

構造体のコンポジション

コンポジションは、複数の構造体を組み合わせて新しい構造体を作成することで、コードの再利用を実現します。コンポジションを利用すると、複雑なモデルを構築し、同じフィールドやメソッドを共有できます。以下はコンポジションの例です:

package main

import "fmt"

// Address 構造体を定義
type Address struct {
  City    string
  Country string
}

// Profile 構造体を定義
type Profile struct {
  Age int
  Bio string
}

// User 構造体を定義し、Address と Profile を組み込む
type User struct {
  Username string
  Email    string
  Address  Address // Address 構造体を組み込む
  Profile  Profile // Profile 構造体を組み込む
}

func main() {
  // 組み込み構造体を初期化
  user := User{
    Username: "bob",
    Email:    "bob@example.com",
    Address: Address{
      City:    "New York",
      Country: "USA",
    },
    Profile: Profile{
      Age: 25,
      Bio: "A software developer.",
    },
  }

  // 組み込み構造体のフィールドにアクセス
  fmt.Printf("User: %s, Email: %s, City: %s, Age: %d, Bio: %s\n", user.Username, user.Email, user.Address.City, user.Profile.Age, user.Profile.Bio)
}

ネスト構造体とコンポジションの違い

  • ネスト構造体:構造体を他の構造体のフィールドとして組み込むことで、階層構造を持つデータモデルを表現します。主にデータの階層関係を表現する際に使用します。

  • コンポジション:1 つの構造体が複数の他の構造体のフィールドを含むことで、コードの再利用を実現します。コンポジションにより、構造体はより複雑な振る舞いや特性を持つことができます。

まとめ

ネスト構造体とコンポジションは、Go 言語の強力な機能であり、複雑なデータ構造を整理し管理するのに役立ちます。データモデルを設計する際に、これらを適切に活用することで、コードをより明確にし、保守性を向上させることができます。

空の構造体

空の構造体は、フィールドを持たない構造体を指します。

サイズとメモリアドレス

空の構造体はメモリサイズがゼロバイトであり、そのアドレスは等しい場合もあれば、異なる場合もあります。メモリ逃避(memory escape)が発生すると、それらのアドレスは等しくなり、すべて `runtime.zerobase` を指します。

// empty_struct.go
type Empty struct{}

//go:linkname zerobase runtime.zerobase
var zerobase uintptr // go:linkname ディレクティブを使用して、zerobase 変数を runtime.zerobase にリンク

func main() {
  a := Empty{}
  b := struct{}{}

  fmt.Println(unsafe.Sizeof(a) == 0) // true
  fmt.Println(unsafe.Sizeof(b) == 0) // true
  fmt.Printf("%p\n", &a)       // 0x590d00
  fmt.Printf("%p\n", &b)       // 0x590d00
  fmt.Printf("%p\n", &zerobase)    // 0x590d00

  c := new(Empty)
  d := new(Empty) // c と d を逃避させるための目的
  fmt.Sprint(c, d)
  println(c)   // 0x590d00
  println(d)   // 0x590d00
  fmt.Println(c == d) // true

  e := new(Empty)
  f := new(Empty)
  println(e)   // 0xc00008ef47
  println(f)   // 0xc00008ef47
  fmt.Println(e == f) // false
}

上記の出力からわかるように、`a`、`b`、`zerobase` のアドレスは同じであり、最終的にはグローバル変数 `runtime.zerobase`(`runtime/malloc.go`)を指しています。

次に、変数が逃避した場合を見てみましょう:

  • 変数 `c` と `d` はヒープに逃避し、それらのアドレスは `0x591d00` であり、比較結果は `true` となります。

  • 一方、変数 `e` と `f` は異なるアドレスを持ち、それぞれの比較結果は `false` となります。

これは、Go 言語の設計によるもので、空の構造体が逃避しない場合、ポインタは異なるものとして扱われ、逃避した場合は等しいと見なされます。

空の構造体を埋め込んだ場合のメモリサイズ

空の構造体自体はスペースを消費しませんが、ある構造体にフィールドとして埋め込まれる場合、その配置に応じてスペースを消費する可能性があります。計算ルールは次のとおりです:

  • 空の構造体が唯一のフィールドである場合、その構造体はスペースを消費しません。

  • 空の構造体が最初または中間のフィールドである場合もスペースを消費しません。

  • 空の構造体が最後のフィールドである場合、前のフィールドのサイズと一致するスペースを消費します。

type s1 struct {
  a struct{}
}

type s2 struct {
  _ struct{}
}

type s3 struct {
  a struct{}
  b byte
}

type s4 struct {
  a struct{}
  b int64
}

type s5 struct {
  a byte
  b struct{}
  c int64
}

type s6 struct {
  a byte
  b struct{}
}

type s7 struct {
  a int64
  b struct{}
}

type s8 struct {
  a struct{}
  b struct{}
}

func main() {
  fmt.Println(unsafe.Sizeof(s1{})) // 0
  fmt.Println(unsafe.Sizeof(s2{})) // 0
  fmt.Println(unsafe.Sizeof(s3{})) // 1
  fmt.Println(unsafe.Sizeof(s4{})) // 8
  fmt.Println(unsafe.Sizeof(s5{})) // 16
  fmt.Println(unsafe.Sizeof(s6{})) // 2
  fmt.Println(unsafe.Sizeof(s7{})) // 16
  fmt.Println(unsafe.Sizeof(s8{})) // 0
}

空の構造体が配列やスライスの要素の場合:

var a [10]int
fmt.Println(unsafe.Sizeof(a)) // 80

var b [10]struct{}
fmt.Println(unsafe.Sizeof(b)) // 0

var c = make([]struct{}, 10)
fmt.Println(unsafe.Sizeof(c)) // 24, スライスヘッダーのサイズ

用途

空の構造体はスペースを消費しないという特性を活用して、追加のメモリを必要とせずにさまざまな機能を実現できます。

unkeyed 初期化の防止

type MustKeydStruct struct {
  Name string
  Age  int
  _    struct{}
}

func main() {
  person := MustKeydStruct{Name: "hello", Age: 10}
  fmt.Println(person)

  person2 := MustKeydStruct{"hello", 10} // コンパイルエラー: MustKeydStruct{...} に値が不足している
  fmt.Println(person2)
}

集合データ構造の実装

package main

import (
  "fmt"
)

type Set struct {
  items map[interface{}]emptyItem
}

type emptyItem struct{}

var itemExists = emptyItem{}

func NewSet() *Set {
  set := &Set{items: make(map[interface{}]emptyItem)}
  return set
}

// 要素を集合に追加
func (set *Set) Add(item interface{}) {
  set.items[item] = itemExists
}

// 要素を集合から削除
func (set *Set) Remove(item interface{}) {
  delete(set.items, item)
}

// 要素が集合に存在するか判定
func (set *Set) Contains(item interface{}) bool {
  _, contains := set.items[item]
  return contains
}

// 集合のサイズを返す
func (set *Set) Size() int {
  return len(set.items)
}

func main() {
  set := NewSet()
  set.Add("hello")
  set.Add("world")
  fmt.Println(set.Contains("hello"))
  fmt.Println(set.Contains("Hello"))
  fmt.Println(set.Size())
}

チャネルでの信号伝達

チャネルを使用する際に、データの中身ではなく、データが存在するかどうかだけを確認する場合があります。たとえば、終了信号として使用する場合です。

// 空の構造体
var empty = struct{}{}

// セマフォ型
type Semaphore chan struct{}

// リソースを取得
func (s Semaphore) P(n int) {
  for i := 0; i < n; i++ {
    s <- empty
  }
}

// リソースを解放
func (s Semaphore) V(n int) {
  for i := 0; i < n; i++ {
    <-s
  }
}

// ロック
func (s Semaphore) Lock() {
  s.P(1)
}

// アンロック
func (s Semaphore) Unlock() {
  s.V(1)
}

// 新しいセマフォを作成
func NewSemaphore(N int) Semaphore {
  return make(Semaphore, N)
}

私たちはLeapcell、Goプロジェクトのクラウドデプロイの最適解です。

Leapcellは、Webホスティング、非同期タスク、Redis向けの次世代サーバーレスプラットフォームです:

複数言語サポート

  • JavaScript、Python、Go、Rustで開発できます。

無制限のプロジェクトデプロイ

  • 使用量に応じて料金を支払い、リクエストがなければ料金は発生しません。

比類のないコスト効率

  • 使用量に応じた支払い、アイドル時間は課金されません。

  • 例: $25で6.94Mリクエスト、平均応答時間60ms。

洗練された開発者体験

  • 直感的なUIで簡単に設定できます。

  • 完全自動化されたCI/CDパイプラインとGitOps統合。

  • 実行可能なインサイトのためのリアルタイムのメトリクスとログ。

簡単なスケーラビリティと高パフォーマンス

  • 高い同時実行性を容易に処理するためのオートスケーリング。

  • ゼロ運用オーバーヘッド — 構築に集中できます。

ドキュメントで詳細を確認!

Xでフォローする:@LeapcellHQ

いいなと思ったら応援しよう!