sagantaf

IT関連の技術記事を書くブログ。

go言語の文法基礎をサンプルとともに(part3)

はじめに

A Tour of GoでGo言語の基本を学んでみましたが、言葉や具体例がいまいち分かりづらい部分もあったため、自分なりにまとめました。 (今の自分のレベルに対する備忘録の側面が強いため、逆に分かりにくい部分もあるかもしれません…)

これはpart3です。

part1 -> go言語の文法基礎をサンプルとともに(part1) - sagantaf

part2 -> go言語の文法基礎をサンプルとともに(part2) - sagantaf


関数と関数型(Method)

複数の引数

可変長引数をとる場合の定義は以下のようになります。

func f(s string, params ...string) {
    fmt.Println(s)
    for _, p := range params {
       fmt.Println(p)
    }
}

これは引数を全てprintする関数となっています。

関数型

Go言語では、関数を変数に格納することができ、その場合、関数を関数型というデータ型で扱うことになります。

func multiply(a int, b int) int {
    return a * b
}
func main(){
    var own_func func(int, int)int // 引数を2つ取り、intをreturnする関数をown_funcという変数として定義
    own_func = multiply // own_funcにmultipyという関数を格納
    fmt.Println(own_func(3,4)) // 12
}

この場合、own_funcという変数の型が関数型となります。

変数だけでなく、引数や戻り値に関数を指定することもできます。

Closures(クロージャ)

関数を変数として定義し、その関数を呼び出すたびに処理を実行することができます。つまり、関数に値を渡して計算をさせ結果を取得することができ、これをクロージャと呼びます。

以下はカウンターを関数で作成した例です。

package main
import "fmt"

func createCounter() func() int {
        n := 0 // initialize
        return func() int { // createCounterが呼び出されるたびにこのreturnが実行される
                n += 1
                return n
        }
}

func main() {
        counter := createCounter() // 関数を変数として定義。initializeの部分のみ実行される
        fmt.Println(counter()) // 1
        fmt.Println(counter()) // 2
        fmt.Println(counter()) // 3
}

構造体のメソッド

go言語では、classがないためclassのメソッドを使う、といったことはできませんが、似たような定義として、構造体にメソッドを持たせることができます。

func (d UserStruct) greeting(greet_word string)  string {
    return greet_word + ", I'm " + d.name + "."
}
func main(){
    taro := UserStruct{"taro", 25, true}
    fmt.Println(taro.greeting("Good Morning"))
}

funcの後ろに構造体を指定します。上記の例だとd Userstructの部分です。これをレシーバ引数と呼びます。後の書き方は、普通の関数の定義と同じです。

ただし、レシーバは上記のように変数として渡すのではなく、ポインタとして渡した方が使いやすくなります。

変数として渡した場合、構造体のメソッドは新たなコピーをreturnします。

例えば、

type nums struct { a, b int }
func (n nums) Twice() nums {
    n.a = n.a * 2
    n.b = n.b * 2
    return n
}
func main(){
    n := nums{2, 5}
    fmt.Println(n.Twice()) // {4 10}
    fmt.Println(n) // {2 5}
}

とした場合は、nに対してTwice()メソッドを実行しても、新たなコピーが作成されるため、元のnは変わっていません。

しかし、classのメソッドのようにして使いたい場合は、元の構造体nが直接変更された方が、便利です。

そこでポインタをレシーバとして渡すことで、元の構造体nのアドレスを渡せるため、Twice()メソッド実行時に新たなコピーを作成することなく、元の構造体nが変化します。

type nums struct { a, b int }
func (n *nums) Twice() *nums {
    n.a = n.a * 2
    n.b = n.b * 2
    return n
}
func main(){
    n := nums{2, 5}
    fmt.Println(n.Twice()) // &{4 10}
    fmt.Println(n) // {2 5}
}

これにより、変数の新たなコピーを作ることもなくなるため、メモリ効率もよくなります。

なお、上記例の場合、レシーバをポインタとして定義しているため、Twice()を呼ぶときは、本当は

fmt.Println((&n).Twice(2))

として書く必要があるはずです。しかし、n.Twice()でも問題なく動いた理由は、レシーバの型を自動的に解釈してくれる機能があるためです。 逆であっても(変数が必要なところをポインタを渡したとしても)自動的に解釈してくれます。

defer と panic と recover

defer

deferは、defer 処理 と書き、この処理の実行を、呼び出し元の関数の終わり(returnする)まで遅延させます。つまり、main内でdeferを使った場合は、mainの処理を最後まで実行した後にdeferに渡した処理を実行させます。評価自体はすぐに実行されますが、処理の実行が最後になる形です。

func main() {
        defer fmt.Println("first")
        fmt.Println("second")
}

この例の場合、出力は

second first

となります。

defer へ渡した処理が複数ある場合、その呼び出しはstackされ、最後にLIFO(last-in-first-out) としてstackした順番とは逆に出力されます。

func main() {
        defer fmt.Println("あ")
        defer fmt.Println("い")
        defer fmt.Println("う")
        fmt.Println("か")
        fmt.Println("き")
        fmt.Println("く")
}

この例の場合、出力は

    か
    き
    く
    う
    い
    あ

となります。

deferの使いどころについては後述しています。

panic

Go言語では、panicという関数を利用して、エラー処理をすることができます。

関数の中でpanicを呼び出すと、その時点で処理が停止し、関数の呼び出し元に戻る仕組みとなっています。main関数まで戻るとスタックトレースを出力し、プログラムが終了します。

func main(){
    fmt.Println("Begin")
    panic("panic happen!")
    fmt.Println("End")
}

上記を実行すると、下記のように表示されます。

Begin
panic: panic happen!

goroutine 1 [running]:
main.main()
        /tmp/sandbox698225432/prog.go:8 +0xa0

deferとpanicの使いどころ

deferは、リソースの後始末をするために利用されることが多いです。

例えば、ファイルのReadをした後にエラーが発生し、ファイルをCloseしないまま終わってしまうことを回避できます。

例は下記のページに記載されています。

Defer, Panic, and Recover - The Go Blog

また、deferに加えてpanic/recoverの使い方やerrとの使い分けについては、下記に詳しく記載されています。

Golang の defer 文と panic/recover 機構について - CUBE SUGAR CONTAINER

インターフェース(Interface)

インターフェースの基本

メソッドの集まりを定義するためにインターフェースは使われます。

type インターフェース名 interface { メソッドA() メソッドB() }という形式で定義します。

インターフェースはその名前の通り、構造体のインターフェースとなるメソッドと考えるとイメージしやすくなります。

戦士という構造体があり、アタッカーというインターフェースがあるとすると、戦士アタッカーというインターフェースをアタッチすることで、戦士はアタッカーの行動が取れる、というイメージです。

より具体的に

戦士という構造体として、

  • 名前
  • レベル

が定義されているとします。

また、アタッカーというインターフェースがあり、下記のメソッドが用意されているとします。

メソッド:

  • 武器を手に取る
  • 狙いを定める
  • 武器で攻撃する

このとき、戦士という構造体に、アタッカーというインターフェースをアタッチすることで、「武器を手に取る」や「武器をで攻撃する」というメソッドを利用できるようになります。

実際に実装すると、以下のようになります。

// アタッカーというインターフェースの定義
type Attacker interface {
        haveWeapon()
        setAim()
        moveWeapon()
}

// 戦士という構造体の定義
type Warrior struct {
        name string
        level int
        weapon string
        aim string
}

// インターフェースのメソッドを定義
func (w *Warrior) haveWeapon() {
        w.weapon = "Sword"
}
func (w *Warrior) setAim() {
        w.aim = "monster"
}
func (w *Warrior) moveWeapon() {
        fmt.Printf("%s attack %s using %s.", w.name, w.aim, w.weapon)
}

func main() {
        w := Warrior{"Taro", 15, "none", "none"}
        var actor Attacker = &w // インターフェースactorと構造体Warriorを紐付け
        
        actor.haveWeapon()
        actor.setAim()
        actor.moveWeapon()
}

出力は、

Taro attack monster using Sword.

となります。

例えば、actor.haveWeapon()actor.setAim()コメントアウトして実行すると、よりインターフェースの動きが理解できると思います。

インターフェースの利点と注意点

戦士という構造体のメソッドとして直接定義した方が手っ取り早いと思うかもしれませんが、その場合、魔法使い盗賊といった構造体を新しく作りたいときに毎回専用のメソッドを定義しなくていけなくなります。

どんな構造体にも紐付けられるメソッドとしてインターフェースを定義しておくことで、魔法使い盗賊といった構造体に対しても同じメソッドをアタッチすることができるため、効率的に開発できます。(オブジェクト指向ポリモーフィズムと同じ)

注意点としては、インターフェースで定義されているメソッドを全て定義する必要があります。定義していない場合はimplementエラーが発生します。(例えば「武器を手に取る」「武器を当てる」のみを定義して、「狙いを定める」を定義しなかった場合)

構造体の中身がnilのままインターフェースをアタッチすることもできる

構造体の中身を定義せずともインターフェースをアタッチすることができます。そのため、例えば先に構造体とインターフェースの枠だけ用意しておき、後から中身を定義するということも可能です。

func main() {
        // w := Warrior{"Taro", 15, "none", "none"}
        var w Warrior
        var actor Attacker = &w // インターフェースactorと構造体Warriorを紐付け

        w.name = "Taro"
        actor.haveWeapon()
        actor.setAim()
        actor.moveWeapon()
}

空インターフェースと型アサーションと型switch

空インターフェース

インターフェースは空っぽの状態でも定義できます。これを空インターフェースと呼び、上述のインターフェースとはまた別の使い方ができます。

空インターフェースは下記のように定義し、どんな型のデータでも格納ができる特徴があります。

var blank_if interface{}
blank_if = 123
blank_if = []int{1, 2, 3}
blank_if = func hello(_ string) string { return "Hello world" }

アサーション

この特徴を活かして、型アサーションすることができます。アサーションというと分かりにくいかもしれませんが、要するに、型のチェックができるということです。

下記のように使います。

var blank_if interface{}
blank_if = 123

inputdata, ok := blank_if.(int)

このようにvalue, ok := <変数>.(type)とすることで、変数とtypeが合っていれば、valueに変数が、okにtrueが格納されます。上記の例でいうと、inputdata=123, ok=trueになります。

変数とtypeが合っていない場合は、valueにはtypeのゼロ値が、okにfalseが格納されます。

inputdata, ok := blank_if.(string) // inputdata= , ok=falseになる
inputdata, ok := blank_if.([]string) // inputdata=[], ok=falseになる
inputdata, ok := blank_if.(func()) // inputdata=<nil> , ok=falseになる

では、この型アサーションができて何が嬉しいかというと、例えば関数に渡されたデータの型によって処理を変えたりすることができます。

具体例を見てみます。

func printItems(items interface{}) {
        var item_list []string

        if value, ok := items.([]string); ok { //string型のsliceの場合
                item_list = value
        } else if value, ok := items.(string); ok { // string型の場合
                item_list = []string{value}
        } else {
                fmt.Printf("you cannot use %T type", items)
                return
        }
        for _, v := range item_list {
                fmt.Println(v)
        }
}

func main() {
        printItems([]string{"hoge", "hogehoge"})
        printItems("string")
        printItems(3)
}

if文の条件として型アサーションを実行し、okにtrueが入れば処理が実行されるように実装しています。これにより、printItems関数はstring型のスライスを入力されても、string型を入力されても問題ないため、柔軟性も持たせられます。

この出力は、

hoge
hogehoge
string
you cannot use int type

になります。

型switch

型switchは、上記の型アサーションのif文をswitch文で行うことです。 上記の例を型switch文で書き換えます。

func printItems(items interface{}) {
        var item_list []string
        
        switch value := items.(type) {
        case []string:
                item_list = value
        case string:
                item_list = []string{value}
        default:
                fmt.Printf("you cannot use %T type", items)
                return
        }
        
        for _, v := range item_list {
                fmt.Println(v)
        }
}

func main() {
        printItems([]string{"hoge", "hogehoge"})
        printItems("string")
        printItems(3)
}

よりシンプルに書けることがわかるかと思います。

並列処理

ゴルーチン (goroutin)

ゴルーチンは並行処理を実現するための仕組みです。 go文で関数を呼び出すことで、実行したプログラムとは別で処理が動き出します。

package main
import (
        "fmt"
        "time"
)

func main(){
        go hello() // hello()関数を別処理として実行
        fmt.Println("main1")   
        time.Sleep(time.Millisecond * 10)
        fmt.Println("main2")
}

func hello(){
        time.Sleep(time.Millisecond * 10)
        fmt.Println("sub1")
}

こうすると、出力は

main1
sub1
main2

となります。

sub1がmain2の前に表示されているため、hello()が並行して稼働していることがわかります。

ただし、main()はhello()の終了を待ってくれないため、main()が終わるとhello()の処理はまだ動いていたとしても終了してしまいます。

2行追加して確認してみます。

func hello(){
        time.Sleep(time.Millisecond * 10)
        fmt.Println("sub1")
        time.Sleep(time.Millisecond * 10) //追加
        fmt.Println("sub2") //追加
}

これを実行しても、sub2が表示されないことが分かるかと思います。これはsub2を表示する前にmain()が最後まで到達してしまったためです。

ゴルーチンを終了してからmain()が終了させるためにはチャネルという仕組みを使います。

チャネル

チャネルはゴルーチン間で値を送受信するための仕組みです。

make関数で生成し、矢印(<-, ->)で通信方向を定義できます。

func add(x int, y int, c chan int) {
        c <- x + y // channelに値を送信
}

func main() {
        c := make(chan int) // channelの作成
        go add(2,3,c) // goroutineの実行
        result := <- c // channelから値を受信
        fmt.Println(result)
}

make時にチャネルの型を明記し、その型でデータを送受信する必要があります。

また、チャネルはバッファを設定することができ、送受信のデータ量を制御できます。

c := make(chan int, 3)

とすると、3回まで送受信ができます。

例えば3回 c <- i という形で送信すると、4回目はブロックされます。

func main() {
        c := make(chan int, 3)
        c <- 1
        c <- 2
        c <- 3
        c <- 4 // ここでバッファを超える
}

上記を実行するとdeadlockというエラーが発生します。

fatal error: all goroutines are asleep - deadlock!

逆にチャネルに何も無い状態で受信したとしても同様のエラーがは発生します。

func main() {
        c := make(chan int, 3)
        fmt.Println(<- c)
}

ゴルーチンの同期

同期の方法は主に3つあります。

ただチャネルの受信を待つパターン

<- doneを書くだけで、ただチャネルが送られてくるのを待つだけの処理になります。

通常は以下を実行すると、worldのみ出力されます。

func wait(done chan bool){
        fmt.Println("hello ")
}

func main(){
        done := make(chan bool)
        go wait(done)
        fmt.Println("world")
}

しかし、以下のように2行追加することで、ゴルーチンの終了を待つようになるため、hello worldと表示されるようになります。

func wait(done chan bool){
        fmt.Println("hello ")
        done <- true  // 追加
}
func main(){
        done := make(chan bool)
        go wait(done)
        <- done // 追加
        fmt.Println("world")
}

closeでチャネルを閉じるパターン

closeを使うことで、チャネルを閉じたことを呼び出し元に伝えられます。

func printnum(n int, c chan int) {
        x := 0
        for i := 0; i < n; i++ {
                c <- x
                x += 1
        }
        close(c)
}

func main() {
        c := make(chan int)
        go printnum(5, c)
        for i := range c {
                fmt.Println(i)
        }
}

チャネルをcloseすることで、rangeループを終了させています。

closeしたことを明示的に受け取って判断するパターン

チャネルを受信するときに、2つ目のreturnにはチャネルのcloseされているかどうかが格納されています。

num, ok <- c

とすることで、okにはチャネルの状況によってtrueもしくはfalseが格納されます。

func wait(c chan int){
        c <- 1
        c <- 2
        close(c)
}

func main(){
        c := make(chan int)
        go wait(c)
        num, ok := <- c
        fmt.Println(num, ok)
        num, ok = <- c
        fmt.Println(num, ok)
        num, ok = <- c
        fmt.Println(num, ok)
}

上記の出力結果は、

1 true
2 true
0 false

となります。

チャネルがcloseされた時には、okにfalseが格納されていることに加えて、numには型のゼロ値(この場合はintなので0)が格納されていることも確認できます。

排他制御(sync.Mutex)

並列処理で同じリソースを扱う場合、どうしても排他制御が必要になってくる時があります。

go言語では、排他制御sync.Mutexによるリソースのロック、アンロックで実現できます。

例えば下記のように、Add()Mul()をゴルーチンで処理させたい場合を考えます。

package main
import (
        "fmt"
        "time"
)

type Counter struct {
        num int
}

func (c *Counter) Add(x int) {
        c.num = c.num + x
}

func (c *Counter) Mul(x int){
        time.Sleep(time.Second) //わざとsleep
        c.num = c.num * x
}

func main() {
        c := Counter{num: 1}
        go c.Add(2)
        go c.Mul(2)
        go c.Add(2)
        go c.Mul(2)

        time.Sleep(time.Second*3)
        fmt.Println(c.num)
}

このプログラムでは、main()にてnum: 1に対して、Add(2)Mul(2)を2回ずつ実行しています。

この計算は、

Add(2) → Mul(2) → Add(2) → Mul(2)

という順番になってほしいところです。

つまり、

(num:1 + 2 * 2) + 2 * 2 = 16

となってほしいですが、実際にはMul()にsleepが入っているため、先にAdd()が2回処理されてから、Add()が2回処理される動きとなってしまいます。

((num:1 + 2) + 2 ) * 2 * 2 = 20

そこで、sync.Mutexを使ってリソースをロックしておくことで、どんなに Mul(2)に時間がかかっても想定した通りの順番で処理を稼働させられます。

package main
import (
        "fmt"
        "sync" //追加
        "time"
)

type Counter struct {
        num int
        mux sync.Mutex//追加
}

func (c *Counter) Add(x int) {
        c.mux.Lock()//追加
        c.num = c.num + x
        c.mux.Unlock()//追加
}

func (c *Counter) Mul(x int){
        c.mux.Lock()//追加
        time.Sleep(time.Second)
        c.num = c.num * x
        c.mux.Unlock()//追加
}

func main() {
        c := Counter{num: 1}
        go c.Add(2)
        go c.Mul(2)
        go c.Add(2)
        go c.Mul(2)

        time.Sleep(time.Second*3)
        fmt.Println(c.num)
}

方法としては、リソースに対する処理を実行するときに、c.mux.Lock()でロックし、処理が終わった後にx.mux.Unlockでアンロックするだけです。

これで想定どおり、20が出力されます。

なお、ロック中にエラーが発生して、アンロックが処理されずに終わるとバグの原因になるため、deferでアンロックを処理する方が安全です。

func (c *Counter) Mul(x int){
        c.mux.Lock()
        defer c.mux.Unlock()
        time.Sleep(time.Second)
        c.num = c.num * x
}

参考