Go言語プログラミング入門|基本文法・構文総まとめ
Go言語に興味を持った人に向けて、プログラミングに必要となる文法や基本的な概念を網羅した記事を作成しました。他のプログラミング言語を学習したことがある方であれば、この記事を読むことでGo言語の全体像を理解できると思います。この記事を通じて、Go言語の基本を理解できます。
はじめに
Go言語は「Go言語」や「Golang」と表記されるGoogleが開発したプログラミング言語です。この記事は、Go言語の基本知識を一通り学べるように構成しています。
Go言語に興味を持った、既に他のプログラミグ言語を使っている方が、Go言語の文法や使い方を理解することができる内容を目指しています。
なお、Go言語はパッケージ管理やテストコードなどの部分にも特徴があるのですがその手の内容は解説していません。そちらについては実際に開発に使い出してから徐々に理解すれば良いと考えています。
この記事を通じて、Go言語の全体像をつかみ、さらに興味を持っていただければ幸いです。
私は、Go言語の可読性の高さが結構気に入っています。他の言語と比較して圧倒的に「他人が書いたプログラムが読みやすい」です。これがGo言語の利点だと思っています。
Hello World〜基本
まずはHello, world!
Go言語のプログラムの拡張子は.go
です。以下は、定番のHello, world!
を表示するプログラムになります。
直接実行する場合は、以下のようにします
go run main.go
また、コンパイルして実行する場合は、以下のようにします。
go build main.go
./main
コンパイルという言葉からわかるように、Go言語はコンパイル時に型が変数の方が決まる静的型付け言語です。このため、Pythonなどのインタプリタ言語と比較して高速です。
C++と比較した場合、行末尾の;
(セミコロン)が必要ない点もポイントです。
package main
import "fmt"
// メイン関数
func main() {
fmt.Println("Hello, world!")
}
Visual Studio CodeのGolangプラグインの機能が充実していますので、VSCodeを使ってプログラミングすることをお勧めします。importの部分の自動挿入など、かなりのことをやってくれます。
まずは、Hello, world!で使った要素を説明していきます。
パッケージ(package)
パッケージはコードをまとめる基本単位です。
プログラムでは必ずmainパッケージが存在する必要があり、mainパッケージ内にmain()
関数が定義されている必要があります。
Hello, World!では、このファイルがmainパッケージになるので、
package main
と先頭で定義しています。
ライブラリなどの場合は、package hogelib
のようにパッケージ名をしています
インポート(import)
Go言語ではimport
文によりパッケージをインポートできます。
Hello, world!では、fmt.Println()
を利用するためにfmt
パッケージをインポートしています。
インポートは以下のようにそれぞれのパッケージを記述することも可能です。
import "fmt"
import "os"
また、以下のように()
で括ることで、まとめて記述することも可能です。
import (
"fmt"
"os"
)
関数(func)
Go言語では、関数の定義はfunc
で行います。main()
関数は、引数も戻り値もないため、
func main() {...}
という定義になります。プログラムは、このmain
関数から実行されることになります。
他のプログラミング言語(C言語など)を知っていれば、ここは馴染みがあると思います。
fmt.Print, fmt.Println, fmt.Printf
パッケージ内の関数を利用する場合、パッケージ名.関数名
という形で呼び出します。fmt.Print
は、fmt
パッケージのPrint
関数を呼び出すという意味になります。
Print文には、Print()
と最後に改行の入るPrintln()
と、フォーマットが指定できるPrintf()
があります。
Hello, world!では、fmt.Println("Hello, world!")
と、”Hello, world!”を表示して改行を行なっています。
なお、Printf
で利用できるフォーマットには以下のようなものがあります(ここに挙げたものは代表的なものだけです)
%v, %#v, %t(真理値), %d(整数), %s(文字列), %c(文字), %f(少数), %F(少数), %e(浮動小数点e表記), %E(浮動小数点e表記), %g(%f/%e自動選択), %b(2進数), %o(8進数), %x(16新数), %U(Unicode), %p(ポインタ), %T(型を表示)
例えば、以下のように利用します。
fmt.Printf("%d %s\n", num, str)
C++とは違い、Pythonなどと同じように、配列の内容を表示したり、構造体の内容を表示させたりすることができます。
コメント
Go言語のコメントは//
で始まる行または、/*
と*/
で囲んだ範囲になります。
// コメント
/* コメント行
コメント行 */
予約済みキーワード(予約語)
Go言語の予約語は以下になります
break, case, chan, const, continue, default, defer, else, fallthrough, for, func, go, goto, if, import, interface, map, package, range, return, select, struct, switch, type, var
予約語とは、プログラミング言語の仕様に定められており、識別名として利用できない文字列のことです。
リテラル
リテラルとは、プログラムのソースコードにおいて使用される、数値や文字列を直接に記述した定数のことです。Go言語では、以下のようなものがリテラルになります
nil
:値が存在しないことを示す特別な値true/false
:真偽値
1234:整数
O123:8進数
0o123:8進数
0x123:16進数
0b0101:2進数
1.234:少数
1.23e4:浮動小数点
1.23i:虚数
“1234”:文字列
‘1’:文字(rune)
エスケープシーケンス
Go言語のエスケープシーケンスは以下のようなものがあります。
\a 警報音
\b バックスペース
\n 復帰改行
\r キャリッジリターン
\f 改ページ
\t タブ
\v 垂直タブ
\\ 文字としての\
\’ シングルクォーテーション(‘)
\” ダブルクォーテーション(“)
\uhhhh Unicode ※hは16進表記
\xhh 16進数の文字コードを持つ文字
変数(var)と定数(const)
var
変数は以下の形式で定義します。
var 変数名 型名
var a int // 初期値がない場合は0(文字列の場合は空文字列)
var b int = 100 // 初期値を設定
var c = 100 // 型を省略すると初期値から推定される
また、以下のようにまとめて定義することも可能です。
var (
a int // 初期値がない場合は0(文字列の場合は空文字列)
b int = 100 // 初期値を設定
c = 100 // 型を省略すると初期値から推定される
)
また、初期値を与える場合は、var
を書かずに変数を定義することも可能です。
この場合は:=
を用います。
a := 0
b := 100
c, d := 100, "abc"
const
定数はconst
で定義します。
const
もvar
と同様にconst (...)
でくくることでまとめて定義することも可能です。
const x = 100
const f = 1.0
型
Go言語の型は以下のようなものがあります。
bool
int8, int16, int32, int64
uint8, uint16, uint32, uint64
float32, float64
complex64, complex128,
byte, rune
uint, int
uintptr, string
型変換について
型名()
で型変換を行うことが可能です。
a := (int)100
var b uint
b = (int)a
Go言語は型に対してのチェックが厳しいです。C++などではintとunsigned intなどは、型変換せずに代入可能ですがGo言語ではエラーになります。
演算子
算術演算子
演算子 | 例 | |
+ | a+b | 加算 |
– | a-b | 減算 |
* | a*b | 乗算 |
/ | a/b | 除算 |
% | a%b | 剰余 |
— | a– | デクリメント(a=a-1) |
++ | a++ | インクリメント(a=a+1) |
& | a&b | ビット毎の論理積(AND) |
| | a|b | ビット毎の論理和(OR) |
^ | a^b | ビット毎の排他的論理和(XOR) |
&^ | a&^b | ビット毎のAND NOT |
<< | a<<b | 左シフト |
>> | a>>b | 右シフト |
比較演算子
演算子 | 例 | |
== | a==b | 等しい |
!= | a!=b | 等しくない |
< | a<b | 左辺が右辺より小さい |
> | a>b | 左辺が右辺より大きい |
<= | a<=b | 左辺が右辺以下 |
>= | a>=b | 左辺が右辺以上 |
論理演算子
Go言語では、論理演算はbool型に対して行います
演算子 | 例 | |
&& | a&&b | 論理積(AND) |
|| | a||b | 論理和(OR) |
! | !a | 否定(NOT) |
代入演算子
演算子 | 例 | |
= | a=b | 左辺に右辺を代入 |
:= | a := 10 | var a = 10と同じ |
+=, -=, *=, /=, %= | a+=bなど | a = a+bなどと同じ |
&=, |=, ^=, &^= | a|=bなど | a = a | bなどと同じ |
<<=, >>= | a <<= b | a = a<<bなどと同じ |
アドレス演算子
演算子 | 例 | |
& | &a | 変数のポインタ |
* | *a | ポインタ型の定義 |
レシーバー
チャネル用の演算子です。主にgoroutineで使います。
演算子 | 例 | |
<- | ch <- 1 | チャネル用 |
演算子の優先順
演算子の優先順は以下の通り
優先順位 | |
高 | *, /, %, <<, >>, &, &^ |
+, -, |, ^ | |
==, !=, <, >, <=, >= | |
&& | |
低 | || |
演算子は、他の言語も似たような感じなので説明は割愛します。
配列、スライス、マップ
配列(array)
個数が決まっていて変更不可のものを配列と呼びます。配列の定義は以下のようになります。
[]
の中に配列の要素数を指定する必要があります。また、配列のインデックスは0始まりです。
なお、初期化とペアで行う場合、個数を...
で省略することも可能です。
a := [4]int{}
b := [2]int{2,3}
var c [2]string
c[0] = "aaa"
c[1] = "bbb"
x := [...]float32{1,2,3,4.4}
スライス(slice)
配列に似ていますが、個数が変更可能なものがスライスです。スライスを定義する場合は[]
の中に個数を入れません。
宣言は以下のような方法があります。
var a []int // 空のスライス
b := []int{} // 空のスライス
c := []int{1,2,3} // 初期個数が3で値が1,2,3のスライス
c := make([]int, 10) // 初期個数10個を確保
d := make([]int, 0, 10) // 初期個数は0個で、10個分のメモリをあらかじめ確保
スライスに値を追加する場合はappend
を使います。同じ型のスライスを後ろに追加したい場合には...b
のように変数名に...
をつけます
a := []int{}
a = append(a, 1) // 1を末尾に追加
a = append(a, 2) // 2を末尾に追加
b := []int{1,2,3}
a = append(a, ...b) // スライスを後ろに追加する場合は...をつける
配列とスライスの要素へのアクセス
スライスや配列の要素へのアクセスは以下のようになります(以下の説明は先頭は0番目からと考えてください)
- スライスaのn番目の要素にアクセスしたい場合はa[n]
- スライスaのi番目からj-1番目の要素にアクセスしたい場合はa[i:j]
- スライスaのi番目から末尾までの要素アクセスしたい場合はa[i:]
- スライスaの先頭からi-1番目までの要素アクセスしたい場合はa[:i]
a := []int{0, 1, 2, 3, 4, 5}
fmt.Println(a[1:3])
// [1 2]
fmt.Println(a[:3])
// [0 1 2]
fmt.Println(a[3:])
// [3 4 5]
マップ(map)
辞書(map)は、キーを用いてアクセスすることができるデータ構造です。
宣言は以下のように行います。
以下のようにキーは文字列でもOKです。また、b[0]
とb[100]
を定義した場合は、キー0と100だけ記録されます。
a := map[string]int { "x" : 100, "y" : 10 }
b := make(map[int]bool)
b[0] = true
b[100] = true
fmt.Println(a, b)
// map[x:100 y:10] map[0:true 100:true]
アクセスする場合は以下のようにします。
mapでは定義されているキーに関しては値を、それ以外の場合は初期値を返します。
なお、定義されているかどうかを確認したい場合は、_, ok := ...
のようにするとokにtrue/falseでキーが存在するかどうかが返されます。
a := make(map[string]int)
a["x"] = 10
a["y"] = 100
fmt.Println(a["x"])
// 10
fmt.Println(a["z"])
//0
v1, ok1 := a["x"]
fmt.Println(v1, ok1)
// 10 true
v2, ok2 := a["z"]
fmt.Println(v2, ok2)
// 0 false
構文
条件判定(if文)
if 条件 {
処理
}
if 条件 {
処理1
} else {
処理2
}
if 条件 {
処理1
} else if 条件 {
処理2
}
if 条件 {
処理1
} else if 条件 {
処理2
} else {
処理3
}
Go言語では条件は真理値である必要があります。C++などでは整数が0かどうかで条件分岐する場合はif (整数値) {...}
のように記述できますが、Goではif 整数値==0 {...}
と書く必要があります。
Go言語のif文の注意事項は以下になります
- 処理が1行でも必ず
{}
する必要があります - 条件は
()
で囲む必要はありません
if a > b {
fmt.Println("a > b")
} else if a < b {
fmt.Println("a < b")
} else {
fmt.Println("a == b")
}
Go言語のif文では以下のように条件の前に式を入れることが可能です。
例えば以下のように記述することが可能です。
m := map[int]bool{1: true}
if _, ok := m[1]; ok {
....
}
条件判定(switch文)
switch 式 {
case 条件1:
処理1
case 条件2:
処理2
:
default:
}
Go言語のif文の注意事項は以下になります
- C++言語などと異なり
break
は必要ありません。逆に次の条件の処理も実行したい場合はfallthrough
を書く必要がある - case文に条件文を入れることが可能です
switch stat {
case 0 :
fmt.Println("STOP")
case 1 :
fmt.Println("RUNNING...")
default :
fmt.Println("Unknown Status")
}
条件を書く例
switch {
case x > y :
fmt.Println("Greater")
case x < y :
fmt.Println("Less")
default :
fmt.Println("Equal")
}
繰り返し(for文)
for 開始処理; 条件; 毎回の処理 {
処理
}
開始処理、条件、毎回の処理は何も書かなくても良いです。以下のようにすると無限ループになります。
for {
}
次のように条件だけ書くとwhile
文と同じにります。
x := 12345
for x > 0 {
x >>= 2
}
一般的なfor文として利用する場合は以下になります。
for i := 0; i < 10; i++ {
fmt.Println(i)
}
for~rangeによるループ
for インデックス変数, 要素変数 := range 配列やスライス {
処理
}
range
を用いてループ処理を行うことも可能です。
a := []int{3,2,5,1,3}
for i, e := range a {
fmt.Println(i, e)
}
// 0 3
// 1 2
// 2 5
// 3 1
// 4 3
for~rangeについては、注意が必要です。
continue, break
ループを抜けたい場合はbreak
、途中でループの先頭に戻りたい場合はcontinue
を利用します。
for i := 0; i < 10; i++ {
if i == 3 {
continue
}
if i == 5 {
break
}
fmt.Println(i)
}
// 0
// 1
// 2 ※3はスキップ
// 4 ※5以上はbreakされて終了
ジャンプ(goto文)
goto ラベル
goto文を使うことで指定のラベルにジャンプすることが可能です。
条件で関数から抜けたり、2重ループから脱出したりするのに使うことが可能です。
if ok {
goto END
}
for i := 0; i < 100; i++ {
for j := 0; j < 100; j++ {
if i+j == 120 {
fmt.Println("i+j=120")
goto LOOP_END
}
}
}
LOOP_END:
fmt.Println("LOOP END")
END:
関数(func)
func 関数名(引数, ...) 戻り値 {
}
Go言語のif文の注意事項は以下になります
- 関数が複数の値を返す場合は、戻り値を
()
で括る必要があります。
func add(a, b int) int {
return a +b
}
func addSub(a, b int) (int, int) {
return a+b, a-b
}
Go言語では、グローバル変数や構造体、関数が大文字で始まるものはパッケージ外からアクセス可能になります。逆に小文字から始まるものはパッケージ外からアクセスすることができません。
結構ハマるので注意が必要です。
構造体(struct)
sturct {
}
structは以下のような形で利用することが可能です。
var a struct {
x int
y string
}
a.x = 1
a.y = "hello"
fmt.Println(a)
// {1, hello}
ただ、type
による型定義と合わせて以下のようにして利用するのが一般的です。
package main
import "fmt"
type S struct {
x int
y string
}
func main() {
var a S
a.x = 1
a.y = "hello"
fmt.Println(a)
}
ジェネリクス
ジェネリクスがあれば、特定の型に依存しないコードを書くことができる仕組みです。Go1.18から導入された機能になります。Go言語でジェネリクスを利用する場合は以下のように記述します。
[型名 型]
以下のAdd
関数は、整数型、浮動小数点型、文字列型を入力することが可能です。
func Add[T int | float64 | string](a, b T) T {
return a + b
}
ジェネリクスについては、以下の記事で詳しく解説しています。
高度な使い方
遅延実行(defer)
defer
で記述されたものは、関数から戻る直前まで実行が遅延します。
使い方は、ファイルをオープンした直後にCloseを書いておくなどになります。途中でif文でreturnしてもdeferで定義した処理は実行されるので、ファイルのClose忘れなどを防ぐことができます。
func readFile(fname string) ([]string, bool) {
fp, err := os.Open(fname)
if err != nil {
return nil, false
}
defer fp.Close()
ret := []string{}
//
// ファイル読み込み
//
return ret, true
}
以下のプログラムはdeferの実行例です。start
が表示されtend
が表示されます。
package main
import (
"fmt"
)
func main() {
defer func() {
fmt.Println("end")
}()
fmt.Println("start")
}
// start
// end
ポインタ
Go言語でポインタを利用する場合は、&
と*
を利用します。
変数のポインタを参照する場合は&
で、ポインタの中身を参照する場合は*
となります。
a := 1
var b *int
b = &a
*b = 2
fmt.Println(a, *b)
// 2 2
メソッド
Go言語はクラスなどが存在しない代わりに、型にメソッドを持たせることができます。
下記の例では、Countable
という型にAdd
とReset
という関数を定義しています。このように、型に関数を持たせることが可能です。
package main
import (
"fmt"
)
type Countable int
func (c *Countable) Reset(n int) {
*c = Countable(n)
}
func (c *Countable) Add() {
*c = *c + 1
}
func main() {
c := Countable(0)
for i := 0; i < 10; i++ {
c.Add()
}
fmt.Println(c)
// 10
c.Reset(0)
fmt.Println(c)
// 0
}
インタフェース
インタフェース(interface
)は、「同じメソッド(機能)を持つ複数の型を、ひとくくりにして扱うための仕組み」です。結構わかりにくいです。
以下、サンプルです。PrintOutという関数はNum, Strのどちらの変数も引数として持つことができます。
package main
import (
"fmt"
"strconv"
)
type Printable interface {
ToStr() string
}
func PrintOut(p Printable) {
fmt.Println(p.ToStr())
}
type Num struct {
d int
}
func (dat *Num) Set(idx int) {
dat.d = idx
}
func (dat Num) ToStr() string {
return strconv.Itoa(dat.d)
}
type Str struct {
d string
}
func (dat *Str) Set(s string) {
dat.d = s
}
func (dat Str) ToStr() string {
return dat.d
}
func main() {
v := Num{1}
var s Str
s.Set("hello")
fmt.Println(v, s)
PrintOut(v)
PrintOut(s)
}
個人的にはあまり利用しないのでうまく説明できないです。
interface{}型
interface{}型は、any
型のようなものです。以下のように定義することでどのような型も受け取ることが可能です。
以下では、PrintOut
関数はinterface{}
型で引数を受け取り、e.(type)
で型を調べ、e.(型名)
で型をキャストして表示しています。
このようにinterface{}型を使うことで、さまざまな型を受け取る関数などが作成できます。
package main
import (
"fmt"
)
func PrintOut(e interface{}) {
switch e.(type) {
case int:
fmt.Println("int =", e.(int))
case string:
fmt.Println("string =", e.(string))
default:
fmt.Println("Unsupported type.")
}
}
func main() {
PrintOut(10)
PrintOut("hello")
PrintOut(1.0)
}
// int = 10
// string = hello
// Unsupported type.
ゴルーチン(goroutine)
ゴルーチン(goroutine)Goの特徴的な機能の1つです。これはスレッドによる並列処理を実現するものです。
go func1()
と書くだけで、メインの処理は継続しながらfunc1()
が呼び出され実行されます。
呼び出した関数とのやりとりはチャネルを利用して行います。また、func1()
の終了を待ち合わせる場合にもチャネルが利用できます(sync.WaitGroup
を使う方法もあります)。
以下、goroutineを使った生産者・消費者問題のプログラムです。
生産者がn, 消費者が1つです。生産者は並列動作しているので、受け取る順番がバラバラです。
このように手軽に並列を記述できるのもGoの魅力です。
package main
import (
"fmt"
"math/rand"
"time"
)
type Pair struct {
idx, val int
}
func Producer(idx int, ch chan Pair) {
var r *rand.Rand
r = rand.New(rand.NewSource(time.Now().UnixNano()))
for i := 0; i < 10; i++ {
ch <- Pair{idx, i}
t := r.Intn(200)
time.Sleep(time.Duration(t) * time.Millisecond)
}
ch <- Pair{idx, -1}
}
func Comsumer(num int, ch chan Pair) {
tot := 0
for {
v := <-ch
if v.val == -1 {
tot++
if tot == num {
break
}
} else {
fmt.Printf("Producer %d: value %d\n", v.idx, v.val)
}
}
}
func main() {
ch := make(chan Pair, 10)
n := 3
for i := 0; i < n; i++ {
go Producer(i, ch)
}
Comsumer(n, ch)
}
goruoutneに関しては、これだけで結構な量になるので、ここでは紹介だけにしておきます。どこかで詳しく記事にしたいと考えています。
まとめ
ざっとGo言語について紹介しました。
いろいろなサイトで入門という形で紹介している内容ですが、改めてまとめることで自身の整理になりました。
とりあえず、「Go言語ってどんな言語?」と興味を持った方が、なんとなく理解できるように全体を浅く網羅したつもりです。他の言語をマスターしている人にとっては、この程度の情報で十分使い始められると思います。