2
0

More than 3 years have passed since last update.

Go言語入門 --基本文法--

Last updated at Posted at 2021-05-20

Go言語の基本構文の説明を整理も兼ねて作成した。他の言語でよく使われる機能そのものの説明はしていない(配列など)。
説明に使用したコードは完全なものでない。コードを試したい時は随時パッケージのインポートや所属するパッケージを宣言(package main)すると良い。
Goの導入や基本的な構造(Hello Worldしただけ)はこの記事に書いたので気になる方は参照していただきたい。

数値の型

###整数の型
int int8 int16 int32 int64
一般的な整数の型として5つの型がある。
intNは$N$ビット幅の値となり、$-2^{N-1}\sim 2^{N-1}-1$の範囲の値を持つことができる。intは環境に依存する型であり、一般的にint64に相当することが多い(int32に相当することも多い)。冒頭に0をつけることで8進数、0xをつけることで16進数をリテラル表記することができる。(03140x314のように)

符号なし整数の型

uint uint8 uint16 uint32 uint64
0以上の整数の型として5つの型がある。
uintNは$N$ビット幅の値となり、$0\sim 2^N-1$の範囲の値を持つことができる。

ポインタを扱う整数型

uintptr
ポインタを扱うために用意された符号なしの整数型である。

浮動小数点(実数)

float32 float64
少数を含む値の型は二つの型がある。
float32よりもfloat64の方が精度が高い。

複素数

complex64 complex128
複素数を扱うための型もあり、これも二つの型がある。
complex64float32の型を実数、虚数で分けて持っている型であり、complex128float64の型を実数、虚数で分けて持っている型である。

バイトデータを扱う型

byte
バイトデータを扱うために作られた型であり、uint8の別名でもある。バイトデータを扱うための型なので、単純な数値の型と考えない方が良い。

Unicodeのコードポイントを扱う型

rune
Unicodeのコードポイントを扱うための型であり、int32の別名である。数値型としてよりは文字型と考えることが自然。

真偽値

bool
true,falseの二つの値を持つことができる。

文字列

string
文字をダブルクォート(")で括る事で文字列のリテラルとされる。シングルクォート(')で括った場合はrune型となる。改行を含む文字をグレイヴ・アクセント(`)で括ることで一つの文字列リテラルとして認識される。

定数

定数はconst hoge = 1のようにして定義する。Goでは定数を定義の際に型を決めなくても良い。また、複数の値を定義するときは以下のように書く。

const (
    foo = 1
    bar = 2
    baz = "go lang"
)

型変換

数値<--->数値

数値間の変換は型名(値)とすることで変換が可能である。多くの言語はint8の値をint64の値と計算すると、int64として自動的に計算するが、Goでは自動的に計算されず、明示的に型変換する必要がある。例:(int64(num8))

数値 <---> 文字列

数値、文字列の相互変換はパッケージを使わずに行うことは難しく、多くの場合strconvというパッケージを呼び出して変換を行う。パッケージの呼び出し法はのちに紹介するが、呼び出し後以下のように変換する。

文字列, 変数 = strconv.Atoi(数値)
数値, 変数 = strconv.Itoa(文字列)

一行目は数値から文字列の変換、二行目は文字列から数値の変換を行っている。strconvがパッケージ名であり、AtoiItoaはメソッドである(AtoiはAscii to integer、ItoaはInteger to asciiの略)。それぞれに出てきた変数は変換に失敗したときのエラー情報が代入される。変換に成功したときは値がないとしてnilが代入される。

パッケージの宣言

パッケージの宣言はimportで行う。

import "strconv"
import (
    "database/sql"
    "io/ioutil"
    "os"
    "strconv"
    "strings"
)

一行目のように一個ずつ行うこともできるが、二行目以降のようにまとめて行うこともできる。

出力

フォーマット出力はパッケージによって実装されており、fmtというパッケージを紹介する。
fmt.Println(1)で出力し、改行する処理を行い、Printfでフォーマット出力を行う。

演算子

足し算

文字列の結合、数値の足し算は+で行う。

引き算

数値の引き算は-で行う。

掛け算

数値の掛け算は*で行う。

割り算

数値の割り算は/で行う。

余り

数値の剰余は%で求める。

代入演算子

X = X 演算子 Yとしたいとき代わりにX 演算子= Yとすることができる(x = x + yだったらx += y)。

++, --

x += 1としたい時にx++とすることができる(インクリメント・ステートメント)。同様にx--とすることもできる(デクリメント・ステートメント)。この二つは式ではなく、文として扱われるため他の言語と同様の方法で使えない場合もあることに注意(式に埋め込むなど)。

変数の宣言

変数の宣言は以下のように行う。

var hoge int
var foo, bar, baz string

二行目のように同じ型であれば一度に複数の変数を宣言することができる。
宣言した変数に値を代入するには以下のように行う。

hoge = 3
foo, bar, baz = "go", "lang", "base"

変数の数と値の数が一致すれば二行目のように代入できる。

変数の宣言と代入

変数を定義と代入は同時に行うことができ、以下のように書く。

var hoge int = 1
var fuga = 2

代入される値から型が推論されるため、二行目のように型宣言を省略することも可能である。これはもっと単純に書くことができて

fuga := 2

のように書くことができる(これが一般的?)。

配列

配列の長さは初めに宣言し、基本的に不変である。また、配列内の型は全て同じ型でなければいけない。

宣言

var arr [3]intのように宣言する。arrは宣言する配列の名前、3は配列の大きさ、intは配列内の値の型である。配列内の具体的な値が決まっている場合はvar arr [3]int{1, 2, 3}のように宣言できる(配列の長さと同数の値が必要)。配列の中身はarr[2]で取得することができる(インデックスは0から)。配列の代入はarr[2] = 4でできる。配列も同様にarr := [3]int{1, 2, 3}のようにvarを省略して宣言することができる。

スライス

配列と基本的には同じだが、長さに制限がない柔軟なもの。

宣言

slice := []int{1, 2, 3}のように宣言する。配列と宣言はほとんど変わらず、要素数を空にする事で宣言できる。また、スライスは配列から切り出すといった形で宣言することも可能で、slice := arr[0:2]のようにする事でarrの0~1番目の要素を持ったスライスを生成することができる。[:]で全ての要素、[:0]で要素を一体持たないスライスを作成することができる。配列を切り出して作成したスライスは元の配列を参照しているだけであるので、配列の要素を変更するとスライスの要素も変更される。スライスは長さと容量という値がある。長さはスライスのサイズ、容量は参照した配列のサイズである。

追加

追加はslice = append(slice, 4)ですることができる。配列から配列の要素数より小さい長さのスライスを取得したとき、そのスライスに値を追加すると配列の値も変わってしまうことに注意する。スライスに配列を丸ごと追加したいときはslice = append(slice, arr...)とする事で全て追加することができる。

マップ

配列やスライスと異なり、インデックスではなくキーを使って管理する。マップ内の値の順番は適当であることに注意する。

宣言

var map[string]intのように宣言することができる。あらかじめ追加したい値があるのであれば以下のように宣言する。

map := map[string]int{
    "apple": 200,
    "banana": 150,
}

追加

追加するときはmap["peach"] = 100とする事で"peach"キー追加する。すでに存在する場合は更新される。

削除

削除するときはdelete(map, "apple")とする事で"apple"がキーの要素を削除する。キーがなくてもエラーは起きない。マップは自動更新される。

#make
初期化された配列やスライス、マップを作成する関数としてmakeというものがある(チャンネルも作成できる)。
配列はこのように作成する。

make([5]int, 5)

スライスはこのように作成する。

make([]int, 5, 5)

マップはこのように作成する。

make(map[string]int, 5)

makeは第一引数に型、第二引数に長さ、第三引数に容量を指定する(第三は省略可能)。

制御構文

if文

if文は以下のように書く。

if 条件 {
    条件が真の時の処理
}

また、条件の前に実行される一文を用意することができる。

if  ; 条件 {
    条件が真の時の処理
}

文が先に実行されてから条件に従って動作する。

if-else文

if-else文は以下のように書く。

if 条件 {
    条件が真の時の処理
} else {
    条件が偽の時の処理
}

比較演算子

比較演算子の一覧を以下に示す。

条件式
A == B AとBは等しい
A != B AとBは等しくない
A < B AはBより小さい
A <= B AはB以下
A > B AはBより大きい
A >= B AはB以上

switch文

switch文は以下のように書く。

switch 条件{
    case 評価1:
        処理1
    case 評価2:
        処理2
         .
         .
         .
    default:
        全て当てはまらなかった時の処理
}

Switchは条件値と評価値が一致したcaseの処理を行いどれとも一致しなかったらdefaultの処理を行う。
if文同様に以下のように条件の前に実行される一文を用意することができる。

switch ; 条件 {・・・}

switch文はfallthroughを使うことで評価値が一致した後もcaseの評価を進めることができる。

switch 条件{
    case 評価1:
        処理1
    case 評価2:
        処理2
        fallthrough
    case 評価3:
        処理3
         .
         .
         .
    default:
        全て当てはまらなかった時の処理
}

上記の例だと、条件の結果が評価2と等しかった場合、処理2を行った後また評価3から評価を進む(fallthroughがなければ処理2を行いスコープの外へ行く)。

for文

for文の基本的な形は以下のようなものである(条件は省略可)。

for 条件 {
    処理
}

このコードは条件が破れるまで処理を繰り返している(他言語のwhileに近い)。また、条件の前に最初だけ実行される一文と条件の後に1回の処理ごとに実行される一文(処理が全て終わった後に実行)を書くことができる。これを利用すると1~nの和を求めるコードを以下のように書くことができる。

sum := 0
for i := 1; i<=n; i++ {
    sum += i
}

配列の値を一つずつ取り出してループしたいときは以下のように書く(スライス、マップも同様)。

for i, ele := range arr {
    処理
}

iがインデックス値、eleが配列の値である(マップの場合iはキーとなる)。

continuebreak

処理中に記述できるものとしてcontinuebreakがある。continueはそこで繰り返し処理を抜けて次の繰り返し処理に進ませる。breakはそこで繰り返し処理を終了させる。

goto文

ラベル

プログラム中の目印としてラベルというものがある。これはラベル名:のようにラベル名の後ろに:をつけることで記述することができる。

goto

gotoはgoto ラベル名のように書くことができる文で、ラベル名にラベル付けされたコードまでジャンプする(エラー文で多く利用される)。

関数

main関数は特別な関数で実行したときに呼び出される。

定義

関数の定義は以下のように行う。

func func1(arg1 string, arg2 int) string {
    str := arg1 + strconv.Itoa(arg2)
    return str
}

この関数はfunc1という関数名で、arg1とarg2が引数でそれぞれstring型とint型のみ引数として渡される。返り値はstringで引数の括弧の後に書く。また、返り値も引数同様複数の値を指定することが可能である。そのときは返り値は括弧でまとめる。

返り値

返り値はあらかじめ名前をつけておくことも可能で先程の例の場合以下のように書くことができる。

func func1(arg1 string, arg2 int) (str string) {
    str = arg1 + strconv.Itoa(arg2)
    return
}

このケースではこの書き方の威力を感じられないが、実践的なケースではわかりやすい変数名で命名するので初めの一行を読むだけでおおよその処理がわかるというメリットがある。

引数(可変長引数)

引数は任意の個数追加できるように設定することができる。

func main() {
   arr1 := [3]string{"1", "2", "3"}
   arr2 := func2(arr1, "4", "5", "6") // [1 2 3 4 5 6]
}
func func2(arg1 []string, arg2 ...string) (arr []string) {
    arr = append(arg1, arg2...)
    return
}

上の例ではarr1を一つ目の引数として渡しており、それ以降に渡した引数は全てarg2に配列のように渡された関数となっている。このように変数 ...型とする事で幾つでも引数を追加できるようになる。このような引数は最後に用意する必要がある。

無名関数

関数は値として扱われ、関数を変数としてに扱うことができる。

f := func(arg1 int, arg2 int) int {
    return arg1 + arg2
}
num1 := 1
num2 := 2
sum := f(num1 + num2)

関数は数値として扱われるので、関数の引数に関数を引き渡すことも可能である。このような場合書いた関数をその場で実行することが必要になるケースもあるが、その場合は関数の定義の後に括弧をすれば良い。

ポインタ

ポインタはメモリ内の変数のアドレスを扱うための変数である。C/C++でもポインタを扱うことができるがこれらの言語と異なり、Goではポインタの演算をすることはできない。ポインタの変数の定義はvar ptr *intのように定義することができる(この例はint型のポインタ、ptrは変数)。変数のアドレスを手に入れるには変数の前にアドレス演算子&をつけることで値が得られる。例:ptr := &1また、ポインタから変数に変換するには*をポインタ変数の前につける。
ポインタが有効な例を一つ紹介する。

func main() {
    arr := []int{1, 2, 3} // [1 2 3]
    arr = initial(arr) // [0 0 0]
}

func initial(arr []int) []int{
    for i := 0; i < len(arr); i++ {
        arr[i] = 0
    }
    return arr
}

このコードはポインタを使うと以下のように簡潔にかける。

func main() {
    arr := []int{1, 2, 3} // [1 2 3]
    initial(&arr) // [0 0 0]
}

func initial(arr *[]int) {
    for i := 0; i < len(*arr); i++ {
        (*arr)[i] = 0
    }
}

これは関数initialに配列そのものではなく、配列のポインタを渡していることによって関数内で配列を変更すると、main内の配列も変更される(*arr[0]でなく(*arr)[0]としたのは、*よりも[]が優先されることによりエラーが起きるから)。普通に配列を渡すと、initialに渡った配列のアドレスとmainの配列のアドレスが異なるので関数内で配列の内容を変更してもmain内の配列は変更されない。

構造体

構造体は特定の用途に必要のなものを一つにまとめるためのもので、複数の型の値や処理をまとめたものである。

宣言

構造体の定義は以下のように行う。

var person struct {
    Name string
    Age int
}

これは変数名をpersonとした構造体で、この構造体はstring型の変数Nameとint型の変数Ageを含んでいる。以下のようにすることで構造体内の値を扱うことが出来る。

// 代入
person.Name = "Bob"
person.Age = 21
// 取得
name := person.Name
age := person.Age

前述した定義方法では構造体を値として定義しているので、構造が同じ別の変数を作るときは再度同様の定義をする必要がある。そこで以下のように定義することで構造体を型として定義し、利用することができる。

// Person is structure.
type Person struct {
    Name string
    Age int
}

func main() {
    bob := Person{"Bob", 21}
    catherine := Person{"Catherine", 22}
}

このコードを見るとわかるようにvarの部分をtypeに変えることによって構造体の型を定義できる。外部から利用可能な型や関数を定義する場合命名は大文字で始める必要があり、例えば// 構造体名 is structure.のようにコメントを書かなければいけない。余談だが、このような型の定義は構造体のみならず一般的な型で行うことが出来る。例:type prime int素数型のようなものを作れたりする。
構造体は引数に渡す場合、ポインタで扱うことが推奨されている。なぜなら構造体は性質上データが巨大であることが多いので関数に渡すたびにコピーが作られるからである。先程紹介したポインタが有効な例と同様の理由からもポインタで扱ったほうが良い。
先程紹介した例では構造体を定義と同時に代入していたが、newを使うことによって空の構造体を定義することができる。

// Person is structure.
type Person struct {
    Name string
    Age int
}

func main() {
    bob := new(Person)
    bob.Name = "Bob"
    bob.Age = 21
}

メソッド

構造体に処理(メソッド)を追加するには構造体を定義の内部ではなく、以下のように別途関数を用意する。

// Person is structure.
type Person struct {
    Name string
    Age int
}

// Older is getting age.
type (per *Person) Older() {
    per.Age += 1
}

func main() {
    bob := Person{"Bob", 21}
    bob.Older // Bob.Age is 22
}

typeの後に割り当てる構造体の型を記述することによってメソッドを用意することができる。このケースでは割り当てる型ではなく、ポインタを記述している。割り当てる型はポインタでも良く、その時々に適切な方を記述すれば良い。また、便利なことに引数がポインタであっても(*per).Ageとする必要がなくper.Ageのように構造体のポインタから直接構造体の値を取り出すことができる(メソッドも同様)。

インターフェイス

構造体に定義した名前のメソッドが必ず存在するようにする仕組みがインターフェイスである。

宣言

インターフェイスは以下のように作成する。

// Human is interface.
type Human interface {
    Older()
}

これはHumanというインターフェイスを作成しており、このインターフェイスを実装した構造体は必ずOlderメソッドを持つ必要がある。構造体はインターフェイス内のメソッドと引数や返り値も完璧に同じメソッドを実装する必要がある。一般的な形は以下のように作成する。

type 名前 interface {
    メソッド名(引数 ) (返り値 返り値の型)
}

実装

インターフェイスを作成したのちに構造体に実装するには以下のように行う。

// Human is interface.
type Human interface {
    Init(name, age)
}

// Employee is structure.
type Employee struct {
    Name string
    Age int
}

// Init is init method.
type (em *Employee) Init(name string, age int) {
    em.Name = name
    em.Age = age
}

func main() {
    var bob Human = new(Employee)
    bob.Init("Bob", 21)
}

インターフェイスの実装はvar bob Human = new(Employee)で行っている。右辺でnew関数を使っていることがポイントでnew関数はそれ自体特定の型の値を作らず、代入する変数の型に合わせて決まるのでbobはHuman型の値として扱われる。これによってEmployeeをHuman型のように扱うことができている。そのため、右辺がEmployee{}などではエラーが起きる(変数と代入された値が異なると判断される)。変数はインターフェイスの型であるので代入した構造体固有のメソッドを実行をすることはできない。
異なる複数の構造体にインターフェイスを実装するには以下のように行う。

// Human is interface.
type Human interface {
    Init(name, age)
}

// Teacher is structure.
type Teacher struct {
    Name string
    Age int
}

// Init is init method.
type (te *Teacher) Init(name string, age int) {
    te.Name = name
    te.Age = age
}

// Student is structure.
type Student struct {
    Name string
    Age int
}

// Init is init method.
type (st *Student) Init(name string, age int) {
    st.Name = name
    st.Age = age
}

func main() {
    var bob Human = new(Teacher)
    var catherine Human = new(Student)
    bob.Init("Bob", 21)
    catherine.Init("Catherine", 12)
}

空のインターフェイス

当然だが、空のインターフェイスはtype General interface{}のような形で実装できる。このようにインターフェイスを作成することにより、どのような値でも代入できる変数を作成することができる。

var g general
g = 1
g = 1.23e4
g = "abc"
g = false

このように実装した値を具体的な型に落とし込みたい時は以下のように行う。

var g General
g = 1
num, ok := g.(int)

上のように変数.(型名)で行うことができる。この例であればnumにint型の数値が代入され、問題なく落とし込めればoktrue落とし込めなければfalseが代入される。型の変換ができるわけではないことに注意する。
#並列処理
Goルーチンを使用することにより同時にいくつものスレッドを同時に行うようにすることができる。

基礎

Goルーチンはgo 関数()で呼び出すことができる。わかりやすい例を以下に示す。

func main(){
    go func(){
        for i := 1; i <= 5; i++ {
            fmt.Printf("<%s %d>", "h", i)
            time.Sleep(10 * time.Millisecond) // timeパッケージ Sleepは引数だけ処理を停止する
        }
    }()
    for i := 1; i <= 5; i++ {
        fmt.Printf("<%s %d>", "m", i)
        time.Sleep(20 * time.Millisecond) // timeパッケージ Sleepは引数だけ処理を停止する
    }
}

このコードを実行すると<h 1><m 1><h 2><m 2><h 3><h 4><h 5><m 3><m 4><m 5>%のように出力される(環境により多少の異なる)。hello関数はメインスレッドの処理停止時間の半分の停止時間であるのでメインスレッドの出力が行われる間に凡そ二度出力を行う。

共有メモリ

スレッドを並行して行うとき、お互いに値をやりとりする方法の一つとして共有メモリがある。共有メモリは機能というよりはスコープの内ではスコープの外の変数を扱うことが出来るという性質を利用したものである。実際の例を以下に示す。

func main() {
    msg := 0
    go func(){
        for i := 1; i <= 3; i++ {
            msg += i
            fmt.Println("hello", msg)
            time.Sleep(20 * time.Millisecond)
        }
    }()

    for i := 1; i <= 3; i++ {
        msg += i
        fmt.Println("bye", msg)
        time.Sleep(10 * time.Millisecond)
    }
}

このコードを実行すると

bye 1
hello 2
bye 4
bye 7
hello 9

と出力される。共有メモリとはmsgのことを指し、二つの処理で加算されている。hello関数の出力が一つ足りないのはメインスレッドの実行が全て終えた時点で終了してしまうからである。

排他的処理

共有スレッドを利用した時、あるスレッドで値を読み込んでいるときに他のスレッドで値を書き換える可能性があるという問題がある。これを解決するために他のスレッドから一時的にアクセスできないようにすることができる。これはsyncパッケージのMutexという構造体により実装されている。アクセスできない(ロック)ようにするにはMutex構造体.Lock()とし、アクセス可能(ロック解除)にするときはMutex構造体.Unlock()とする。具体的な例を以下に示す。

// MsgMemory is structure.
type MsgMemory struct {
	msg string
	mux sync.Mutex
}

func main() {
	m := MsgMemory{msg: "Start"}
	go func() {
		m.mux.Lock()
		for i := 0; i < 3; i++ {
			m.msg += "first: " + strconv.Itoa(i)
			fmt.Println(m.msg)
			time.Sleep(10 * time.Millisecond)
		}
		m.mux.Unlock()
	}()
	go func() {
		m.mux.Lock()
		for i := 0; i < 3; i++ {
			m.msg += "second: " + strconv.Itoa(i)
			fmt.Println(m.msg)
			time.Sleep(10 * time.Millisecond)
		}
		m.mux.Unlock()
	}()
	time.Sleep(2 * time.Second)
}

メインスレッドが終わると他のスレッドも終了するので、最後にtime.Sleep(2 * time.Second)と書いた。これを実行すると以下のような結果が得られる。

first: 0
first: 0first: 1
first: 0first: 1first: 2
first: 0first: 1first: 2second: 0
first: 0first: 1first: 2second: 0second: 1
first: 0first: 1first: 2second: 0second: 1second: 2

結果からわかるように初めに呼び出した関数がmsgをロックしたため、二つ目の関数は解除されるまで処理は開始されない(できない)。このような例では単に実質的にgoルーチンとして呼び出しをやめれば良いだけだが、ある処理中だけはアクセスをやめて欲しい時などではこの機能が使える。

チャンネル

共有メモリの他にスレッド間でお互いにやりとりする方法としてチャンネルというものがある。お互いにやりとりする方法と書いたが、チャンネルは一方向のやり取りしかできず、双方向のやりとりは複数のチャンネルを利用する必要がある。共有メモリと異なりチャンネルはGoに搭載された機能であり、変数 := make (chan 型)のように作成する。値を取り出すには変数 := <-チャンネルとする。追加するときはチャンネル <- 値とする。チャンネルは複数の値を保管することが可能である(make関数の第二引数で数を指定できる)。
チャンネルを使った例として以下のようなものがある。

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

func main() {
    c := make(chan int)
    go total(10, c)
    go total(50, c)
    go total(100, c)
    fmt.Println(<-c, <-c, <-c)
}

この例の出力は100 10 50である。チャンネルから値を取り出す時、チャンネルに値が入るまでその処理はストップし、それ以降の処理は行わないためこのような結果が得られる。また、チャンネルから値を取り出す順番はチャンネルに値が入った順番である(FIFO)。この例ではそれぞれの処理時間に差がほとんどないため、出力される順番は不確定である。この例ではわかりにくいが、メインスレッドから他スレッドにチャンネルを送信するとき、チャンネルへの代入は他スレッドを呼び出した後でなければいけない。つまり、チャンネルは送受信する双方向に準備できてないと値のやりとりはできない。

select

複数のスレッドを扱う場合、チャンネルも複数作ることが多い。こうした場合一つのスレッドで複数のチャンネルを受信することがあり、これまで書いた内容で実装することはとても難しい(そのまま羅列すると先頭のチャンネルの処理待ちで他のチャンネルが受信待ちの状態にできないから)。これを解決するのがselectだ。selectはswitch文にとても似た形をしており、一般的には以下のように書くことができる。

select {
case :
    処理
case :
    処理
     
     
     
default:
    全て当てはまらない時の処理
}

文には基本的には<-チャンネルという形で描かれる。実際にチャンネルごとに処理している様子を実装すると以下のようになる。

func main() {

    c1 := make(chan int)
    c2 := make(chan int)

    go func() {
        time.Sleep(20 * time.Millisecond)
        c1 <- 1
    }()
    go func() {
        time.Sleep(10 * time.Millisecond)
        c2 <- 2
    }()

    for i := 0; i < 2; i++ {
        select {
        case num1 := <-c1:
            fmt.Println(num1)
        case num2 := <-c2:
            fmt.Println(num2)
        }
    }
}

このコードでは二つ目の関数の方が先に処理停止時間(time.Sleep)が短いので2, 1の順に出力される。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0