My External Storage

Feb 1, 2020 - 4 minute read - Comments - Go

[Go] Named typeとType aliasを使い分ける

Goには既存の型に新しい名前をつける方法が2つある。

  • type MyType intと宣言するNamed type
  • type MyType = intと宣言するType alias

すでにいろいろ記事はあるものの、最近数回聞かれることがあったので改めてまとめておく。

Tl;DR

  • Goには型に違う名前をつける方法がある。
    • Named typeとType alias
  • Named typeを使うと完全に違う型として扱える
    • プリミティブな型に異なる型名をつけたり、メソッドを生やすこともできる
    • Value Object的な型を簡単に作ることができる
  • Type aliasを使うと異なる名前だが同じ型として扱われる
    • リファクタリングをするときに使われる
    • 型付けを利用した使い分けをしたいならば向かない
  • 基本的にNamed Typeを使えばよい。

なお、この記事はGo1.13を使って検証している(正確に言うと、2020/02/01時点のGo Playgroundで検証している)。

Named type

Named typeはGo1.0からある機能だ。(正確な導入時期は知らないが、基本構文でできるので最初からありそうだ)。

ある型の定義をそのまま利用して、まったく違う型として認識される新しい型を定義できる。

  • 別の型なので明示的にキャストしないと互換性がない
  • 新しいメソッドを追加できる

以下のコードはhttp.Request型を使った新しいMyRequest型を宣言し、メソッドを追加している。

package main

import (
	"fmt"
	"net/http"
)

type MyRequest http.Request

func (mc MyRequest) MyFunc() {
	fmt.Println("in MyFunc", mc.Host)
}

func useMyRequest(mc MyRequest) {
	mc.MyFunc()
}

func main() {
	myReq := MyRequest{Host: "http://google.com"}
	useMyRequest(myReq)
}

Named typeはプリミティブな型に対しても利用することができる。
Named typeで宣言した新しい型と元になった型には互換性がないため、Value Object的に利用することができる。

以下の例では ユーザー名とメールアドレスの文字列の取り扱いを間違えている。

package main

import "fmt"

type User struct {
	Name, Email string
}

func NewUser(name, email string) User { return User{Name: name, Email: email} }

func main() {
	name := "John Doe"
	mail := "example@foo.com"
	u := NewUser(mail, name) // 順番を間違えている。
	fmt.Println(u.Name) // example@foo.com
}

Named typeを使うことで、型チェックを利用して誤った使いかたを防ぐことができる。

package main

import "fmt"

type Name string
type MailAddress string

type User struct {
	Name  Name
	Email MailAddress
}

func NewUser(name Name, email MailAddress) User { return User{Name: name, Email: email} }

func main() {
	name := Name("John Doe")
	mail := MailAddress("example@foo.com")
	// cannot use mail (type MailAddress) as type Name in argument to NewUser
	// cannot use name (type Name) as type MailAddress in argument to NewUser
	u := NewUser(mail, name)
	fmt.Println(u.Name)
}

type UserID inttype DocumentID intのようにNamed typeを使っていけば、データベースまわりでIDの取り扱いをして間違えることもなくなる。
また、メソッドを追加することもできるので、バリデーションなども追加しておくこともできる。

package main

import (
	"fmt"
	"log"
)

type Password string

func (p Password) Validate() error {
	if len(p) < 16 {
		return fmt.Errorf("パスワードは16文字以上")
	}
	return nil
}

type User struct {
	Name     string
	Password Password
}

func NewUser(n string, pw Password) (*User, error) {
	if err := pw.Validate(); err != nil {
		return nil, err
	}
	return &User{n, pw}, nil
}

func main() {
	n := "John Doe"
	pw := Password("p@ssw0rd")
	u, err := NewUser(n, pw)
	if err != nil {
		log.Fatal(err) // パスワードは16文字以上
	}
	fmt.Println(u.Name)
}

型エイリアス(Type alias)

型エイリアスはGo1.9から追加された機能だ。

型エイリアスは主に特徴を持つ。

  • キャストせずに同じ型として利用できる
  • エイリアスに新しいメソッドは定義できない。

異なる型として利用できるようになるNamed typeと違い、型エイリアスを使った場合はまったく同じ型として利用できる。
そのため、Named Typeのような型チェックを期待してもそれは行われないので注意する。

package main

import "fmt"

type Name = string // type alias
type MailAddress = string // type alias

type User struct {
	Name  Name
	Email MailAddress
}

func NewUser(name Name, email MailAddress) User { return User{Name: name, Email: email} }

func main() {
	name := Name("John Doe")
	mail := MailAddress("example@foo.com")
	u := NewUser(mail, name) // 型違いでコンパイルエラーにはならない!!!!!
	fmt.Println(u.Name) // example@foo.com
}

正直私は型エイリアスを使ったことがない。サードパーティ製のライブラリを使っていて、どうしても困るときがあったら出番なのかもしれない。

型エイリアスを使ったリファクタリングや型エイリアスが必要な背景は公式の記事や、 @tenntenn さんのQiitaの記事が詳しい。

終わりに

Type aliasの説明は省略してしまったが、Named typeとType Aliasについてまとめた。
Goは型をうまく使うことで安全なコーディングをすることができる。Named typeを使えばたった一行で新しい型が宣言でき、IDや文字列などの取り間違いを防ぐことができる。

参考

関連記事