機会があってGoに入門してみた。Goはシンプルで機能が少ないらしいので、基本的なことを抑えればある程度読めそうと期待。
この記事で書くのは以下の二つ。
- Hello World
環境構築まではやる。それ以外に多少目的あるコード書くのもやった。それは別記事に分ける。個人メモ〜Go言語入門(少し目的のあるコードを書く編) - 文法についてメモ
自分が概念を思い出せるように書くだけ。for文とかは調べればすぐ出てくるので、へぇと思ったところだけ。ゴルーチンのところはサンプルを動かしつつ。
環境構築
OS: Ubuntu 24.04 LTS
コマンドやバージョンは公式から。
https://go.dev/dl/
https://go.dev/doc/install
以下を実行
curl -OL https://go.dev/dl/go1.22.5.linux-amd64.tar.gz
rm -rf /usr/local/go && sudo tar -C /usr/local -xzf go1.22.5.linux-amd64.tar.gz
echo 'export PATH=$PATH:/usr/local/go/bin' >> $HOME/.profile
source $HOME/.profile
go version
# go version go1.22.5 linux/amd64
特にまだ必要ないが一応以下も実行。
go mod init hello
hello.go
を作成し、go run hello.go
で実行
package main
// helloモジュールでもファイル名がhelloでも、packageはmain
import (
"fmt"
)
func main() {
fmt.Printf("Hello, World!\n")
}
今回はgo mod init hello
としたが、それでも常にpackage main
にするらしい。
https://go.dev/doc/code#ImportingLocal
The first statement in a Go source file must be package name. Executable commands must always use package main.
文法
変数定義
関数内では :=
で変数宣言できる。
関数の外では:=
での定義はできないので、以下のように定義する。
var str = "hoge"
定数の場合はvar
ではなくconst
を使い、関数内でも :=
で宣言できない。
C言語と同じ概念
構造体、ポインタがある。使い方も多分同じ。
structの初期化方法はいくつかあり、newを使う場合はポインタを返す。
a := new(Animal)
// a := &Animal{} // 上記と同じ意味
継承の機能はないが、structの埋め込みというものはある(structのフィールドに別のstructを定義するだけのように見える)
関数
シンプルな定義
こう。
func someMethod(name string, age int) (string, error) {
// 処理
}
-
name
,age
が引数で、(string, error)
が返り値の型(Goでは複数の値を返せる) - 1文字目が大文字だとexportedメソッドとなり、外部パッケージから参照できる。シンプルなファイルなら小文字で良さそう
https://go.dev/doc/code
Because our ReverseRunes function begins with an upper-case letter, it is exported, and can be used in other packages that import our morestrings package.
レシーバーを使った定義
先ほどのとは別に、構造体と関連付けたメソッドを定義することでオブジェクト指向likeな表現ができるようだ。
書き方としては以下。
func (c *Client) someMethod() (string, error) {
// 処理
}
(c *Client)
の部分がレシーバーっぽい。レシーバーに指定している構造体のインスタンスが指定されてメソッドが実行されると思っておけば良さげ。使い方は以下などをみて都度思い出す。
https://qiita.com/ryo_manba/items/c567858befd04602e3ec
defer
deferをつけた処理は関数の処理終了時に実行されるようになる。接続のクローズ処理とかで使えそう。
なお、複数の処理にdeferをつけた場合はスタックされるので後ろのものから順に実行されることになる。
データ型
interface{}
型
任意の型を表す。Goバージョン1.18以降ではanyが使えるようになり、同じものと思って良さそう(細かい違いはあるかもしれないが)。
データ構造
配列
配列は固定長なので、後述のスライスの方が便利そう。
配列定義では要素数を明示しないことで、引数の数を勝手に数えて長さを定義してくれる(それでも固定長)。以下のように書くと勝手に要素数2の配列になる。
arr := [...] string{"Golang", "Java"}
スライス
sliceは可変長の配列のようなもの。appendで要素を追加できる。
var slice []int
とかで定義できる。makeを使っての定義も可能。
map
map[キーの型]値の型
という形式で定義。
値の型をanyにすることで、JSONオブジェクトのようなものを作れる。
map[string]interface{}
or
map[string]any
こんな感じで初期化できる。
// いずれも値がintだけの場合
m := map[string]int{"key1": 1, "key2": 2, "key3": 3}
// これらはさらに、空オブジェクトのようなもの
m := map[string]int{}
m := make(map[string]int)
参照するときはドット記法(m.key1
)ではダメで、m["key1"]
と取得する。
他のAPIからのレスポンスなどを扱うときは構造体を定義するのが本来はいいのであろうが、結構便利そう。
ゴルーチン(goroutine)とチャネル
実際の例はここをすごくみさせてもらった。
https://qiita.com/k-penguin-sato/items/5b09fa89d8d231bcdac8
ゴルーチン
関数呼び出し時にgoをつけて呼び出すとゴルーチンになる。スレッドのようなものと理解した。
ゴルーチンの終了前に呼び出し元が終了した場合にはゴルーチンの出力結果が得られないため(止まる)、channelを使ってデータを受け取るよう処理をブロックする。
チャネル
チャネルの定義方法は以下。バッファイサイズも指定できる。
ch := make(chan 型)
ch := make(chan 型, バッファサイズ)
チャネルとのデータの送受信方法は以下。わかりやすい。
ch <- data //dataをchへ送信する
arg := <-ch // chの値をargに代入
チャネルの受信側では受信可能なデータがくるまで処理がブロックされる!(処理待ちができる
ゴルーチンとチャネルを使ったシンプルなコード
goroutineというメソッドを作成してゴルーチンとして呼び出すだけ。
goroutineメソッドは指定された時間だけsleepだけして、終了後にチャネルへdoneという文字を送信する。
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan string)
go goroutine(3, ch)
fmt.Println(<-ch)
}
func goroutine(sleepTime time.Duration, ch chan string) {
fmt.Printf("goroutine start! wait time is %s\n", sleepTime)
time.Sleep(sleepTime * time.Second)
fmt.Printf("goroutine end! wait time is %s\n", sleepTime)
ch <- "done"
}
出力はこうなる。goroutineが終わるまでmain()
の処理はブロックされている。
goroutine start! wait time is 3ns
goroutine end! wait time is 3ns
done
なお、main()
の最後のfmt.Println(<-ch)
がない場合はgoroutineの終了を待たないため、何も出力されずに終わる。
select
ゴルーチン用のswitchのようなもの。以下のような特徴がある。
https://qiita.com/keisuke-333/items/af1222e14018dd8b6131
- 非ブロッキング: select文は、実行可能なcaseがない場合、即座にdefaultケースを実行します。defaultがない場合、実行可能なcaseが現れるまでブロックされます。
- 複数のcaseが実行可能な場合: もし複数のcaseが同時に実行可能だった場合、selectはランダムにその中の1つを選択して実行します。
- タイムアウトの実装: select文は、特定のタイムアウトを持つ操作のために頻繁に使用されます。これは、time.After関数と組み合わせることで、操作が一定時間内に完了しない場合のタイムアウトを実装するのに便利です。
context
ゴルーチンのタイムアウト設定とタイムアウト時のキャンセルなどをよしなにやってくれる。
contextの説明はこれが丁寧。
https://zenn.dev/hsaki/books/golang-context/viewer/intro
selectとcontextを使ったコード
ゴルーチンとチャネルだけを使ったコードに少し変更を加えて、selectとcontextの動きを見る。
ゴルーチンはsleepを3秒として呼び出しているので、以下のコードだとcontextタイムアウト(2秒)が先に発生し、case <-ctx.Done():
に分岐する。
goroutineの方のメソッドも<-ctx.Done()
をしておかないと処理がそのまま続行してしまうようだ。
package main
import (
"context"
"fmt"
"time"
)
func main() {
ch := make(chan string)
// タイムアウト2秒の context 定義
ctx, _ := context.WithTimeout(context.Background(), 2000*time.Millisecond)
go goroutine(3, ch, ctx)
//fmt.Println(<-ch)の代わりにselect
select {
// ゴルーチンが完了したらこのcase
case done := <-ch:
fmt.Println(done)
// contextがタイムアウトしたらこのcase
case <-ctx.Done():
fmt.Println("canceled: ", ctx.Err())
// default:
// fmt.Println("default")
}
fmt.Println("main end")
//goroutineの処理の様子を見るために待つ
time.Sleep(5 * time.Second)
}
// 引数にcontextも追加
func goroutine(sleepTime time.Duration, ch chan string, ctx context.Context) {
fmt.Printf("goroutine start! wait time is %s\n", sleepTime)
//time.Sleep(sleepTime * time.Second)
select {
case <-ctx.Done():
fmt.Printf("goroutine timed out! wait time is %s\n", sleepTime)
case <-time.After(sleepTime * time.Second):
fmt.Printf("goroutine end! wait time is %s\n", sleepTime)
ch <- "goroutine done"
}
}
出力はこうなる。
goroutine start! wait time is 3ns
canceled: context deadline exceeded
main end
goroutine timed out! wait time is 3ns
なお、selectにdefaultを入れた場合は先ほど引用した挙動により処理のブロックは起こらず、defaultが実行されて終わる。