type X stringで、型できる。
type TableID string と書くと、TableID という新しい型ができる。中身のメモリは string と同じ。それでも string とは別の型。代入できるかどうかは、メモリの形ではなく型の名前で決まる。
type DatasetID string
type TableID string
func loadTable(ds DatasetID, tb TableID) string {
return fmt.Sprintf("%s.%s", ds, tb)
}
ds := DatasetID("raw")
tb := TableID("orders")
loadTable(ds, tb) // raw.orders
loadTable(tb, ds) // コンパイルエラー:cannot use tb (variable of string type TableID) as DatasetID value
引数がどちらも string だと、逆に渡してもコンパイルが通る。
raw.orders に書くつもりで orders.raw に書いてしまい、気づくのは本番。型を分けておけば、ビルドの時点で止まる。
| 書き方 | 通るか | 理由 |
|---|---|---|
var t TableID = s(s は string の変数) | 通らない | 型が違う |
var t TableID = TableID(s) | 通る | 自分で型を変換している |
var t TableID = "orders" | 通る | "orders" は型の付いていない定数。代入先の型に合わせて変わる |
止まるのは型の付いた変数を取り違えたときだけ。リテラルは素通りするので、loadTable("orders", "raw") はコンパイルが通る。
入れ違いなし、短くなる、X だけのメソッドを作れる
string でも、DatasetID と TableID は混ざらないstruct{ Name string; Age int } を毎回書かずに User で済む= を付けるとエイリアス
=を付けたらエイリアスになるだけ。名前は違うけど、両方 string。
type TableID string // 定義型。string とは別の型
type ColumnName = string // エイリアス。string に別名を付けただけ
var s string = "x"
var c ColumnName = s // 通る。同じ型なので
関数の中で var int int = 1 と書くと、コンパイルは通る。int は if や func のような予約語ではなく、Go があらかじめ用意している名前(事前宣言された識別子)だから。ただし、その後のスコープでは int が変数を指すので、型としての int が使えなくなる(int (local variable) is not a type)。len error url も同じ。
Go の型は一級市民ではない。t := int のように、型そのものを変数に入れることはできない。型を値として扱いたいときは reflect.TypeOf(値) を使う。ライブラリを作る側の道具なので、今は名前だけ。
影響なし、他パッケージは無理/操作は引き継ぐ/検査のため
type Minutes int の int のように、材料になった型を基底型という。定義型には、int や string が元でもメソッドを足せる。
type Minutes int
// ↓レシーバー:Minutes の値に付く。中では m と呼ぶ
func (m Minutes) Hours() float64 {
return float64(m) / 60
}
var m Minutes = 90
m.Hours() // 1.5
this や self を自分で名前を付けて受け取るもの。型の頭文字1文字が慣習Minutes にだけ付く。ほかの int には何も起きないn := 90 // int
n.Hours() // コンパイルエラー:type int has no field or method Hours
Minutes(n).Hours() // 1.5
Minutes(n) は関数の呼び出しではなく型変換。見た目は同じ。カッコの前が type で作った名前なら変換、func で作った名前なら呼び出し。
同じ名前のメソッドでも、呼ばれるのは値の型に付いているほう。
type Seconds int
func (s Seconds) Hours() float64 { return float64(s) / 3600 }
var x Minutes = 90
var y Seconds = 90
fmt.Println(x.Hours(), y.Hours()) // 1.5 0.025
よそのパッケージの型(time.Duration など)には、メソッドを直接足せない。足したいなら自分のパッケージで型を作る。
| 引き継ぐか | 例 | |
|---|---|---|
| 基底型の操作 | 引き継ぐ | Minutes 同士の足し算、マップが元なら r["id"]・len・range |
| 型の区別 | 残る(混ぜられない) | Minutes と int の変数は足せない(mismatched types Minutes and int) |
| 元の型のメソッド | 引き継がない | type MyDuration time.Duration には .Hours() が付いてこない |
type Row map[string]string
func (r Row) ID() string { return r["id"] }
r := Row{} // マップと同じで初期化が要る。var r Row のままだと書き込みで panic
r["id"] = "1" // マップの操作がそのまま使える
len(r) // 1
r.ID() // "1"
メソッドで全部包むか、基底型の操作も使わせるかは設計で決める。
型変換は値を検査しない。BatchSize(-3) は -3 のまま通る。不正な値を防ぎたいなら、検査を入れた New で始まる関数を用意する。
type BatchSize int
// 検査は型のメソッドに置く
func (b BatchSize) Validate() error {
if b < 1 || b > 1000 {
return fmt.Errorf("batch size must be 1..1000: %d", int(b))
}
return nil
}
func NewBatchSize(n int) (BatchSize, error) {
b := BatchSize(n)
err := b.Validate()
if err != nil {
return 0, err
}
return b, nil
}
NewBatchSize(-3) // 0 batch size must be 1..1000: -3
NewBatchSize(500) // 500 <nil>
NewBatchSize で作る」とわかるd := "2026-10-23"
year := d[0:4] // 何を取り出しているのか、読んでもすぐにはわからない
type Date string
func (d Date) Year() string {
return string(d[0:4]) // 取り出し方はここに1回だけ書く
}
year := d.Year() // 読めばわかる
呼び出し元に散らばっている処理を、その値の型のメソッドに寄せる。寄せると、呼び出し元は読んでわかる言葉だけになり、寄せたメソッドは1つずつテストできる。形は4つあるが、やっていることは全部これ。
| 形 | 寄せる前 | 寄せた後 |
|---|---|---|
| スライス | 呼び出し元で毎回 for と if | rows.FailedToLoad() |
| 値 | 呼び出し元で d[0:4] と切り出す | d.Year() |
| 列挙 | 呼び出し元で if plan == PlanPro | plan.MonthlyFee() |
| 構造体 | IsAdult(customer) のように構造体を引数に取る関数 | customer.IsAdult() |
type Record struct {
Table string
Err string
}
type Records []Record
func (rs Records) FailedToLoad() Records {
out := make([]Record, 0, len(rs))
for _, r := range rs {
if r.Err != "" {
out = append(out, r)
}
}
return out
}
// InTable(t string) も同じ形で、条件を r.Table == t にしたもの
rows.InTable("orders").FailedToLoad()
つなげて書けるのは、戻り値を Records にそろえたから。レシーバーが同じ型だからではない。戻り値が []Record だと、そこで .FailedToLoad() が付かなくなって切れる。
取ってきたデータにループを回す処理が多いなら、コードを見直すきっかけ。
type SubscriptionPlan int
const (
PlanFree SubscriptionPlan = iota + 1 // 1
PlanPro // 2
PlanTeam // 3
)
func (p SubscriptionPlan) MonthlyFee() int {
switch p {
case PlanPro:
return 1000
case PlanTeam:
return 3000
}
return 0
}
iota は const ( ) の中で 0, 1, 2… と自動で数を振る。2行目以降は型と = iota を省略できるiota + 1 で1から始めると、何も入れていない変数(ゼロ値の 0)がどのプランとも一致しない。「設定し忘れ」と PlanFree を区別できるMonthlyFee() の中だけPlan)を付けると、PlanPro だけで何の列挙かわかるfunc IsAdult(c Customer) bool { return c.Age >= 18 } // 関数のまま
func (c Customer) IsAdult() bool { return c.Age >= 18 } // メソッド
中身は同じ。Customer をカッコの中に書くか、関数名の前に書くかだけ。関数の中で引数の構造体のフィールドばかり触っていたら、メソッドにする候補。
構造体が2つ出てくるときは、その処理が誰のことを聞いているかで分ける。
// 「この客は何割引きか」は Customer のこと
func (c Customer) MemberDiscountRate() float64 {
if c.Premium {
return 0.1
}
return 0
}
// 「割引を反映した合計」は Order のこと
func (o Order) TotalAfterDiscount(rate float64) int {
return int(float64(o.Amount) * (1 - rate))
}
total := order.TotalAfterDiscount(customer.MemberDiscountRate())
Order は Customer の中身を知らずに済む。どちらのことでもない処理(注文を確定してメールを送って在庫を減らす、のような段取り)は、無理にメソッドにしない。
このサイトのノートから、知りたいことが書いてあるページを探して案内するよ。
AI の案内はまちがうことがあるよ。中身はリンク先のノートで確かめてね。