覚えたら書く

IT関係のデベロッパとして日々覚えたことを書き残したいです。twitter: @yyoshikaw

Go言語 - enumを定義する

Go言語には、C言語やJavaにおける「enum(列挙型)」のような機能がありません。
Go言語をやり始めて最初にこれを知った時、 え嘘でしょ? と言いたくなりました。

が、実際には 定数定義とiotaを利用することでC言語における列挙型に近い振る舞いを実現することが可能です。
例えば、Red, Blue, Yellowを持つColorというenumを作る場合は以下のように定義します。

type Color int

const (
    Red Color = iota
    Blue
    Yellow
)


で、iotaって何ですかって話なんですが。
識別子iotaは、定数宣言文(const)内で使用される、型なしの連続する整数定数を表します。
本定数は予約語constが現れた時に0に初期化されて、各定数定義の後に1ずつインクリメントされます。
ちなみに読み方は「イオタ」。

例えば、iotaを使って定数(val0, val1, val2)を定義すると、各定数の値は0, 1, 2になります。

const (
    val0 = iota // val0 == 0
    val1 = iota // val1 == 1
    val2 = iota // val2 == 2
)

iotaは 0 から開始され、(以下例のように)iotaを省略しても評価されて連続する整数がセットされます

const (
    val0 = iota // val0 == 0
    val1        // val1 == 1
    val2        // val2 == 2
)


ということで、冒頭のColorは、実際には以下コード内のコメントのように、Redに0, Blueに1, Yellowに2がセットされています。
これにより疑似的にenumを表現しています。

type Color int

const (
    Red Color = iota // Red    == 0
    Blue             // Blue   == 1
    Yellow           // Yellow == 2
)


実際にはenumとして作成したtypeに対してfmt.Stringerインターフェースを実装(Stringメソッドを用意)しておくのが良いです。
Stringerインターフェースを実装しておいた方がfmt.Println等で表示した際に分かりやすくなります)

そこまでやるとColorの定義は以下のようになります。

type Color int

const (
    Red Color = iota
    Blue
    Yellow
)

func (c Color) String() string {
    switch c {
    case Red:
        return "Red"
    case Blue:
        return "Blue"
    case Yellow:
        return "Yellow"
    default:
        return "Unknown"
    }
}

Stringメソッドのこの実装はどうにかならないんだろうかと良く思いますが・・・)


というわけでGo言語でもenumを定義することができました。

Go言語 - 空インターフェースと型アサーション

Go言語には、全ての型と互換性を持っているinterface{}型(空インターフェース)というものが存在しています。

たとえば以下のように、interface{}で宣言した変数にはどんな型の値でも代入可能です

var obj interface{}

obj = 123                                                              // int
obj = "str"                                                            // string
obj = []string{"linux", "windows", "android"}                          // slice
obj = make(chan string)                                                // channel
obj = func(val int) string { return fmt.Sprintf("number is %d", val) } // function


また、引数の型をinterface{}にすると、どんな型の値でも受け取ることができる関数を記述できます

func anyExec(any interface{}) {
    ・・・
}

func main() {
    anyExec(12)
    anyExec("hello")
    anyExec([]string{"cat", "dog"})
    anyExec([2]string{"hello", "world"})
}


型アサーション

どんな型の値でも受け取れるinterface{}ですが、interface{}型の引数で受け渡された値は、元の型の情報が欠落しています。
(元の型の値を操作するための関数等を実行できません)

Go言語ではこのような局面で利用するための型アサーションを提供しており、型アサーションにより実体の型が何であるかを動的にチェックすることができます。(以下構文を使用します)

<変数>.(<型>)


基本的に以下のように記述します。

value, ok := <変数>.(<型>)

1番目の変数には型アサーション成功時に実際の値が格納されます。2番目の変数には型アサーションの成功の有無(true/false)が格納されます。


以下はサンプルの関数です。引数に型アサーションを用いて型に応じた処理の分岐をさせています

func printIf(src interface{}) {
    if value, ok := src.(int); ok {
        fmt.Printf("parameter is integer. [value: %d]\n", value)
        return
    }

    if value, ok := src.(string); ok {
        value = strings.ToUpper(value) // 対象がstring型なのでstringを引数に取る関数が実行できる
        fmt.Printf("parameter is string. [value: %s]\n", value)
        return
    }

    if value, ok := src.([]string); ok {
        value = append(value, "unknown") // 対象がsliceなのでAppendができる
        fmt.Printf("parameter is slice string. [value: %s]\n", value)
        return
    }

    fmt.Printf("parameter is unknown type. [valueType: %T]\n", src)
}


■呼び出し側

func main() {
    printIf(12)
    printIf("hello")
    printIf([]string{"cat", "dog"})
    printIf([2]string{"hello", "world"})
}

■実行結果

parameter is integer. [value: 12]
parameter is string. [value: HELLO]
parameter is slice string. [value: [cat dog unknown]]
parameter is unknown type. [valueType: [2]string]


型switch

型アサーションと分岐を組み合わせた処理を手軽に記述するための型switchが提供されています。
型switchを利用すると前述のprintIf関数は以下のように記述できます

func printIf(src interface{}) {
    switch value := src.(type) {
    case int:
        fmt.Printf("parameter is integer. [value: %d]\n", value)
    case string:
        value = strings.ToUpper(value) // 対象がstring型なのでstringを引数に取る関数が実行できる
        fmt.Printf("parameter is string. [value: %s]\n", value)
    case []string:
        value = append(value, "<不明>") // 対象がsliceなのでAppendができる
        fmt.Printf("parameter is slice string. [value: %s]\n", value)
    default:
        fmt.Printf("parameter is unknown type. [valueType: %T]\n", src)
    }
}


■呼び出し側

func main() {
    printIf(120)
    printIf("bye")
    printIf([]string{"apple", "orange", "banana"})
    printIf([2]string{"good", "bye"})
}

■実行結果

parameter is integer. [value: 120]
parameter is string. [value: BYE]
parameter is slice string. [value: [apple orange banana <不明>]]
parameter is unknown type. [valueType: [2]string]



関連エントリ

Go言語 - interfaceを触ってみる

Go言語にはメソッドの型だけを定義したinterfaceというものが存在しています。
interfaceは型の一種であり、任意の型が"どのようなメソッドを実装するべきか"を規定するためのものです。

大雑把にいえばJavaでいうところのinterfaceに似たものです。
interfaceを利用することで、オブジェクト指向言語でいうところのポリモーフィズムと同様の機能を実現できます。


インターフェースの定義は以下のように記述します(メソッドは複数記述することができます)

type <型名> interface {
    <メソッド名>(<引数の型>, ...) (<戻り値の型>, ...)
    ・
    ・
}


■具体的なインターフェスの定義

本エントリ内の各サンプルプログラムで利用するためのインターフェスとして、
describeというものを以下のように定義しました。説明文をstring型で返すdescriptionメソッドを1つだけ持ちます

type describe interface {
    description() string
}


■インターフェスを引数に取る関数

describeインターフェースを利用するprintDescription関数を定義して以降のサンプルで利用します。
printDescription関数は、descriptionメソッドを実行して説明文を取得し、その説明文を標準出力に書きだすだけのものです)

func printDescription(d describe) {
    fmt.Printf("Description: %s\n", d.description())
}


以下の各サンプルで具体的にdescribeインターフェースを実装してみます。


既存の組み込み型にインターフェースを実装させる

正確には既存の組み込み型には追加で新しいインターフェースを実装させることはできませんが、
typeを使って既存の組み込み型の別名の型を定義することで、その新しい型にインターフェースを実装させることができます。
本サンプルではint型の別名の方に実装させます


■インターフェースを実装

type MyInt int

// 組み込み型の別名の型をレシーバにして対象インターフェースのメソッドを定義
func (i MyInt) description() string {
    return fmt.Sprintf("MyInt is actually int. value is %d", i)
}


■実行サンプル

func main() {
    val1 := MyInt(100)
    printDescription(val1)
}

■実行結果

Description: MyInt is actually int. value is 100


構造体にインターフェースを実装させる

構造体もtypeで別名を付けて定義することでインターフェースを実装させることができます


■インターフェースを実装

type Square struct {
    edgeLength int
}

// 構造体をレシーバにして対象インターフェースのメソッドを定義
func (sq *Square) description() string {
    return fmt.Sprintf("The lengths of all four sides are equal. [edgeLength: %d]", sq.edgeLength)
}


■実行サンプル

func main() {
    sq := &Square{edgeLength: 10}
    printDescription(sq)
}

■実行結果

Description: The lengths of all four sides are equal. [edgeLength: 10]


関数にインターフェースを実装させる

関数もtypeで別名を付けて定義することでインターフェースを実装させることができます


■インターフェースを実装

type Product struct {
    id    uint
    name  string
    price uint
    PR    PRStatement
}

type PRStatement func() string

// PRStatementという関数をレシーバにして対象インターフェースのメソッドを定義
func (pr PRStatement) description() string {
    return pr()
}


■実行サンプル

func main() {
    p1 := &Product{id: 1, name: "Golang PC", price: 10000}
    p1.PR = func() string {
        return fmt.Sprintf("この %s は、値段が%d円なのでとてもお買い得です", p1.name, p1.price)
    }
    printDescription(p1.PR)

    p2 := &Product{id: 2, name: "リンゴ", price: 100}
    p2.PR = func() string {
        return fmt.Sprintf("この %s は、とても美味しいです", p2.name)
    }
    printDescription(p2.PR)
}

■実行結果

Description: この Golang PC は、値段が10000円なのでとてもお買い得です
Description: この リンゴ は、とても美味しいです

describeインターフェースの実装内容によって出力結果が変わっています。


まとめ

というわけでinterfaceを利用することで、Goのプログラムに型の柔軟性を与えることができることが分かりました



関連エントリ

Go言語 - forループによる繰り返し処理

Go言語で繰り返し処理(ループ)を記述するために用意されている構文はforのみとなっています。
whiledo whileといったものは提供されていません。


無限ループ

他の言語であれば、無限ループを記述する場合は、 while(true) { xxx } といったようにwhileを利用する場合が多いと思います。

が、先に書いたようにGo言語にはwhileはありません。forを使って以下のように記述します。

for {
    // ここに無限ループで実行する処理を記述
    fmt.Println("Running...")
}


独特と言えば独特ですが、慣れてしまえばなんてことない気もします。


while的な記述

他言語のwhile文のようにループ条件だけを記述したい場合はforで以下のように書くことができます

coundown := 10
for coundown > 0 {
    fmt.Printf("coundown: %d\n", coundown)
    coundown--
}


典型的なfor

ループ変数を利用して1ずつ値を増加させてある最大値まで処理をさせるといった、基本的なループは以下のように記述します

for i := 0; i < 10; i++ {
    // iの値が0から9まで変化していく
    fmt.Printf("i = %d\n", i)
} 

Go言語の構文の関係で、他言語でみられる 初期値や条件を指定する部分を括弧で囲む という記述はできません


rangeを使ったfor

いわゆる 拡張for文(foreach文)と呼ばれるループを行う場合はrange節を使用します


例えば配列に対するrangeを利用したforループは以下のように記述できます

strArray := [5]string{"taro", "jiro", "hanako", "ken", "tom"}

for i, s := range strArray {
    fmt.Printf("index: %d, name: %s\n", i, s)
}

配列に対するrangeでは、index番号と要素の値が返されます。

例えば、配列に対するrangeで返されるindex番号が処理上不要という場合には以下のように無視することもできます。

strArray := [5]string{"taro", "jiro", "hanako", "ken", "tom"}

for _, s := range strArray {
    fmt.Printf("name: %s\n", s)
}

Go言語 - メソッド

Go言語には、任意の型に関数を紐づけるためのメソッドという機能が存在しています。


メソッドの定義方法

メソッドの定義は以下のように記述します。レシーバーに関する記述をする部分が、関数と異なる箇所になります

func (<レシーバー>) <関数名>([引数]) [戻り値の型] {
    [関数の本体]
}

例えばPersonという構造体を定義して、そのPersonのプロフィールを返すメソッドを定義する場合は以下のような記述をします

type Person struct {
    name string
    age  uint
}

func (p *Person) profile(header string) string {
    return fmt.Sprintf("%s Profile[name: %s, age: %d]", header, p.name, p.age)
}

func (p Person) profile2(header string) string {
    return fmt.Sprintf("%s Profile[name: %s, age: %d]", header, p.name, p.age)
}


定義したメソッドは、 <レシーバ>.<メソッド> という形式で呼び出すことができます

func main() {
    person1 := &Person{name: "KimuraJiro", age: 28}
    fmt.Printf("person1 -> %s\n", person1.profile("!!!"))

    person2 := &Person{name: "KimuraTaro", age: 38}
    fmt.Printf("person2 -> %s\n", person2.profile2("@@@"))
}

■実行結果

person1 -> !!! Profile[name: KimuraJiro, age: 28]
person2 -> @@@ Profile[name: KimuraTaro, age: 38]

定義したメソッドの内容が実行されていることが分かります


メソッドは関数としてどう定義されているか

<レシーバの型>.<メソッド>でメソッドを関数型として参照することができます。

これを利用してPersonに定義したprofileメソッドとprofile2メソッドの関数定義を確認してみます

f1 := (*Person).profile
f2 := (Person).profile2

fmt.Printf("Person.profile -> %T\n", f1)
fmt.Printf("Person.profile2 -> %T\n", f2)

■実行結果

Person.profile -> func(*main.Person, string) string
Person.profile2 -> func(main.Person, string) string

実行結果から、メソッドのレシーバを第1引数にした関数になっていることがわかります


レシーバを値型にしたりポイント型にしたり

Person型をレシーバとするリネームメソッドとPersonのポインタ型をレシーバとするリネームメソッドを以下のように定義します。
引数で与えられた文字列をレシーバPersonnameフィールドにセットしています。

type Person struct {
    name string
    age  uint
}

func (p *Person) rename1(newName string) {
    p.name = newName
}

func (p Person) rename2(newName string) {
    p.name = newName
}


値型に定義したメソッドとポインタに定義したメソッドを実行して結果の違いを確認してみます

fmt.Printf("Person.rename1 -> %T\n", (*Person).rename1)
fmt.Printf("Person.rename2 -> %T\n", (Person).rename2)

personA := &Person{name: "Hanako", age: 19}
oldName := personA.name

personA.rename1("Kumiko")

fmt.Printf("rename1 exec renamed. %s -> %s\n", oldName, personA.name)

personB := &Person{name: "Rika", age: 19}
oldName = personB.name

personB.rename2("Miho")

fmt.Printf("rename2 exec renamed. %s -> %s\n", oldName, personB.name)


■実行結果

Person.rename1 -> func(*main.Person, string)
Person.rename2 -> func(main.Person, string)
rename1 exec renamed. Hanako -> Kumiko
rename2 exec renamed. Rika -> Rika

実行結果から、
ポインタ型に対するメソッド呼び出しの場合はレシーバ(構造体)の値が変更されていますが、
値型に対するメソッド呼び出しの場合はレシーバ(構造体)の値が変更されていないことが分かります。

これは、以下の事を勘案すれば納得いく結果です

  • メソッドがレシーバを第1引数に取る関数であること
  • 値渡しした引数はコピーされて関数に渡される



関連エントリ