ブログに戻る

Go の struct キーワード:フィールド、タグ、埋め込み、メソッド

Go の struct キーワードの仕組みを解説します。フィールドの宣言、ゼロ値と複合リテラル、エクスポートされたフィールドと JSON、omitempty と omitzero を使った struct タグ、値レシーバーとポインタレシーバー、埋め込み、比較とコピー、空の struct、フィールドの順序を扱います。

Go の struct キーワード:フィールド、タグ、埋め込み、メソッド

struct キーワードは、名前付きのフィールドを 1 つの型にまとめます。Go で注文、ユーザー、HTTP リクエストのボディ、設定ファイルを表すときに書くのが、type Order struct { ... } という宣言です。struct にはコンストラクターも継承もありません。振る舞いはメソッドで追加し、コードの再利用は struct を別の struct に埋め込むことで行い、エンコードの方法は struct タグで制御します(Go spec)。

要約

  • type Order struct { ... } は struct 型を宣言します。各フィールドは名前と型を持ち、同じ型のフィールドは 1 行にまとめられます。
  • struct のゼロ値では、すべてのフィールドがそれぞれの型のゼロ値になります。そのため var o Order は、準備なしですぐに使えます。
  • 複合リテラルは Order{ID: 1001, Total: 4999} のようにフィールド名を付けて書きます。ほかのパッケージの型をフィールド名なしのリテラルで書くと、go vet が警告します。
  • encoding/json やほかのパッケージから見えるのは、エクスポートされた(大文字で始まる)フィールドだけです。小文字のフィールドは、エラーにならないまま読み飛ばされます。
  • json:"email,omitempty" のような struct タグでエンコードを制御します。time.Time などの struct には omitzero(Go 1.24)を使います。omitempty では struct が省略されることはないからです。
  • ポインタレシーバーのメソッドは struct を変更できます。値レシーバーのメソッドはコピーに対して動作します。
  • 型を埋め込むと、そのフィールドとメソッドが外側の struct に昇格されます。Go は継承を使わず、この仕組みでコードを再利用します。
  • struct を == で比較できるのは、すべてのフィールドが比較可能な場合だけです。struct を代入するとコピーされますが、中にある slice や map は共有されたままです。

Go で struct を宣言するには?

type、名前、struct の順に書き、波括弧の中にフィールドを並べます。各フィールドは名前の後ろに型を書きます。

example.gogo
package main

import (
	"fmt"
	"time"
)

type Order struct {
	ID         int64
	CustomerID int64
	Status     string
	Total      int64 // cents
	CreatedAt  time.Time
}

type Address struct {
	Street, City, Country string
}

func main() {
	var o Order
	fmt.Printf("%+v\n", o)
	fmt.Println(o.Status == "", o.CreatedAt.IsZero())
}
example.texttext
{ID:0 CustomerID:0 Status: Total:0 CreatedAt:0001-01-01 00:00:00 +0000 UTC}
true true

Address は短い書き方の例です。同じ型のフィールドは、カンマで区切って 1 行にまとめられます。それでも多くのコードベースでは 1 行に 1 フィールドずつ書きます。差分が小さく保たれ、タグやコメントを付ける余地も残るからです。

var o Order にコンストラクターは必要ありません。すべてのフィールドはゼロ値から始まります。数値なら 0、文字列なら ""、ポインタ、slice、map なら nil、CreatedAt ならゼロの time.Time です。ゼロ値については Go の var キーワードで詳しく解説しています。

ほかの型と同じく、struct も、その関数でしか使わないなら関数の中で宣言できます。Go の type キーワード:定義型、型エイリアス、メソッドでは、JSON のボディをデコードするためにローカルな loginRequest struct を使う例を紹介しています。

struct の値を作るには?

たいていは、var によるゼロ値、フィールド名を付けた複合リテラル、& を付けたリテラルへのポインタのどれかを使います。

example.gogo
keyed := Order{ID: 1001, CustomerID: 42, Status: "paid", Total: 4999}
partial := Order{ID: 1002, Status: "pending"}
positional := Order{1003, 42, "paid", 1250}
p := &Order{ID: 1004}
p.Status = "shipped"
example.texttext
{ID:1001 CustomerID:42 Status:paid Total:4999}
{ID:1002 CustomerID:0 Status:pending Total:0}
{ID:1003 CustomerID:42 Status:paid Total:1250}
{ID:1004 CustomerID:0 Status:shipped Total:0}

フィールド名付きのリテラルでは、フィールドを好きな順番で、好きな数だけ書けます。書かなかったフィールドはゼロ値になります。位置指定のリテラルでは、すべてのフィールドを宣言順に書く必要があるので、壊れやすくなります。Order に Currency フィールドを追加すると、位置指定のリテラルはすべてコンパイルできなくなりますが、フィールド名付きのリテラルはそのまま動きます。

ほかのパッケージの struct 型については、go vet が composites アナライザーで位置指定のリテラルを報告します。

example.gogo
addr := net.TCPAddr{net.ParseIP("10.0.0.5"), 5432, ""}
example.texttext
main.go:9:10: net.TCPAddr struct literal uses unkeyed fields

p.Status は、(*p).Status と書かなくてもポインタに対して使えます。struct へのポインタからフィールドにアクセスすると、Go が自動的にデリファレンスするからです。

省略可能なポインタフィールドを 1 行で設定するには?

Go 1.26 からは、new に値を渡せます。PATCH リクエストのボディで「送られていない」と「ゼロとして送られた」を区別するには、ポインタフィールドを使うのが一般的です。Go 1.26 より前は、フィールドごとに一時変数かヘルパー関数が必要でした。

example.gogo
type UpdateUser struct {
	Name  *string `json:"name,omitempty"`
	Email *string `json:"email,omitempty"`
	Age   *int    `json:"age,omitempty"`
}

func main() {
	patch := UpdateUser{
		Email: new("[email protected]"),
		Age:   new(31),
	}
	out, _ := json.Marshal(patch)
	fmt.Println(string(out))
}
example.texttext
{"email":"[email protected]","age":31}

new(31) は int を割り当てて 31 を格納し、そのポインタを返します(Go 1.26 リリースノート)。go.mod に go 1.25 と書かれていると、コンパイラはこのコードを拒否します。

example.texttext
./main.go:16:10: new("[email protected]") requires go1.26 or later (-lang was set to go1.25; check go.mod)

小文字の struct フィールドが JSON に出てこないのはなぜか?

エクスポートされていないフィールドは、ほかのパッケージから見えないからです。encoding/json もほかのパッケージなので、エラーを出さずにそれらを読み飛ばします。

example.gogo
type User struct {
	ID       int64
	Email    string
	password string
	role     string
}

func main() {
	u := User{ID: 7, Email: "[email protected]", password: "hunter2", role: "admin"}
	out, err := json.Marshal(u)
	fmt.Println(string(out), err)

	var in User
	err = json.Unmarshal([]byte(`{"ID":8,"Email":"[email protected]","role":"admin"}`), &in)
	fmt.Printf("%+v %v\n", in, err)
}
example.texttext
{"ID":7,"Email":"[email protected]"} <nil>
{ID:8 Email:[email protected] password: role:} <nil>

どちらの呼び出しもエラーは nil で、入力の role は何の知らせもなく捨てられます。Go の API が {} を返したり、デコード後にフィールドが空のままだったりしたら、まずここを確認してください。直し方は、フィールド名を大文字で始め、タグで JSON 上の小文字の名前を保つことです。

同じルールは、リフレクションでフィールドを読むすべてのパッケージに当てはまります。encoding/xml、database/sql のスキャナー、GORM、YAML ライブラリ、テンプレートの実行などです。この性質は意図して使うこともできます。データベースのハンドルやキャッシュのように、決してエンコードしてはいけないデータは、エクスポートされないフィールドに置けば安全です。

Go の struct タグとは?

struct タグは、フィールドの型の後ろに書く文字列リテラルです。Go 自体はタグを無視し、各パッケージがリフレクションでタグを読んで、そのフィールドの扱い方を決めます。いちばん目にするのは JSON のタグでしょう。

example.gogo
type User struct {
	ID           int64     `json:"id"`
	Email        string    `json:"email"`
	Nickname     string    `json:"nickname,omitempty"`
	PasswordHash string    `json:"-"`
	DeletedAt    time.Time `json:"deleted_at,omitempty"`
	LastLoginAt  time.Time `json:"last_login_at,omitzero"`
}

func main() {
	u := User{ID: 7, Email: "[email protected]", PasswordHash: "$2a$10$..."}
	enc := json.NewEncoder(os.Stdout)
	enc.SetIndent("", "  ")
	enc.Encode(u)
}
example.texttext
{
  "id": 7,
  "email": "[email protected]",
  "deleted_at": "0001-01-01T00:00:00Z"
}

出力をフィールドごとに見ていきます。

  • json:"id" は、出力でのフィールド名を変えます。
  • omitempty は、値が false、0、""、nil、空の slice や map のときにフィールドを省きます。Nickname は空なので出力されません。
  • json:"-" は、そのフィールドを決してエンコードしません。PasswordHash はどのレスポンスにも含まれません。
  • omitempty は struct には効きません。DeletedAt はゼロの time.Time ですが、0001-01-01T00:00:00Z として出力されてしまいます。クライアントはこれを実在する日付として解釈しがちです。
  • Go 1.24 で追加された omitzero は、フィールドがその型のゼロ値のときに省きます。time.Time のように IsZero() bool メソッドを持つ型では、そのメソッドを呼び出します。LastLoginAt は出力されません(Go 1.24 リリースノート)。

新しいコードでは、struct のフィールドと time.Time には omitzero を使います。文字列、数値、slice、map には、今でも omitempty で問題ありません。

1 つのフィールドに、複数のパッケージ向けのタグをスペース区切りで付けられます。`json:"email" db:"email" validate:"required,email"` のような形です。各パッケージは reflect.StructTag.Get で自分のキーだけを読みます。

example.gogo
f, _ := reflect.TypeFor[User]().FieldByName("Nickname")
fmt.Println(f.Tag.Get("json"))
example.texttext
nickname,omitempty

struct タグが無視されるのはなぜか?

たいていは、スペースが入っているか、引用符が抜けているかです。書式は厳密で、key:"value" のようにコロンの後ろにスペースを入れてはいけません。json: "email" は人間には問題なく見えますが、Tag.Get("json") は空文字列を返すので、フィールドは Email としてエンコードされます。コンパイラはどんな文字列もタグとして受け付けるので、何も指摘しません。指摘してくれるのは go vet の structtag アナライザーで、同じ JSON 名を持つ 2 つのフィールドも検出します。

example.gogo
type User struct {
	Email string `json: "email"`
	Name  string `json:"name" db:"name"`
	ID    int64  `json:"id"`
	Ref   int64  `json:"id"`
}
example.texttext
main.go:4:2: struct field tag `json: "email"` not compatible with reflect.StructTag.Get: bad syntax for struct tag value
main.go:7:2: struct field Ref repeats json tag "id" also at main.go:6

gopls を使うエディターでは、入力中に同じ警告が表示されます。

struct のメソッドはどう動くのか?

メソッドは、名前の前にレシーバーを書いた関数です。レシーバーは値(c Cart)かポインタ(c *Cart)のどちらかで、どちらを選ぶかによって、メソッドが struct を変更できるかどうかが決まります。

example.gogo
type Cart struct {
	Items []string
	Total int64
}

func (c Cart) AddValue(item string, price int64) {
	c.Items = append(c.Items, item)
	c.Total += price
}

func (c *Cart) Add(item string, price int64) {
	c.Items = append(c.Items, item)
	c.Total += price
}

func main() {
	var c Cart
	c.AddValue("keyboard", 4999)
	fmt.Println(len(c.Items), c.Total)

	c.Add("keyboard", 4999)
	fmt.Println(len(c.Items), c.Total)
}
example.texttext
0 0
1 4999

AddValue はコンパイルでき、実行もできますが、カートは空のままです。値レシーバーは struct のコピーなので、メソッドはコピーを更新し、そのまま捨ててしまいます。エラーも警告も出ないため、Go を書き始めたばかりのコードではこのバグがよく起こります。Add はポインタを受け取るので、呼び出し側のカートを変更します。

(&c).Add(...) と書く必要はありません。c がアドレス可能であれば、Go が代わりにアドレスを取ります。逆も同じで、ポインタから値レシーバーのメソッドを呼び出せます。

メソッドが struct を変更する場合、struct が大きい場合、sync.Mutex のようなコピーしてはいけないフィールドを含む場合は、ポインタレシーバーを使います。Money や Point のような小さくて不変な値には、値レシーバーを使います。ある型のメソッドの 1 つでもポインタレシーバーが必要なら、すべてのメソッドをポインタレシーバーにします。2 種類を混ぜると、メソッドセットが把握しにくくなります。すべてのメソッドを持つのは *Cart だけになり、それらすべてを必要とする interface を満たせるのも *Cart だけになるからです(Go spec)。

Go の struct の埋め込みはどう動くのか?

名前を書かずに型だけを書いたフィールドは、埋め込みフィールドです。そのフィールドとメソッドは昇格され、外側の struct に宣言されているかのように使えます。

example.gogo
type Timestamps struct {
	CreatedAt time.Time
	UpdatedAt time.Time
}

func (t *Timestamps) Touch(now time.Time) {
	if t.CreatedAt.IsZero() {
		t.CreatedAt = now
	}
	t.UpdatedAt = now
}

type Order struct {
	ID int64
	Timestamps
}

type User struct {
	ID    int64
	Email string
	Timestamps
}

func main() {
	now := time.Date(2026, 9, 26, 9, 0, 0, 0, time.UTC)
	var o Order
	o.Touch(now)
	fmt.Println(o.CreatedAt.Format(time.DateOnly), o.Timestamps.UpdatedAt.Format(time.Kitchen))

	u := User{ID: 7, Email: "[email protected]", Timestamps: Timestamps{CreatedAt: now}}
	fmt.Println(u.CreatedAt.Year())
}
example.texttext
2026-09-26 9:00AM
2026

o.Touch(now) は o.Timestamps.Touch(now) の省略形です。埋め込みフィールドは型名のフィールドとしてそのまま存在し、リテラルで値を設定するときも Timestamps: Timestamps{...} のように型名を使います。encoding/json は埋め込まれた struct も平坦化するので、上の Order は {"ID":0,"CreatedAt":"2026-09-26T09:00:00Z","UpdatedAt":"2026-09-26T09:00:00Z"} とエンコードされます。

埋め込みは継承ではありません。Order は Timestamps ではないので、Timestamps を受け取る関数に Order は渡せません。Touch が実行されるとき、そのレシーバーは内側の Timestamps です。外側の Order にアクセスする手段はなく、Order が定義するメソッドを呼ぶこともできません。Java や Python のサブクラスならそれができ、「どのオーバーライドが実行されるのか」という疑問はそこから生まれます。Go ではこの疑問は出てきません。

外側の struct が同じ名前のフィールドやメソッドを宣言している場合は、外側が優先されます。埋め込まれた側には、型名を通じて引き続きアクセスできます。

example.gogo
type Base struct{ ID int64 }

func (Base) Describe() string { return "base" }

type Order struct {
	Base
	ID string
}

func (Order) Describe() string { return "order" }

func main() {
	o := Order{Base: Base{ID: 42}, ID: "ord_42"}
	fmt.Println(o.ID, o.Base.ID)
	fmt.Println(o.Describe(), o.Base.Describe())
}
example.texttext
ord_42 42
order base

struct に interface を埋め込めるか?

できます。1 つのメソッドだけを差し替えて型をラップするときによく使う方法です。ステータスコードを記録する HTTP ミドルウェアは http.ResponseWriter を埋め込み、Header と Write をそのまま使いつつ、WriteHeader だけを置き換えます。

example.gogo
type statusRecorder struct {
	http.ResponseWriter
	status int
}

func (r *statusRecorder) WriteHeader(code int) {
	r.status = code
	r.ResponseWriter.WriteHeader(code)
}

func logStatus(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
		rec := &statusRecorder{ResponseWriter: w, status: http.StatusOK}
		next.ServeHTTP(rec, req)
		slog.Info("request", "path", req.URL.Path, "status", rec.status)
	})
}
example.texttext
2026/09/26 09:04:44 INFO request path=/orders/99 status=404

*statusRecorder は 3 つのメソッドをすべて持つので、http.ResponseWriter を満たし、次のハンドラーに渡せます。埋め込んだ interface が nil のままだと、そのメソッドを呼んだ時点で panic になります。リテラルで必ず設定してください。

struct に sync.Mutex を埋め込むべきか?

エクスポートされる型では避けてください。埋め込むと Lock と Unlock が型の公開 API に昇格されるので、呼び出し側の誰もが外からキャッシュをロックできてしまいます。

example.gogo
type Cache struct {
	sync.Mutex
	items map[string]string
}

名前を付けたエクスポートされないフィールドにすれば、ロックは実装の詳細にとどまります。

example.gogo
type SafeCache struct {
	mu    sync.Mutex
	items map[string]string
}

func (c *SafeCache) Get(key string) (string, bool) {
	c.mu.Lock()
	defer c.mu.Unlock()
	v, ok := c.items[key]
	return v, ok
}

sync.Mutex のゼロ値はロックされていない mutex なので、SafeCache のロックには準備が要りません。ただし、map は最初の書き込みの前に作成しておく必要があります。

Go で struct を比較、コピーできるか?

すべてのフィールドが比較可能であれば、2 つの struct を == で比較できます。比較はフィールドごとに行われます。そのため小さな struct は、値型としても map のキーとしても使いやすくなります。

example.gogo
type Money struct {
	Amount   int64
	Currency string
}

type RouteKey struct {
	Method string
	Path   string
}

func main() {
	fmt.Println(Money{4999, "EUR"} == Money{4999, "EUR"})

	hits := map[RouteKey]int{}
	hits[RouteKey{"GET", "/orders"}]++
	hits[RouteKey{"GET", "/orders"}]++
	hits[RouteKey{"POST", "/orders"}]++
	fmt.Println(hits[RouteKey{"GET", "/orders"}], len(hits))
}
example.texttext
true
2 2

RouteKey のような複合キーは、"GET /orders" のように文字列をつなげるよりもすっきりしていて、パスにスペースが含まれていても衝突しません。ここで位置指定のリテラルを使っても問題ないのは、RouteKey が同じパッケージで宣言されていて、フィールドが 2 つだけの見てわかる構造だからです。

slice、map、関数は比較できないので、それらを含む struct も比較できません。コンパイラがそう教えてくれます。

example.gogo
type Order struct {
	ID    int64
	Items []string
}

func main() {
	a := Order{ID: 1}
	b := Order{ID: 1}
	fmt.Println(a == b)
}
example.texttext
./main.go:13:14: invalid operation: a == b (struct containing []string cannot be compared)

このような struct は、フィールドごとに比較します。slice には slices.Equal を使い、テストでは reflect.DeepEqual も使えます。

struct を代入するとコピーされるのか?

されます。ただし、浅いコピーです。すべてのフィールドがコピーされますが、slice や map のフィールドは、同じデータを指したままのヘッダーとしてコピーされます。

example.gogo
type Order struct {
	ID    int64
	Items []string
	Meta  map[string]string
}

func main() {
	original := Order{ID: 1, Items: []string{"keyboard"}, Meta: map[string]string{"source": "web"}}
	copied := original
	copied.ID = 2
	copied.Items[0] = "mouse"
	copied.Meta["source"] = "api"
	fmt.Println(original.ID, original.Items, original.Meta)
}
example.texttext
1 [mouse] map[source:api]

copied.ID を変更しても、もとの値はそのままです。一方、copied.Items の要素や copied.Meta のキーを変更すると、もとの値も変わります。独立したコピーが必要なら、slices.Clone と maps.Clone でそれらのフィールドを自分で複製します。

struct を値として関数に渡すときも、同じコピーが起こります。sync.Mutex を含む struct では、このコピーはバグになります。コピーが自分専用のロックを持ってしまうからです。go vet は copylocks アナライザーでこれを報告します。

example.gogo
type Counter struct {
	mu   sync.Mutex
	hits int
}

func report(c Counter) {
	fmt.Println(c.hits)
}
example.texttext
main.go:13:15: report passes lock by value: shop.Counter contains sync.Mutex
main.go:19:9: call of report copies lock value: shop.Counter contains sync.Mutex

直し方は func report(c *Counter) とすることです。

Go の無名 struct とは?

名前を付けずにその場で書く struct 型のことです。一度しか必要のない形に使います。よく見かけるのは、テーブル駆動テストと、JSON レスポンスの一部だけをデコードする場面です。

example.gogo
var resp struct {
	Data struct {
		Status string `json:"status"`
	} `json:"data"`
}
body := `{"data":{"status":"shipped","carrier":"DHL"},"meta":{"request_id":"abc"}}`
if err := json.Unmarshal([]byte(body), &resp); err != nil {
	panic(err)
}
fmt.Println(resp.Data.Status)

tests := []struct {
	name  string
	input string
	want  string
}{
	{"trims spaces", "  [email protected] ", "[email protected]"},
	{"already clean", "[email protected]", "[email protected]"},
}
for _, tt := range tests {
	got := strings.ToLower(strings.TrimSpace(tt.input))
	fmt.Println(tt.name, got == tt.want)
}
example.texttext
shipped
trims spaces true
already clean true

デコーダーは struct に宣言されたフィールドだけを埋めるので、carrier と meta は無視されます。フィールドを 1 つ読むだけのレスポンスに、名前付きの型は必要ありません。同じ形が 2 か所に登場したら、名前を付けます。

struct{} は何に使うのか?

struct{} はフィールドを 1 つも持たない struct です。サイズが 0 バイトなので、何かが存在するかどうかだけが重要な場面で、値の型として選ばれます。よくあるのは、map で作る集合と、合図を送るためだけの channel の 2 つです。

example.gogo
seen := map[string]struct{}{}
for _, id := range []string{"evt_1", "evt_2", "evt_1"} {
	if _, dup := seen[id]; dup {
		fmt.Println("skipping duplicate", id)
		continue
	}
	seen[id] = struct{}{}
}
fmt.Println(len(seen), unsafe.Sizeof(struct{}{}))

done := make(chan struct{})
go func() {
	defer close(done)
}()
<-done
fmt.Println("worker finished")
example.texttext
skipping duplicate evt_1
2 0
worker finished

map[string]struct{} は、map[string]bool よりも「文字列の集合」であることがはっきり伝わります。map[string]bool では、false が何を意味するのかを読み手が考えなければなりません。chan struct{} は、channel でデータが送られることはなく、伝わるのは close されたという事実だけであることを示します。ctx.Done() が <-chan struct{} を返すのも同じ理由です。

フィールドの順序で struct のサイズは変わるのか?

変わります。各フィールドはそのサイズに合わせてアラインされるので、小さなフィールドの後ろに大きなフィールドが続くと、コンパイラはその間にパディングを挿入します。64 ビットのマシンでは、同じ 5 つのフィールドでも、順序が悪いと 2 倍のメモリを使います。

example.gogo
type EventLoose struct {
	Active   bool
	ID       int64
	Retried  bool
	Attempts int32
	Urgent   bool
}

type EventPacked struct {
	ID       int64
	Attempts int32
	Active   bool
	Retried  bool
	Urgent   bool
}

func main() {
	fmt.Println(unsafe.Sizeof(EventLoose{}), unsafe.Sizeof(EventPacked{}))
}
example.texttext
32 16

EventLoose では、ID が 8 バイト境界から始まるように Active の後ろに 7 バイトのパディングが入り、Retried と Urgent の後ろにもさらにパディングが入ります。EventPacked はフィールドを大きい順に並べているので、無駄になるのは末尾の 1 バイトだけです。

ほとんどの struct ではこの違いは問題にならず、意味ごとにフィールドをまとめたほうが読みやすくなります。問題になり始めるのは、何百万もの値を slice やキャッシュに保持するときです。golang.org/x/tools の fieldalignment アナライザーは、もっと小さくできる struct を報告してくれます。

Go の struct とクラスの違いは?

Go の struct はデータを持ち、メソッドも持てるので、Java、C#、Python のクラスが担う役割のほとんどをカバーします。それ以外の点は、意図的に違う設計になっています。

Go の structクラス(Java、C#、Python)
コンストラクターなし。ゼロ値か、慣例として NewX 関数を使う専用のコンストラクターメソッド
再利用埋め込み(コンポジション)継承とコンポジション
ポリモーフィズムinterface(暗黙的に満たす)基底クラスと宣言された interface
可視性パッケージ単位、大文字かどうかで決まるメンバーごとの public、private、protected
メソッド型の外側、パッケージ内のどこにでも宣言するクラス本体の中で宣言する
コピーの意味論デフォルトは値、ポインタには & を使うデフォルトは参照

struct に検証やデフォルト値が必要なときは、それを返す普通の関数を書きます。慣例として名前は NewX にし、あわせてエラーを返すのも一般的です。

example.gogo
type Client struct {
	baseURL string
	http    *http.Client
	retries int
}

func NewClient(baseURL string) (*Client, error) {
	if baseURL == "" {
		return nil, errors.New("baseURL is required")
	}
	return &Client{
		baseURL: baseURL,
		http:    &http.Client{Timeout: 10 * time.Second},
		retries: 3,
	}, nil
}

フィールドがエクスポートされていないので、パッケージの外の呼び出し側は、デフォルト値が適用された Client を NewClient 経由でしか取得できません。Go には super もオーバーライドもないので、メソッドの動きを知るためにクラス階層をたどる必要はありません。struct の振る舞いは、自身のメソッドと埋め込んだ型のメソッドだけで、すべて 1 つのパッケージの中で読めます。Go のコードベースが大きくなったとき、これが大いに役立ちます。

LevelUpGo で学ぶ

LevelUpGo では、ブラウザ上で本物の Go コードを実行する演習を通じて Go を学べます。Composite Types では、最初の struct、コンストラクター関数、メソッド、埋め込みを段階的に作り、それらを小さなプロジェクトで使います。Pointers & Memory では、struct へのポインタ、new、値レシーバーとポインタレシーバーの違いを扱い、最後に struct をその場で更新するフィーチャーフラグのストアを作ります。Interfaces & Polymorphism では、struct が宣言なしで interface を満たす仕組みを学びます。Training Ground には、コースの外で練習できる短い単独の演習があります。残り 24 個の予約語については、Go のキーワード一覧:全 25 個の予約語を解説をご覧ください。

よくある質問

Go で struct はキーワードですか?

はい。struct は Go の 25 個の予約語の 1 つなので、変数名や関数名には使えません。struct 型を始めるキーワードで、type Order struct { ... } のように、ほとんどの場合 type で名前を付けて使います。無名 struct や空の struct struct{} にも登場します。

Go にクラスはありますか?

ありません。代わりに、メソッドを持つ struct、interface、埋め込みがあります。struct がデータを持ち、メソッドが振る舞いを加え、interface がポリモーフィズムを提供し、埋め込みがコンポジションによってコードを再利用します。継承もコンストラクター用のキーワードもありません。その役割は、慣例として NewX 関数が担います。

値レシーバーとポインタレシーバーのどちらを使うべきですか?

メソッドが struct を変更する場合、struct が大きい場合、sync.Mutex を含む場合は、ポインタレシーバーを使います。Money や Point のように、変更されることのない小さな値には値レシーバーを使います。ある型のメソッドの 1 つでもポインタレシーバーが必要なら、型の振る舞いをそろえるために、すべてをポインタレシーバーにします。

json.Marshal が struct に対して空のオブジェクトを返すのはなぜですか?

struct のフィールドがエクスポートされていないからです。encoding/json から見えるのは大文字で始まるフィールドだけで、それ以外はエラーを出さずに読み飛ばされます。email を Email に変え、`json:"email"` を付ければ、JSON 上では小文字の名前のままにできます。

omitempty と omitzero の違いは何ですか?

omitempty は false、0、""、nil、空の slice や map を省きますが、struct は決して省かないので、ゼロの time.Time は 0001-01-01T00:00:00Z として出力されます。Go 1.24 で追加された omitzero は、その型のゼロ値を持つフィールドをすべて省き、型に IsZero メソッドがあればそれを使って判定します。

出典

シニアエンジニアのように Go を書く

ブラウザで学べるインタラクティブなレッスン。最初のレッスンは無料です。

無料レッスンを試すまたは無料アカウントを作成