Go言語基礎 その7 Go言語の基本構文5 (可視性, スコープ, goto, defer, panic, recover, goroutine, channel)

はじめに

前回の続きで Go 言語の文法をまとめておきたいと思います。

Go言語文法

可視性

  • 定数、変数、関数、フィールド、メソッドが他のパッケージから参照可能であるかどうかは 識別子の 1 文字目が大文字 で決定する
  • 先頭が大文字であればパッケージ外からアクセス可能
    • 例えば、 json.Marshaljson.Unmarshal で使用したいフィールドは先頭を大文字のフィールド名にする必要がある(外部のjsonパッケージからアクセスする必要があるため)
var OK = 1 // package 外からアクセス可能
var ng = 2 // package 外からアクセス不可

スコープ

  • スコープは大きい順に パッケージ、ファイル、関数、ブロック、制御構文
  • スコープが小さい順に優先される
var a = 10
func printNumber() {
  fmt.Println(a) // => 10

  a = 20
  fmt.Println(a) // => 20

  {
    a := 30
    fmt.Println(a) // => 30
  }
  {
    a := 40
    fmt.Println(a) // => 40
  }

  if a = 100; a > 50 {
    fmt.Println(a) // => 100
  }
}

goto

無理に使う必要はないと思いますが、一応 Go でも goto 使えます。


func doSomething() error {
  if err := func1(); err != nil {
    goto Error
  }
  if err := func2(); err != nil {
    goto Error
  }
  if err := func3(); err != nil {
    goto Error
  }
  if err := func4(); err != nil {
    goto Error
  }
  if err := func5(); err != nil {
    goto Error
  }

  return nil

Error:
  return fmt.Errorf("doSomething error")
}

defer

  • defer で実行する関数は呼び出し元の関数が終了(return する)時に実行される
  • defer へ渡した関数は LIFO(last-in-first-out) の順番で実行される
  • defer に渡した関数の引数は呼び出し時に評価される
// 1st -> 2nd -> 3rdの順で呼び出される
func doSomething() {
  defer fmt.Println("3rd")
  defer fmt.Println("2nd")
  defer fmt.Println("1st")
}

panic

  • 予期せぬ問題が起きたことを示すために使う
    • 回復の余地がないとき
    • 異常終了させたいとき
  • panic が発生するとエラーメッセージとゴルーチンのトレースが表示され、プログラムが終了する
  • Go のライブラリにおける任意の関数が panic を起こす可能性があることを前提にデザインされるようなことは絶対に避ける
func main() {
  panic("What The Fuck")
}

recover

panic 関数が呼ばれた場合にその内容を取得できる。

func main() {
	defer func() {
		fmt.Println("recover started")
		err := recover()
		if err != nil {
			fmt.Println("recovered", err)
		}
	}()
	fmt.Println("panic occured")
	panic("panic")
}
/*
出力結果
panic occured
recover started
recovered panic
*/

go (goroutine)

  • Go特有でGoのプログラムでの最も基本的な構成単位
    • OSスレッドではなく、必ずしもグリーンスレッド(言語のランタイムにより管理されるスレッド)ではない
    • コルーチンとして知られる高水準の抽象化
    • ゴルーチンの生成コストは非常に低い
  • すべてのGoプログラムには最低1つのゴルーチンが存在する
  • 非同期処理を行いたいときに使用
func main () {
  printFunc := func() {
    fmt.Println("Hello, printFunc goroutine!")
  }
  go printFunc
  fmt.Println("Hello, main goroutine!")
}

channel(チャネル)

  • ゴルーチンとゴルーチンの間でデータの受け渡しを司るためにデザインされた Go に特有のデータ構造
    • チャネルを使ってメモリの共有アクセスを回避し、データ競合を避ける
    • チャネルはFIFOキュー(待ち行列)の性質を備える
    • バッファはキューのサイズ
  • チャネルはあくまで複数のゴルーチン間で安全にデータを共有するための仕組み
  • ゴルーチンがチャネル操作によって停止(ブロック)する条件は以下のいずれか
    • 送信時 チャネル内のバッファが満杯だとブロック
    • 受信時 チャネル内のデータが存在しないとブロック
var ch1 chan int
var ch2 <-chan int  // 受信専用のチャネル
var ch3 chan-> int // 送信専用のチャネル

ch4 := make(chan int, 10) // バッファサイズ10

channel + goroutine を使用したコードのサンプル

package main

import (
    "fmt"
)

func receiver(ch <-chan int) {
    for {
        i := <-ch
        fmt.Println(i)
    }
}

func main() {
    ch := make(chan int)

    go receiver(ch)

    i := 0
    for i < 10000 {
        ch <- i
        i++
    }
}

/*
出力結果
1
2
3
4
5
(以下略)
*/

バッファサイズを超えたデータを超えると、ランタイムパニックが発生する

ch := make(chan rune, 3)
ch <- 'A'
ch <- 'B'
ch <- 'C'
ch <- 'D' // panic発生
  • チャネルはcloseできる
  • 送信側で1度だけcloseを行う
  • closeを行ったチャネルには送信できない
ch := make(chan rune, 3)
ch <- 'A'
ch <- 'B'
len(ch) // == 2
cap(ch) // == 3
close(ch)

// ch <- 'C' // closeしたチャネルに対して送信するとpanic

// closeしたあとでも受信は可能
<-ch // == 'A' 
<-ch // == 'B'

// チャネルのバッファが空でかつcloseされた状態のときのみfalse
i, ok := <-ch // i == 0, ok == false 
  • select を使った複数のゴルーチン処理
  • select を使った場合、複数の channel に値があれば、どれかひとつをランダムに評価 (switch とは異なり必ず上から順番に評価されるわけではない)
// 複数のcaseが成立する場合は、ランダムに選択される
select {
case e1 := <- ch1:
    // ch1からの受信が成功した場合の処理
case e2 := <- ch2:
    // ch2からの受信が成功した場合の処理
default:
    // case節の条件が成立しなかった場合
}

range も使用可能だが、使い所は限られる

参考にした本

おわりに

ひとまずGo言語の文法を最低限まとめました。