この章では、Go言語における「型を定義する力」について解説されています。
単なる構造体定義にとどまらず、組み込み型の拡張やアプリケーションのドメイン設計にも型を活用できることがわかります。
2.1 型を定義してデータの不整合を防止する
2.1.1 Goにおける型の定義
-
type
キーワードで新しい型を定義可能 - 目的に応じた意味づけされた型を作成することで、不正な代入を防ぐ
type UserID string
type Price int
2.2 既存のデータ型を拡張する
2.2.1 メソッドを追加して組み込み型を拡張する
-
type MyString string
のように再定義し、独自のメソッドを生やす
type MyString string
func (m MyString) Upper() string {
return strings.ToUpper(string(m))
}
2.2.2 拡張した型でも元の機能は利用できる
標準型を元に新しい型を定義すると、元の機能を活かしつつメソッドを追加できます。
例:url.Values を拡張
type MyValues url.Values
func (v MyValues) GetOrDefault(key, def string) string {
if val := url.Values(v).Get(key); val != "" {
return val
}
return def
}
利用例:
m := MyValues{"lang": {"ja"}}
fmt.Println(url.Values(m).Get("lang")) // "ja"
fmt.Println(m.GetOrDefault("mode", "debug")) // "debug"
2.3 アプリケーションドメインに対応した型定義
2.3.1 スライスへの型定義
スライスに型を定義すると、問い合わせ結果に対する処理をメソッドにまとめることができます。
例:UserList
型にドメインロジックを委譲
type User struct {
Name string
Age int
}
type UserList []User
// 未成年ユーザーのみを抽出するドメインロジック
func (ul UserList) Underage() UserList {
var result UserList
for _, u := range ul {
if u.Age < 20 {
result = append(result, u)
}
}
return result
}
利用例(DB取得結果に対して)
users := UserList{
{Name: "Taro", Age: 17},
{Name: "Jiro", Age: 25},
}
fmt.Println(users.Underage()) // [{Taro 17}]
このように、取得結果(スライス)に意味を持たせたメソッドを定義することで、可読性と保守性が高まります。
ロジックがサービス層に散らばらず、ドメイン単位でまとまるのが利点です。
2.3.2 値への型定義
string
や int
などの基本型に明示的な意味を持たせるために型を定義すると、可読性と型安全性が向上します。
例:SKUコードを明示的に扱う
type SKU string
func (s SKU) IsValid() bool {
return strings.HasPrefix(string(s), "SKU-")
}
利用例:
var code SKU = "SKU-12345"
if code.IsValid() {
fmt.Println("有効なSKUコードです")
}
-
string
のままだと"SKU-12345"
なのか"USER-ID"
なのか区別がつきませんが、SKU
型にすることで意味づけと専用メソッドを持たせられます。
2.3.3 列挙への型定義
-
iota
と組み合わせて列挙型を定義し、状態管理などに使う
type Status int
const (
StatusUnknown Status = iota
StatusActive
StatusInactive
)
2.3.4 構造体への型定義
構造体に独自型を定義し、引数として渡すのではなくレシーバーメソッドで操作することで、意図が明確なコードになります。
例:注文情報を表す Order
構造体にロジックを持たせる
type OrderID string
type Yen int
type Order struct {
ID OrderID
Amount Yen
}
// 割引処理などのドメインロジックをレシーバーで保持
func (o *Order) ApplyDiscount(rate float64) {
o.Amount = Yen(float64(o.Amount) * rate)
}
利用例:
order := Order{ID: "ORD-001", Amount: 1000}
order.ApplyDiscount(0.9)
fmt.Println(order.Amount) // 900
-
Order
を毎回関数に渡すのではなく、自分自身に責務を持たせることで自然な設計になります。
このように、構造体を「ただのデータの入れ物」にせず、意味ある振る舞いを持つ自己完結型にすることで、保守性と意図の明確さが向上します。
2.4 型の変換(キャスト)
2.4.1 型変換によって型をキャストする
Goでは、別名がついた型(定義型)は元が同じでも別の型として扱われます。
そのため、暗黙の代入は不可で、明示的な変換が必要です。
例:
type UserID string
type Email string
var id UserID = "user-123"
// var email Email = id // エラー!別の型なので代入できない
var email Email = Email(id) // 明示的にキャストすればOK
-
UserID
もEmail
もstring
ベースですが、Goでは完全に別物とみなされます。 - 「同じベース型でも互換性なし」=明示的な型変換が必要です。
このルールのおかげで、意味の異なるデータをうっかり代入してしまうバグを防止できます。
2.5 機密情報を扱うフィールドを定義して出力書式をカスタマイズする
2.5.1 実装方法
-
String()
メソッドを実装することでfmt.Println()
等での出力を制御可能
type Password string
func (p Password) String() string {
return "****" // 出力時には伏せ字に
}
✅ まとめ
Goでは型を「意味づけ」「安全性」「ドメイン設計」の武器として活用できます。
- 組み込み型の拡張でメソッド追加
- iotaや定義型で状態の明示化
- ファクトリー関数で安全に初期化
型を活用することで、より堅牢で読みやすいGoコードを実現しましょう。