My External Storage

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

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

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

  • type MyType intと宣言するDefined type
    • 以前はNamed typeと言っていたが、Go1.11からDefined typeと呼ぶようになった
  • type MyType = intと宣言するType alias

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

Tl;DR

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

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

2020/11/09更新

Named typeと呼称して記事を書いていたが、Go1.11から該当言語仕様はDefined typeと呼ばれるようになった。

The new type is called a defined type. It is different from any other type, including the type it is created from.

Defined type

Defined typeはGo1.0からある機能だ。(正確な導入時期は知らないが、基本構文でできるので最初からありそうだ)。
Go1.10まではNamed typeと呼ばれていたが、Go1.11から言語仕様上はDefined typeと呼ばれるようになった。

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

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

以下のコードは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)
}

Defined typeはプリミティブな型に対しても利用することができる。
Defined 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
}

Defined 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のようにDefined 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から追加された機能だ。

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

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

異なる型として利用できるようになるDefined typeと違い、型エイリアスを使った場合はまったく同じ型として利用できる。
そのため、Defined 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の説明は省略してしまったが、Defined typeとType Aliasについてまとめた。
Goは型をうまく使うことで安全なコーディングをすることができる。Defined typeを使えばたった一行で新しい型が宣言でき、IDや文字列などの取り間違いを防ぐことができる。

2020/11/09追記

ずっとNamed typeと読んでいたが、ずっと前からDefined typeという名前になっていた。 @yoshiki_shibataさんに指摘していただいた。

参考

関連記事