はじめに
この記事はGoについて何も知らなかった時の自分へ向けた備忘録です。
まずはGoの環境構築を行い、最小限のGoプログラムの実行からエラー制御、構造体などについてをまとめています。
環境
- Ubuntu 22.04.2 LTS
- go version go1.20.5 linux/amd64
Go環境構築
Goのインストール
以下はUbuntu環境の場合の手順です。その他のプラットフォームをお使いの場合は、Download and install - The Go Programming Languageでインストール手順を確認し、All releases - The Go Programming Languageから必要なファイルをダウンロードしてください。
$ rm -rf /usr/local/go && tar -C /usr/local -xzf go1.20.5.linux-amd64.tar.gz
export PATH=$PATH:/usr/local/go/bin
$ source ~/.profile
$ go version
go version go1.20.5 linux/amd64
Vimで快適にGoを書くために
エディタとしてNeovimを、LSPにはcoc.nvimを採用しているため、Vim で Go を書く環境を整えたという記事を参考に環境構築を行いました。
わたしの環境ではcoc-go
のインストール時にgopls
もインストールされたため、個別でのインストールは不要でした。
Go基本構文
Goの基本的な構文を確認するために、Hello Worldと出力するプログラムを作成します。
$ mkdir hello
$ cd hello
$ go mod init example.com/hello
$ touch main.go
go mod init
コマンドを実行することで、依存関係を追跡するためのgo.mod
ファイルが作成されます。このファイルに依存関係を記述することで、自分のコードがほかのモジュールに含まれるパッケージをインポートすることができます。
以下はHello Worldを出力するmain.go
ファイルです。
// エントリポイントとなるパッケージです。
//
// メインパッケージは、mainというパッケージ名を持ち、
// 引数を取らず値を返さないmain関数を宣言する必要があります。
package main
import "fmt"
func main() {
/**
* fmt.Println("Heo Word")
* fmt.println("Hello World") pが小文字
*/
fmt.Println("Hello World")
}
上記で作成したプログラムは、go run
コマンドで実行することができます。go run
は、指定されたmain
パッケージをコンパイルして実行するコマンドです。通常はgo run .
のようにソースファイルを指定します。
$ go run .
Hello World
パッケージ名とインポート/エクスポート
Effective Go package-namesによると、パッケージ名は小文字の1語であり、アンダースコアや大文字小文字の混在は不要とされています。
fmt
パッケージ以外の標準ライブラリを例に挙げると、net
やtime
のように、小文字の1語となっています。
外部パッケージはimport
キーワードでインポートすることができます。上記のサンプルコードでは、I/Oを実装したfmt
パッケージをインポートし、fmt.Println
という関数で、コンソール出力を行っています。
パッケージは1行に1つ記述する方法以外に、()
で囲むことでグループ化し、まとめてインポートすることができます。また、エイリアスを付けることも可能です。
package main
import (
"fmt"
t "time"
)
func main() {
fmt.Println("Wait 1sec...")
t.Sleep(1 * t.Second)
fmt.Println("Hello World")
}
fmt
パッケージのPrintln
や、time
パッケージのSleep
のように、外部からインポートした関数はすべて大文字で始まることに注目してください。Goでは名前を大文字ではじめると外部へ公開されるようになります。
例えば、fmt
パッケージのprint.go
ファイルで定義されているnewPrinter
関数を呼び出そうとすると、外部へ公開されていない関数のため以下のようなエラーが発生します。
$ go run .
main.go:9:11: undefined: fmt.newPrinter
変数宣言
Goではvar
キーワードで変数を宣言することができます。例えばstr
というstring型の変数を宣言したい場合は、
var str string
という構文になります。
上記は変数宣言時に初期値を与えない構文であり、このように変数を宣言すると型に応じた「zero value」という値が与えられます。例として、intやfloatなどの数値型には0
が、bool型にはfalse
が、string型には””(空文字列)
が、ポインタ・スライス・マップなどにはnil
が与えられます。
変数宣言時に初期値を与えたい場合は:=
構文を活用します。例えば上記の変数に対して”string”という値を与えたい場合は、
str := "string"
とすることで初期値を与えることができます。
変数宣言と同時に値を代入する場合は、値から型を推測できるため型の情報は必要ありません。また、var
キーワードは不要です。
package main
import "fmt"
var (
str string = "string"
flag = true
number int
)
const Tax = 1.08
func main() {
fmt.Printf("str = %s, flag = %t, number = %d \n", str, flag, number) // str = string, flag = true, number = 0
numA := 100
numB := 200
fmt.Println(numA + numB)
fmt.Println(float64(numA) * Tax)
}
定数宣言
Goにおいて定数はconst
キーワードで宣言することができます。上記のサンプルコードでは、Tax
が定数です。
定数は再代入が不可能なため、変数と同じように再代入しようとするとエラーが発生します。
var str = "string"
const Tax = 1.08
func main() {
str = "striiing"
// Tax = 1.16 cannot assign to Tax (untyped float constant 1.08)
}
制御構文
If
Goにおいてのもっともシンプルなif文の例です。
count := 0
if count == 0 {
fmt.Println("countは0です")
}
条件を括弧で囲む必要はありませんが、中括弧は必須です。
またif(とswitch)は初期化文を受け付けるため、ローカル変数の設定に使うことができます。
if _, err := os.Open("invalid.file.txt"); err != nil {
fmt.Println(err)
}
Goではelse
句を使うこともできます。
if文で初期化した変数はその条件分岐と、それに続くすべての分岐で有効です。if文を抜けると使えなくなります。
if num := 10; num < 0 {
fmt.Println("numは小さい: ", num)
} else {
fmt.Println("numは大きい: ", num)
}
// fmt.Println(num) undefined: num
2023年7月現在、Goには3項演算子が存在しないため、簡単な条件分岐だろうと完全なif文を記述する必要があります。
For
for文はGoにおいて唯一のループ処理です。
以下は、Goのもっともシンプルなfor文の例です。
i := 1
for i <= 5 {
fmt.Println(i)
i = i + 1
}
他の言語でよく見かける一般的な構文を使用することもできます。
for j := 1; j <= 5; j++ {
if j%2 == 0 {
continue
}
fmt.Println(j)
}
またrange
句を活用することで、配列、スライス、文字列、マップなどを簡単にループさせることができます。
nums := []int{1, 2, 3, 4, 5}
sum := 0
for _, num := range nums {
sum += num
}
fmt.Println(sum)
for _, str := range "string" {
fmt.Println(str)
}
maps := map[string]string{"name": "test", "password": "Pa$$w0rd"}
for key, value := range maps {
fmt.Printf("%s:%s \n", key, value)
}
Switch
以下はGoのもっともシンプルなswitch文です。
i := 1
switch i {
case 1:
fmt.Println("one")
case 2:
fmt.Println("two")
case 3:
fmt.Println("three")
}
switch文は、,
で区切ることで複数の条件文を追加することができます。また、case
句にbool型を返す式を渡すことでif/else文のように扱うこともできます。
switch {
case i <= 0, i > 1:
fmt.Println("0以下または1より大きい")
default:
fmt.Println("それ以外")
}
Goのswitch文は、インターフェース変数の動的な型を判定(Type switch)するためにも使用できます。
以下の例では引数tに同じ名前で異なる型の変数を再代入しています。例えばint型だと判定された場合は、その分岐内では変数tはint型になります。
typeSwitch := func(t interface{}) {
switch t := t.(type) {
case int:
fmt.Println("int: ", t)
case bool:
fmt.Println("bool: ", t)
case *int:
fmt.Println("pointer int: ", *t)
default:
fmt.Println("それ以外: ")
}
}
num := 10
typeSwitch(num) // int
typeSwitch(false) // bool
typeSwitch(&num) // pointer int
エラー制御
Errors
呼び出し元に対して何らかのエラー情報を返したいようなケースはよくあります。Goでは複数の値をリターンすることができる言語仕様を活用し、これを簡単に提供することができます。
例えばos
パッケージに含まれるos.Open
関数の実装を見てみると、通常の返り値である*File以外にもerror型を返します。
func Open(name string) (*File, error) {
return OpenFile(name, O_RDONLY, 0)
}
以下の例では2つ目の返り値がnilかどうかを判定し、nilではない場合はエラーが発生したとしてコンソール出力しています。nil値の場合はエラーなしを表しています。
if _, err := os.Open("invalid.file.txt"); err != nil {
fmt.Println(err)
}
$ go run .
open invalid.file.txt: no such file or directory
カスタムエラー
またGoではerror
という組み込みインターフェースを実装することで、独自のカスタムエラーを実装することが可能です。
type error interface {
Error() string
}
package main
import (
"fmt"
"unicode/utf8"
)
// カスタムエラー
type invalidValueError struct {
field string
message string
}
func (error *invalidValueError) Error() string {
return fmt.Sprintf("invalidValueError %v: %v", error.field, error.message)
}
type fileName struct {
value string
}
// fileName構造体の作成
// バリデーションに失敗した場合は、
// カスタムエラーを返す
func newFileName(value string) (*fileName, error) {
if len := utf8.RuneCountInString(value); len == 0 {
return nil, &invalidValueError{
"fileName",
"value is required",
}
}
return &fileName{
value,
}, nil
}
func main() {
if _, err := newFileName(""); err != nil {
fmt.Println(err)
}
}
$ go run .
invalidValueError fileName: value is required
上記の例では、invalidValueError
という不正な値を表すカスタムエラーを実装しています。
エラーハンドリング
Goでのエラーハンドリングのbest practicesを調べていく過程で、今goのエラーハンドリングを無難にしておく方法(2021.09現在)という興味深い記事を見つけました。Go2で新しいエラーハンドリングの方法が実装されるまではこちらを参考にしようかなと考えています。
上記の記事によると、fmt
パッケージのfmt.Errorf関数でエラーをどんどんラップしていくという方法を取っています。
package main
import (
"errors"
"fmt"
)
func usecase() error {
if err := service(); err != nil {
return fmt.Errorf("usecase error: %w", err)
}
return nil
}
func service() error {
if err := write(); err != nil {
return fmt.Errorf("service error: %w", err)
}
return nil
}
func write() error {
return errors.New("failed to write")
}
func getOriginalError(err error) error {
if wrappedError := errors.Unwrap(err); wrappedError != nil {
return getOriginalError(wrappedError)
}
return err
}
func main() {
if err := usecase(); err != nil {
fmt.Println(err)
fmt.Println(getOriginalError(err))
}
}
$ go run .
usecase error: service error: failed to write.
failed to write.
確かに開発者側としては、どこでエラーが発生したのか分かりやすい出力になりました。この方法ではユーザー側には不要な情報も含まれるので、errors.Unwrap
で大元のエラーを取得するのもありかなと思い、getOriginalError
という関数も実装してみました。
Panic
通常のエラーであれば追加の返り値としてエラーを返すだけで良いのですが、プログラムが続行できないような回復不可能なエラーが発生することもあります。
このような場面でプログラムを停止させるための組み込み関数panic
が用意されています。
panic("paniiiic!")
$ go run .
panic: paniiiic!
goroutine 1 [running]:
main.main()
/<パス>/main.go:6
exit status 2
Effective Go panicによると、可能な限りパニックは避けるべきとあります。ただしパニックが妥当な例として、セットアップできないという場面を挙げていました。
Githubのプロジェクトを何件か目を通して実際に見かけた例として、プログラムの開始時に環境変数を取得できないようなエラーが発生した場合はパニックを起こしていました。
Recover
recover
関数は、パニックを起こしたgoroutineの動作を正常な実行に戻すことができます。recover
は返り値としてpanic
に渡された引数を返します。この関数はdefer文の中でのみ有効です。
defer func() {
fmt.Println("done")
if err := recover(); err != nil {
fmt.Printf("run-time panic: %v", err)
}
}()
fmt.Println("start")
nums := []int{1, 2, 3, 4, 5}
num := nums[10]
fmt.Println(num)
$ go run .
start
done
run-time panic: runtime error: index out of range [10] with length 5
関数
Goの特徴的な点として、関数やメソッドが一度に複数の値を返せることです。これまでにも何度か登場してきましたが、os
パッケージのos.Open
関数を見てみましょう。
func Open(name string) (*File, error) {
return OpenFile(name, O_RDONLY, 0)
}
まずfunc
というキーワードがあり、関数名、その後ろには引数が続きます。そしてその後ろでは、返り値がFileのポインタ型とerror型であることを宣言しています。
上記のような複数の値を返す関数から返り値を受け取るには、変数を,
で区切ります。不要な値は_
とすることで無視することも可能です。
if _, err := os.Open("invalid.file.txt"); err != nil {
fmt.Println(err)
}
関数とメソッドの違い
Goでは後述のレシーバがある関数のことをメソッド、それ以外の関数をそのまま関数と呼ぶようです。
A method is a function with a receiver.
配列とスライス
配列
Goでも配列をサポートしていますが、特に理由がない限りはスライスが利用されます。逆に、変換行列のような明示的な次元を持つものについては配列が使われます。
Goの配列は要素数も型の一部であり、その要素数は負でないint型の定数である必要があります。
typeSwitch := func(t interface{}) {
switch t := t.(type) {
case [1]int:
fmt.Println("t is [1]int: ", t)
case [2]int:
fmt.Println("t is [2]int: ", t)
default:
fmt.Println("それ以外")
}
}
typeSwitch([1]int{1}) // t is [1]int: [1]
typeSwitch([2]int{1, 2}) // t is [2]int: [1 2]
typeSwitch([3]int{1, 2, 3}) // それ以外
スライス
スライスは配列をラップしてより強力なインターフェースを開発者に提供してくれます。
以下のコードでは2種類の方法でスライスを作成しています。型Tのスライスは、[]T型として表されます。配列とは異なり、スライスの要素数は型には含めません。
nums := []int{1, 2, 3}
nums2 := make([]int, 3) // [0 0 0]
typeSwitch := func(t interface{}) {
switch t := t.(type) {
case [3]int:
fmt.Println("t is [3]int: ", t)
case []int:
fmt.Println("t is []int: ", t)
default:
fmt.Println("それ以外")
}
}
typeSwitch(nums) // t is []int: [1 2 3]
続いて、スライスの要素にアクセスしてみたいと思います。for文のセクションでも紹介しましたが、スライスはrange
句で簡単にループさせることができます。また、範囲外の添え字にアクセスするとパニックが起きます。
nums := []int{1, 2, 3}
fmt.Printf("添え字でアクセス nums[0]: %d\n", nums[0])
// fmt.Println(nums[len(nums)]) panic: runtime error: index out of range [3] with length 3
for i, num := range nums {
fmt.Printf("range句でループ index: %d, num: %d\n", i, num)
}
最後にスライスに対して要素の追加や比較などの操作を行ってみます。
nums1 := []int{1, 2, 3}
nums2 := []int{4, 5}
nums3 := []int{3, 2, 1}
// スライスの長さと容量
fmt.Println(len(nums1)) // 3
fmt.Println(cap(nums1)) // 3
// 要素の追加
fmt.Println(append(nums1, nums2...)) // [1 2 3 4 6]
fmt.Println(append([]int{0}, nums1...)) // [0 1 2 3]
// 要素の削除
index := 1
fmt.Println(append(nums1[:index], nums1[index+1:]...)) // [1 3]
// スライスのソート
sort.Slice(nums3, func(i, j int) bool {
return nums3[i] < nums3[j]
})
fmt.Println(nums3) // [1 2 3]
// スライスの比較
fmt.Println(reflect.DeepEqual(nums1, nums2)) // false
fmt.Println(reflect.DeepEqual(nums1, nums3)) // trues
マップ
マップはある型の値(キー)と別の型の値(要素)を関連付けるデータ構造です。キーには整数や文字列、構造体など、等式演算子が定義されている型であれば何でも取ることができます。
map1 := map[string]string{"name": "test-user", "password": "Pa$$w0rd"}
map2 := make(map[string]string)
fmt.Println(map1) // map[name:test-user password:Pa$$w0rd]
fmt.Println(map2) // map[]
// 要素の追加・更新
map1["name"] = "test"
fmt.Println(map1["name"]) // test
// 要素の削除
delete(map1, "name")
// 要素の取得
name, ok := map1["name"]
fmt.Printf("map1[name]: %s, isExists: %t\n", name, ok) // "", false
// range句でループ
for key, value := range map1 {
fmt.Printf("key: %s, value: %s\n", key, value)
}
マップに存在しないキーで要素を取得しようとしてもパニックは起こさず、2つ目の返り値で判定することができます。また要素が存在しない場合には、1つ目の返り値は要素のゼロ値を返します。
構造体
構造体とは
構造体とは型と名前を持ったフィールドの集まりです。フィールド名は構造体内で一意である必要があります。
Errorsセクションでは、カスタムエラーを実装する際に構造体を作成しました。
type invalidValueError struct {
field string
message string
}
上記の例では、field
とmessage
というフィールドを持つinvalidValueError
という構造体を作成しています。
メソッド
Goにはクラスの仕組みはありませんが、型にメソッドを定義することができます。メソッドは関数の構文と似ていますが、func
キーワードとメソッド名の間に「レシーバ」という特別な引数を取ります。
同じくカスタムエラー実装時のコードですが、
func (error *invalidValueError) Error() string {
return fmt.Sprintf("invalidValueError %v: %v", error.field, error.message)
}
上記がメソッドであり、(error *invalidValueError)
の部分を(ポインタ)レシーバと呼びます。レシーバ引数の型がポインタではないレシーバを変数レシーバと呼びますが、こちらはレシーバが指す先の変数を変更できないため、一般的にはポインタレシーバの方が使用されます。