ブログに戻る

Go の type キーワード:定義型、型エイリアス、メソッド

Go の type キーワードの仕組みを解説します。定義型と型エイリアスの違い、基底型と型変換、メソッドが引き継がれない理由、http.HandlerFunc のような関数型、ジェネリック型、型 switch を扱います。

Go の type キーワード:定義型、型エイリアス、メソッド

type キーワードは型に名前を付けます。書き方は 2 種類あります。type UserID int64 は型定義で、独自のアイデンティティと独自のメソッドを持つまったく新しい型を作ります。type Handler = http.Handler はエイリアス宣言で、すでにある型に 2 つ目の名前を付けます。どちらを選ぶかによって、2 つの名前を混ぜて使えるかどうか、メソッドを追加できるかどうかが決まります(Go spec)。

要約

  • type Name Underlying は新しい型を定義します。type Name = Other はエイリアスを宣言します。type ( ... ) ブロックは複数の宣言をまとめます。
  • 定義型は、基底型が同じ型も含め、ほかのどの型とも異なります。UserID は、明示的に変換しない限り、OrderID や int64 が必要な場所には渡せません。
  • 型なし定数や、map リテラルのような名前のない型の値は、変換なしで定義型に代入できます。そのため cancelOrder(42) はコンパイルできます。
  • 定義型は最初はメソッドを 1 つも持ちません。type Timestamp time.Time とすると Format がなくなります。メソッドを残したい場合は、struct に time.Time を埋め込みます。
  • メソッドを宣言できるのは、自分のパッケージの型だけです。func (d time.Duration) ... は cannot define new methods on non-local type で失敗します。
  • エイリアスは、同じ型を別の名前で呼ぶだけです。byte、rune、any はエイリアスです。Go 1.24 からは、エイリアスも型パラメータを取れます。
  • http.HandlerFunc のような関数型はメソッドを持てます。Go はこの仕組みで、ただの関数を interface の値に変えています。
  • ジェネリック型は Page[T any] のように型パラメータを取ります。Go 1.27 からは、メソッドも独自の型パラメータを宣言できます。

Go で型を宣言するには?

type、名前、もとになる型の順に書きます。const ブロックや var ブロックと同じように、複数の宣言を 1 つの type ( ... ) ブロックにまとめられます。

example.gogo
package main

import (
	"fmt"
	"net/http"
	"time"
)

type (
	UserID  int64
	OrderID int64

	Order struct {
		ID        OrderID
		UserID    UserID
		CreatedAt time.Time
	}

	OrderStore interface {
		Get(id OrderID) (Order, error)
	}

	Handler = http.Handler
)

func main() {
	o := Order{ID: 1001, UserID: 42}
	fmt.Println(o.ID)
}

このプログラムは 1001 と出力します。このブロックには、型定義が 4 つとエイリアスが 1 つ含まれています。右辺として最もよく使われるのは struct と interface で、それぞれに専用のキーワードがあります。これらは Go のキーワード一覧:全 25 個の予約語を解説で扱っています。Order のような struct については Go の struct キーワードで詳しく解説しています。OrderStore のような interface については Go の interface キーワードで詳しく解説しています。

型の名前は、通常の可視性のルールに従います。Order は大文字で始まるのでエクスポートされ、orderRow ならパッケージの中からしか見えません。エクスポートされた名前について詳しくは、Go の package キーワードで解説しています。

型は関数の中でも宣言できます。その型は関数の中からしか見えないので、リクエストボディのような一度しか使わない形に向いています。

example.gogo
func main() {
	type loginRequest struct {
		Email    string `json:"email"`
		Password string `json:"password"`
	}
	var req loginRequest
	err := json.Unmarshal([]byte(`{"email":"[email protected]","password":"hunter2"}`), &req)
	fmt.Println(req.Email, err)
}

このプログラムは [email protected] <nil> と出力します。メソッドの宣言はパッケージレベルにしか書けないので、ローカルな型はメソッドを持てません。

Go の定義型とは?

= を使わずに type Name Underlying と宣言した型のことです。言語仕様ではこれを型定義と呼びます。定義型にはすべて基底型(underlying type)があり、値の格納方法や使える演算子は基底型で決まります。上の UserID と OrderID はどちらも基底型が int64 なので、ほかの int64 と同じように足し算、比較、出力ができます。

共有していないのはアイデンティティです。定義型はほかのどの型とも常に異なるので、コンパイラは混ぜて使うことを許しません。

example.gogo
package main

import "fmt"

type UserID int64
type OrderID int64

func cancelOrder(id OrderID) {
	fmt.Println("cancelling order", id)
}

func main() {
	var user UserID = 42
	cancelOrder(user)
}
example.texttext
./main.go:14:14: cannot use user (variable of int64 type UserID) as OrderID value in argument to cancelOrder

このエラーこそが、ID の型を定義する価値です。refund(userID, orderID int64) というシグネチャの関数は、引数をどちらの順番で渡しても受け付けてしまいます。UserID と OrderID を使えば、2 つを取り違えたときに、別の顧客に返金されるのではなくコンパイルエラーになります。

同じルールで、定義型とその基底型も区別されます。int64 を受け取る関数に UserID を渡しても失敗します。

example.texttext
./main.go:13:11: cannot use id (variable of int64 type UserID) as int64 value in argument to loadUser

定義型どうしを変換するには?

変換先の型を関数呼び出しのように書きます。2 つの型の基底型が同じであれば変換が許されるので、OrderID(user) も int64(user) もコンパイルできます。この変換に実行時のコストはかかりません。値はそのままで、コンパイラから見た型だけが変わります。

変換を明示的に書かせるのは意図的な設計です。コードレビューで OrderID(user) を見かければ、誰かがユーザー ID を注文 ID として扱うと判断したことがわかり、その理由を尋ねられます。

42 や map リテラルを変換なしで渡せるのはなぜか?

どちらもまだ定義型を持っていないからです。代入可能性(assignability)には「型が同一である」以外にもいくつかルールがあり、そのうち 2 つは頻繁に登場します(Go spec)。

1 つ目は型なし定数に関するルールです。42 は型なし定数で、その値を表現できる基底型を持つ型であれば、どの型にもなれます。そのため cancelOrder(42) はコンパイルでき、cancelling order 42 と出力します。5 * time.Second と書けるのも同じルールによるものです。time.Duration 自体が、基底型を int64 とする定義型だからです。型なし定数については Go の const キーワードで詳しく解説しています。

2 つ目は名前のない型に関するルールです。基底型が同一で、少なくとも一方が名前付きの型でなければ、その値を定義型に代入できます。map[string]string は名前のない型リテラルなので、Headers を受け取る関数にそのまま渡せます。

example.gogo
type Headers map[string]string

func send(h Headers) {
	fmt.Println(len(h), h["Content-Type"])
}

func main() {
	send(map[string]string{"Content-Type": "application/json"})

	raw := map[string]string{"Accept": "text/plain"}
	send(raw)
}

どちらの呼び出しもコンパイルできます。このルールは []byte、func(...)、struct リテラルでも同じように働きます。ただし、UserID から OrderID へ、あるいは UserID から int64 へのように、名前付きの型から別の名前付きの型へ直接渡すことは決して許しません。

定義型がメソッドを引き継がないのはなぜか?

定義型は基底型の構造を受け継ぎますが、メソッドは 1 つも受け継ぎません。初めて time.Time をラップしたときに、多くの人がこれに驚きます。

example.gogo
package main

import (
	"fmt"
	"time"
)

type Timestamp time.Time

func main() {
	created := Timestamp(time.Now())
	fmt.Println(created.Format(time.RFC3339))
}
example.texttext
./main.go:12:22: created.Format undefined (type Timestamp has no field or method Format)

メソッドセットが空の状態から始まるのは、新しい型がもとの型とは異なる振る舞いをできるようにするためです。http.Header は map[string][]string に、ヘッダー名を正規化する Get や Set などのメソッドを付けた型です。sort.StringSlice は []string に、sort.Interface が必要とするメソッドを付けた型です。もし定義型が基底型のメソッドをすべてコピーしていたら、Timestamp には、与えるつもりのなかったメソッドまで付いてしまいます。

もとのメソッドを使いたいときの方法は 2 つあります。1 つ目は、必要な場所でもとの型に変換することです。この方法で String メソッドを書けば、値の出力形式を制御できます。

example.gogo
type Timestamp time.Time

func (t Timestamp) String() string {
	return time.Time(t).Format(time.RFC3339)
}

2 つ目は埋め込みです。time.Time を埋め込んだ struct では、そのメソッドがすべて昇格され、その隣に自分のメソッドを追加できます。

example.gogo
package main

import (
	"fmt"
	"time"
)

type Timestamp struct {
	time.Time
}

func (t Timestamp) ISO() string {
	return t.UTC().Format(time.RFC3339)
}

func main() {
	created := Timestamp{time.Date(2026, 9, 25, 14, 30, 0, 0, time.UTC)}
	fmt.Println(created.Format("2006-01-02"))
	fmt.Println(created.ISO())
}

このプログラムは 2026-09-25 と出力し、続けて 2026-09-25T14:30:00Z と出力します。埋め込みを使うと、Format、Before、Unix をはじめ、time.Time のメソッドがすべて残ります。また、Timestamp は json.Marshaler を含め、time.Time が満たす interface をすべて満たすことになります。そのため JSON にエンコードすると、オブジェクトではなく RFC 3339 形式の文字列になります。

組み込み型やインポートした型にメソッドを追加できるか?

できません。メソッドは、レシーバーの型と同じパッケージで宣言する必要があります。time.Duration にメソッドを追加しようとすると失敗します。

example.gogo
func (d time.Duration) Minutes10() float64 {
	return d.Minutes() / 10
}
example.texttext
./main.go:5:9: cannot define new methods on non-local type time.Duration

int、string をはじめとする事前宣言された型や、ほかのパッケージの型へのエイリアスでも同じです。このルールによって、2 つのパッケージが同じ型に矛盾するメソッドを付けることを防いでいます。振る舞いを追加したいときは、type Cents int64 のように自分の型を定義し、そこにメソッドを付けます。

Go の型エイリアスとは?

エイリアスは、すでにある型に付ける 2 つ目の名前です。= を使って書きます。

example.gogo
type Handler = http.Handler

Handler と http.Handler は同一の型です。一方が必要な場所にもう一方を渡せ、コンパイラが変換を求めることはありません。%T ではもとの名前が出力されます。また、エイリアスは新しいメソッドセットを作りません。エイリアスにメソッドを宣言することは、もとの型に宣言するのと同じなので、「non-local type」のルールもそのまま当てはまります。

Go は自身の事前宣言された名前にもエイリアスを使っています。byte は uint8 の、rune は int32 の、any は interface{} のエイリアスです。[]byte を変換なしで []uint8 を受け取る関数に渡せるのはこのためです。

エイリアスが Go 1.9 で追加されたのは、主に、呼び出し側を壊さずに型をパッケージ間で移動できるようにするためです。大規模なコードベースで Order を billing から orders に移すとき、古いパッケージにはしばらく type Order = orders.Order を残しておきます。まだ billing をインポートしているコードはそのままコンパイルでき、呼び出し側を 1 つずつ移行できます(Go blog)。

型エイリアスをジェネリックにできるか?

Go 1.24 からはできます。エイリアスは型パラメータを宣言でき、使う場所でインスタンス化する必要があります(Go 1.24 リリースノート)。

example.gogo
type Set[K comparable] = map[K]struct{}

func main() {
	allowed := Set[string]{"GET": {}, "HEAD": {}}
	var plain map[string]struct{} = allowed
	_, ok := plain["GET"]
	fmt.Println(ok, len(allowed))
	fmt.Printf("%T\n", allowed)
}

このプログラムは true 2 と出力し、続けて map[string]struct {} と出力します。Set[string] はあくまで map[string]struct{} の別名なので、plain への代入に変換は必要ありません。

定義型と型エイリアスの違いは?

定義型(type UserID int64)エイリアス(type Handler = http.Handler)
新しい型かはい、ほかのどの型とも異なるいいえ、もとの型と同一
もとの型との混在明示的な変換が必要そのまま使える
もとの型のメソッド引き継がれない同じ型なので、同じメソッドを持つ
新しいメソッドの宣言できる(同じパッケージ内)もとの型が自分のパッケージの定義型である場合のみ
%T の出力main.UserIDもとの名前
主な用途ID、単位、enum、ドメインの型パッケージ間での型の移動、短い名前

ほとんどの場合に必要なのは定義型です。エイリアスを使うのは、2 つの名前がまったく同じ型を指す必要があるときで、たいていはリファクタリングの途中です。

Go の関数型はどう使うのか?

関数のシグネチャもほかと同じく型なので、type で名前を付けられます。名前を付ければ、メソッドを持たせることもできます。標準ライブラリはこの仕組みを使って、ただの関数を interface の値に変えています。http.Handler は ServeHTTP というメソッドを 1 つだけ持つ interface で、関数はそのままでは Handler になりません。

example.gogo
func health(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintln(w, "ok")
}

var h http.Handler = health
example.texttext
./main.go:13:23: cannot use health (value of type func(w http.ResponseWriter, r *http.Request)) as http.Handler value in variable declaration: func(w http.ResponseWriter, r *http.Request) does not implement http.Handler (missing method ServeHTTP)

net/http は type HandlerFunc func(ResponseWriter, *Request) を宣言し、その関数自身を呼び出す ServeHTTP メソッドを付けています。http.HandlerFunc(health) と変換すれば、interface を満たす値ができます。http.HandleFunc や mux.HandleFunc は、この変換を代わりに行ってくれます(net/http ドキュメント)。関数値、クロージャ、メソッド値については Go の func キーワードで詳しく解説しています。

同じパターンは、自分で定義するポリシーにも使えます。リトライポリシーは試行回数から待ち時間を返す関数で、その関数型に付けたメソッドでポリシーをラップできます。

example.gogo
package main

import (
	"fmt"
	"time"
)

type RetryPolicy func(attempt int) time.Duration

func (p RetryPolicy) Capped(limit time.Duration) RetryPolicy {
	return func(attempt int) time.Duration {
		return min(p(attempt), limit)
	}
}

func Exponential(base time.Duration) RetryPolicy {
	return func(attempt int) time.Duration {
		return base << attempt
	}
}

func main() {
	policy := Exponential(100 * time.Millisecond).Capped(time.Second)
	for attempt := range 5 {
		fmt.Println(attempt, policy(attempt))
	}
}
example.texttext
0 100ms
1 200ms
2 400ms
3 800ms
4 1s

名前を見れば、その関数が何のためのものかが読み手に伝わります。また Capped の呼び出しは、ほかの値のメソッドを呼ぶのと同じ見た目になります。HTTP クライアントは RetryPolicy 型のフィールドを持てますし、テストでは常に 0 を返すポリシーを渡せます。

Go のジェネリック型はどう使うのか?

型宣言では、名前の後ろの角括弧に型パラメータを書けます。よくある例はページネーションされた API のレスポンスです。ページングのフィールドは、要素の種類が何であっても同じだからです。

example.gogo
package main

import (
	"fmt"
	"strconv"
)

type Page[T any] struct {
	Items      []T
	NextCursor string
}

func (p Page[T]) HasMore() bool {
	return p.NextCursor != ""
}

func (p Page[T]) Map[U any](fn func(T) U) Page[U] {
	out := Page[U]{NextCursor: p.NextCursor}
	for _, item := range p.Items {
		out.Items = append(out.Items, fn(item))
	}
	return out
}

type Order struct {
	ID    int64
	Total int64
}

func main() {
	orders := Page[Order]{
		Items:      []Order{{ID: 1, Total: 4999}, {ID: 2, Total: 1250}},
		NextCursor: "b3JkZXI6Mg",
	}
	ids := orders.Map(func(o Order) string { return strconv.FormatInt(o.ID, 10) })
	fmt.Println(ids.Items, ids.HasMore())
}

このプログラムは [1 2] true と出力します。Page[Order] と Page[string] は、1 つの宣言から作られた 2 つの異なる型です。メソッドでは、func (p Page[T]) のようにレシーバーで型パラメータを繰り返します。

Map は独自の型パラメータ U を宣言しています。これはジェネリックメソッドで、Go 1.27 で新しく加わった機能です(Go 1.27 リリースノート)。go.mod が go 1.26 以前を指定していると、同じコードは次のエラーで失敗します。

example.texttext
./main.go:17:22: generic method requires go1.27 or later (-lang was set to go1.26; check go.mod)

interface のメソッドは、今でも型パラメータを持てません。interface の中に Map[U any](fn func(string) U) []U と書くと interface method must have no type parameters で失敗します。そのため、ジェネリックメソッドが interface を満たすためのメソッドとして数えられることはありません。

Go で type はほかにどこに登場するのか?

型 switch の .(type) は何をするのか?

x.(type) は switch の中でしか使えず、interface の値の動的な型によって分岐します。よくある用途はエラーの判定です。

example.gogo
func describe(err error) string {
	switch e := err.(type) {
	case nil:
		return "ok"
	case *fs.PathError:
		return "file error on " + e.Path
	case net.Error:
		return fmt.Sprintf("network error, timeout=%v", e.Timeout())
	default:
		return "other: " + e.Error()
	}
}

各 case の中では e がその case の型になるので、別途型アサーションを書かなくても e.Path や e.Timeout() がコンパイルできます。os.Open("/etc/levelupgo.yaml") が返したエラーで describe を呼ぶと、ファイルが存在しない場合は file error on /etc/levelupgo.yaml と出力されます。

型 switch が見るのは、いちばん外側のエラーだけです。fmt.Errorf("load config: %w", err) でラップした後は、同じ呼び出しが default に落ちます。ラップされたエラーには、チェーンをたどってくれる errors.As を使います。

example.gogo
var pathErr *fs.PathError
if errors.As(wrapped, &pathErr) {
	fmt.Println("errors.As found", pathErr.Path)
}

ジェネリクスの制約で type はどう使われるのか?

制約は interface で、通常は type で宣言します。制約には、メソッドだけでなく型も列挙できます。型の前に付ける ~ は「基底型がこの型であるすべての型」を意味し、これによって定義型も制約を満たせるようになります。

example.gogo
type Cents int64

type Amount interface {
	~int64 | ~float64
}

func Sum[T Amount](values []T) T {
	var total T
	for _, v := range values {
		total += v
	}
	return total
}

Sum([]Cents{4999, 1250}) は Cents 型の 6249 を返します。~ がなければ Cents は拒否され、コンパイラが直し方を示してくれます。

example.texttext
./main.go:20:17: Cents does not satisfy Amount (possibly missing ~ for int64 in Amount)

Go の enum も定義型をもとに作ります。type OrderStatus int のような型と、iota を使った const ブロックを組み合わせれば、型付きの値のリストができます。その手順は Go の const キーワードで解説しています。また、こうした型の変数がゼロ値から始まる仕組みは Go の var キーワードで扱っています。

LevelUpGo で学ぶ

LevelUpGo では、ブラウザ上で本物の Go コードを実行する演習を通じて Go を学べます。Composite Types では、struct、コンストラクター関数、メソッドを段階的に作っていきます。Interfaces & Polymorphism では、暗黙的な interface、型アサーション、型 switch を扱います。Go Generics Masterclass では、ジェネリック型と独自の制約を学びます。Training Ground には、コースの外で練習できる短い単独の演習があります。残り 24 個の予約語については、Go のキーワード一覧:全 25 個の予約語を解説をご覧ください。

よくある質問

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

はい。type は Go の 25 個の予約語の 1 つなので、変数名や関数名には使えません。型宣言を始めるキーワードで、type UserID int64 のような型定義と、type Handler = http.Handler のようなエイリアスのどちらにも使います。また、型 switch では x.(type) の形で登場します。

Go の型定義と型エイリアスの違いは何ですか?

type UserID int64 のような型定義は、int64 とは異なる新しい型を作り、その型は最初はメソッドを持ちません。type ID = int64 のようなエイリアスは、int64 そのものの別名です。定義型の値をもとの型と混ぜて使うには変換が必要ですが、エイリアスでは 2 つの名前が同じ型を指すので、変換は必要ありません。

Go で組み込み型にメソッドを追加できますか?

できません。メソッドを宣言できるのは同じパッケージで定義した型だけなので、func (n int) IsEven() bool は cannot define new methods on non-local type int で失敗します。type Quantity int のように自分の型を定義し、そこにメソッドを宣言します。

Go で「cannot use id (variable of int64 type UserID) as int64 value」と表示されるのはなぜですか?

UserID が定義型だからです。Go は、基底型が同じであっても、ある名前付きの型から別の名前付きの型へ、型付きの値を自動的に変換することはありません。必要な型に合わせて変わるのは、42 のような型なし定数だけです。int64(id) のように明示的に変換してください。いつも変換しているようなら、その関数はおそらく UserID を受け取るべきです。

Go の .(type) は何をしますか?

型アサーションの、型 switch 用の形です。switch e := err.(type) は、interface の値の動的な型を各 case と照らし合わせ、case の中では e がその case の型になります。switch の中でしか使えません。1 つの型だけを調べるなら v, ok := x.(T) を使い、ラップされたエラーには errors.As を使います。

出典

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

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

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