フューチャー技術ブログ

Goを学ぶときにつまずきやすいポイントFAQ

他の言語になれた人が、初めてGoを書いた時にわかりにくいな、と思った部分はどういうところがあるのか、難しいポイントはどこか、という情報を自分の経験や、会社の内外の人に聞いたりしてまとめてみました。まだまだたくさんあるのですが、多すぎるのでまずはこんなところで。コンテナで開発することがこれからますます増えていくと思われますし、その時にコンテナとの相性が抜群なGoをこれから使い始める人もどんどん増えていくと思います。

Goは特に言語のコアをシンプルに、何かを実現するときはそのシンプルな機能を組み合わせて実現しよう、というコンセプトです。つまり、他の言語で実現したいこと・できていることに比べて、Goは組み合わせ(イディオム)でカバーする領域が広くなります。そのあたりのとっかかりになる情報を提供することが、これからGoを触る人にとってつまずきを減らすことになると思います。

Go Conference’19 Summer in Fukuokaではこちらの中からいくつかピックアップをしましたが、こちらが今の所の完全版です。

見え方の違い

Goになれている人となれていない人では、同じコードを見た時にも見えている景色がだいぶ違いますし、コードを書くときの頭の使い方もだいぶ違います。

再生 vs 再認

認知心理学では、2つの記憶のモード、再生と再認を考えます。

再生は、過去の記憶を何もインプットがなくても思い出せる状態です。再認は、「これは体験したことがありますか?」と問われたら「ああ、これは体験したことがある」と思い出せる状態です。

再生をするには完全に記憶しきっている状態にならないとダメです。慣れている人は、数多くのパターンを記憶しており、それを再生することでコードが書けますし、他の人のコードを見た時にも、過去のパターンと照らし合わせて「より良いコード」パターンを思いついたりします。

再認の方が難易度は低いです。ドキュメント、サンプルコード、ネットで調べた情報などを辿りながら、それを組み合わせて実装します。時間もかかりますし、検索で出てこないパターンだとパフォーマンスが落ちます。ですが、なんどもなんどもコードを見て繰り返し再認していくと、再生でコードが書けるようになります。このドキュメントは再認の効率アップがゴールです。

とはいえ、必ずしも全員が再生レベルになる必要はありません。たとえGoに慣れていても、普段使わないパッケージ(cryptoパッケージの暗号化とか)を使う場合は再認で(サンプルのコピペで)コードを書くことになるでしょう。上級者でも、自分用のスニペット集を作ることで、記憶の能力の節約しつつパフォーマンスは落とさないということをしますね。

アンラーニング

すでに学んでいる知識がかえって学習の妨げになりがちです。このバイアスを除外して(客観化して)、あらためて学ぶというのはなかなか難易度の高いことです。自分の知識をリセットして(組み替えて)楽しむSF好きとか、逆転裁判好きとか何かしらのプラスアルファの能力が必要じゃないでしょうか?

受けての言葉で差分を表現して教えて上げるというのはこのスタートのつまずきを解消するてっとり早い方法です。そして一通り自分で手が動くようになればそこでどんどん新しい考えが定着していきます。それまでは温かい目で見守る必要があるでしょう。

なお、うまくアンラーニングできるということは、自分がそれまで持っていた知識が客観化されることになるので、過去に学んだ言語が勝手に上達することもありえます。

実装パターン集

では説明していきましょう。そのうちカテゴリーに分けるかもしれません。以下のような形式で統一します。

  • これを使うときのGoの作法はなにか?(慣れている人がよく選ぶものはなにか)
  • この言語でやっていたように〇〇したいがどうすればいいのか?
    • できない場合はその理由
    • 複数ある場合はそれぞれの選択肢とトレードオフ
  • Goにこの機能があるけどどう使うのか?

パッケージ

Q: リポジトリはそもそもどこにおけば良いですか?

適当なフォルダを作業フォルダにするのは可能ですか?


A: Goは$GOPATHという環境変数のところにいろいろ置きます。デフォルトでは$HOME/goです。これはソースコードだけではなく、ビルド済みのライブラリなどです。とりあえず作業フォルダを作ったら、そこが$GOPATHとなるように、direnvなりで設定されるようにしましょう。

プロジェクトごとに完全に分けたいのであれば、トップの$GOPATH用のフォルダを必要なだけ切って利用します。そこまで厳密でなければデフォルトの$HOME/goで良いと思います。

$GOPATH以下は次のようになっています。appが自分の作りたいアプリケーションだとしたら、そこにフォルダを作り、git initしてしまえば良いです。go getを行ってもここにダウンロードされます。

$GOPATH
+ bin
| ... go getで取得してビルドした実行ファイル
+ pkg
| + [os_arch]
| | + ...ビルド済みのライブラリ
| + mod
| | + ...go modでダウンロードしたパッケージのキャッシュ
| + dep
| + ...今となっては古いdepコマンドでダウンロードしたパッケージのキャッシュ
+ src
+ github.com
+ user
+ app
+ library

複数のリポジトリに分離したプロジェクトを作るときも同じ場所に置きます。ただし、依存ライブラリ側は、github等からダウンロードされて、pkg/mod以下に入れられたバージョンを利用しようとします。一緒に開発したい場合、わざわざ不安定なバージョンをgit pushしなければならない、というのは不便でしょう。幸い、go.modにはreplaceという機能が使えます。appがlibraryを使う場合は、appのgo.modに次の行を加えておくと、同じ場所にあるフォルダを参照してくれます。複数リポジトリを一緒に変更しつつ動作検証するにはこの方法がベストです。

replace (
github.com/user/library => ../library
)

memo: Go 1.12では$GOPATH以下はgo.modが自動では有効にならないため、環境変数設定で、GO111MODULE=onを入れておきましょう。あと数ヶ月で不要になりますが。

環境構築にはあまり凝りすぎないで、デフォルト多めの方が、世間の情報とのずれが少なく、後から見返すときに楽です。凝りすぎたMakefileやらシェルスクリプトやらは引き継ぎのコストが多くなったりするし、自分でも忘れてしまったりしがちです。

Q: 関数や構造体は、パッケージ(ディレクトリ)やファイルにどのように配置していけばいいか?

各言語で、パッケージ、モジュール、ファイルの扱いがかなり違います。Goでは関数をどのように格納していけばいいのですか?例えば、Javaではフォルダがパッケージ。ファイルがクラスと対応づけられています。JavaScriptやPythonはファイル単位でモジュール、ディレクトリはデフォルトのファイル(index.js__init__.py)を自動で読み込む動作をします。


A: Goではディレクトリがパッケージになります。ディレクトリに含まれるすべてのファイルは同一のパッケージ名でなければなりません。一方、ファイル間で要素を移動してもビルド時には差はありません。

関数や構造体、変数などはディレクトリ内部のファイルに書きます。1つのファイルには1つの何かを書くというルールはありません。このあたりはPythonやJavaScriptに近いと言えます。

a.goとb.goが同じディレクトリ内にあった場合、a.goで定義された要素はb.goから何も宣言せずに利用できます。スコープに関してはどちらかというとJavaに近い感じです。そういった点で、ファイルに関してはどこに何をおいても、ファイル間で移動してもビルド時には差はありません。最初は1ファイルでなんでも入れておいて、大きくなってきたときに、パッケージの中で、コードなどをグルーピング化する単位でファイルに分ければ良いでしょう。

唯一、条件コンパイルで、Windowsの場合だけ、Linuxの場合だけ利用される関数といった条件コンパイルを実現するときは、ファイル単位でビルドに含めたり除外したりしますので、その単位で分けます。

Q: プロジェクトを作るときのフォルダ構成の定番はどういう構成ですか?

新規で作るときにどのようなプロジェクト構成にすればよいでしょうか?


A: ライブラリなのか、ライブラリ+CLIツールも提供するのか、ウェブサーバーなのかで変わります。

これはGoのリファレンスなどには書かれていませんが、一般的な慣習として使われているフォルダ構成です。

ライブラリの場合は、トップフォルダがライブラリのルートになります。リポジトリのルートで、go mod initをします。code.goの先頭は pakckage yourlibになり、go.modの方はmodule github.com/yourname/yourlib になります。READMEには go get -u github.com/yourname/yourlibでインストールしてねって書きます。

+ github.com
+ yourname
+ yourlib ←ここがリポジトリのルート
+ code.go
+ go.mod
+ go.sum
+ README

ライブラリが、おまけでCLIツールを提供する場合は、cmdフォルダを作ってその中にコマンド名のフォルダを作り、その中に実行ファイルのソースをおきます。Goの場合ビルドを実行したフォルダ名がデフォルトで実行ファイルの名前になります。go modulesで成果物の名前を設定できるようになりましたが、それ以前からの慣習として一般的に使われています。

複数のコマンドを提供するときはcmd以下に複数フォルダを作ります。各実行ファイルは当然package mainになります。READMEには go get -u github.com/yourname/yourlib/...でインストールしてねって書きます。

+ github.com
+ yourname
+ yourlib ←ここがリポジトリのルート
+ cmd
| + yourcmd
| + main.go
+ code.go
+ go.mod
+ go.sum

ウェブサービスのプロジェクトの場合は、トップフォルダがアプリケーションのルートになります。リポジトリのルートで、go mod initをします。実行ファイルなのでmain.goの先頭は pakckage mainになり、go.modの方はmodule github.com/yourname/yourapp になります。あるいは、go mod init yourappと実行しても良いです。その場合は、module yourappとなります。READMEには go get -u github.com/yourname/yourappでインストールしてねって書きます。

+ github.com
+ yourname
+ yourapp ←ここがリポジトリのルート
+ main.go
+ go.mod
+ go.sum
+ README

Q: パッケージやフォルダの名前には何を使えばいいか?

パッケージやフォルダには自分で名前を設定できますが、何を使うのが良いでしょうか?


A: 基本的にはEffective Goで説明されている作法(簡潔な何をするパッケージ化が明確になる一つの単語、小文字、フォルダ名と同じ)に従えば良いでしょう。

なお、この推奨はフォルダ名と同じというのが入っているので、フォルダ名も必然的に同じルールが適用されます。複数の単語で構成させたい場合は、 encoding/json のようにフォルダを分け、末端のパッケージは pakcage json とするのが一般的です。

パッケージはインポートしたあとに利用するシンボル名としてデフォルトで利用されます。簡潔で短い方が、プログラムが短くなります。GoはJavaのようななるべく明示的な名前を使う作法とは逆に、記憶の再認が可能な限り短くするのが作法です。また、フォルダ名と違うと、import文とプログラム中で利用されるシンボルがずれるため、コードを読むときに困ります。

ただし、この1単語に沿わないケースが2つほどあります。

テスト用パッケージ

たとえば、 slices という名前のフォルダを作り、 pakcage slices のような名前のパッケージでコードを書くとします。通常はGoの場合は同一フォルダ内のファイルはすべて同じパッケージにしなければなりませんが、 _test が末尾に入っている名前(ここではpackage slices_test)だけは特別に設定可能です。

リリースされるライブラリには含まれない、テスト用の共通処理を書いたり、ユニットテスト自体をこの_testパッケージで書くことができます。ただこの場合は本体のslicesとは別のパッケージ扱いになるのでimport文を書かないとslicesの提供する関数や構造体にはアクセスできませんし、公開されているものしかアクセスできません。exampleテストなどのブラックボックステストには逆にメリットですが、プライベートな要素のテストはできません。

xxxx-go, go-xxxx, xxxx.goフォルダ

たまに見かけるケースです。何か既製のライブラリのGoラッパーとか、Go移植とかで見られるものです。たとえばlxc開発元自体が提供しているgo-lxcなんかは、GitHubの同一階層に本体のlxcがあるので、それと区別するためにgo-が入っています。

なお、ハイフンとかピリオドはパッケージ名には使えません。この場合は package xxxxのように、-goとかgo-とか.go部分を外した名前をパッケージ名につけることが多いです。

ビルド環境・ツール

Q: Goについて調べるといろいろなツールが出てきますが、何が本当に必要なんでしょうか?

それぞれのツールの役割、入れた場合のメリットとか、入れなかった時のデメリットについて教えてください。


A: とりあえず、go fmtだけはエディタの保存時に実行されるように設定しておけば良いです。

  • go fmt: コードのスタイルを標準スタイルの設定する純正ツール。他の言語でいうところのPythonのblackとかJS/TSのprettier。gitのマージでのトラブルが減ります。やらないとコンフリクト等が増えるかもしれません。
  • go vet: コンパイルはできるんだけど、静的解析で問題が起きそうな箇所を見つけてくれる純正ツール。Go 1.10からはユニットテスト実行時に一緒に実行されるようになったので特に何もしなくてもご利益が得られる(当然テストはしてますよね?)
  • その他のツール群: 基本的に静的チェックでエラーを見つけてくれるものが多いです。これらのツールをまとめてチェックしてくれるツールもあります。

Goはそもそもコンパイル言語であるので、変なコードはビルドすればわかります。型の整合性もいろいろわかります。何も入れなくてもスタート地点がかなり高いしgo vet自動実行もあるので、他の動的言語で徒手空拳で戦うのに比べたら遥かに安心感を持ってコードを書くことができます。

それ以上にいろいろチェックしたければ入れると良いと思います。

ドキュメント

Q: Goはドキュメントが充実していると聞いたのですが、リファレンスを見ても使いたい機能にどうアプローチすれば良いのかわかりません。

便利そうな関数あっても、それの利用方法がドキュメントを読んでも想像できません。ついつい、Qiitaなどのサンプルコードを探してしまいます。あと、どのようにソートされているのかわからず、探しにくいです。


A: ファクトリー関数とインタフェースの2つがわかると、読みやすくなります。

まず、Goは構造体そのものを使うときには、何も初期化しないで使うか、ファクトリー関数を使って初期化します。例えば、syncのWaitGroupとかRWMutexはゼロ初期化でも正常に動きます。一方、初期化が必要なものはファクトリー関数を使って初期化します。ゼロ初期化でも動くけどユーザーの利便性のためにファクトリー関数を用意しているものもあります。

ファクトリー関数(その構造体のポインタを返す)はgo docはその構造体のメソッドリストの中に入れてくれます。なので、「この構造体が使いたい」というときはまずファクトリー関数を見つけると、そこが解決の糸口になります。

// ゼロ初期化
wg := &sync.WaitGroup{}

// ファクトリー関数で初期化
f, _ := os.Create("new.zip")
w := zip.NewWriter(f)

ただ、その「ファクトリー関数を整列する」go docの機能が仇になって、読みにくくなるケースも多少あります。net/httpのGet/Postなどは単独で使ってHTTPアクセスをする関数ですが、Responseのファクトリー関数としてリストされてしまっています。少し、使い手側の感覚とはちょっと違いますよね?関数と構造体の順番にソートされていて、単独で使える機能は関数のところだけを探しがちですが、構造体のファクトリーメソッドのところにも使える機能が隠されている可能性があります。

次はインタフェースです。例えば、先程の zip.Writer を使いたい場合、 io.Writer というものが必要というのはわかります。で、 io.Writer を見ても、どう作っていいのかわからず、ここでお手上げになってしまう、というのがGoではありがちです。知識がついてくると、「os.Createで作ればOK、テスト用にはbytes.Bufferを使おうとかわかってくるのですが、どうしてもここは引っかかりがちです。

go docをローカルで動かして静的解析するとインタフェースを実装している構造体一覧とかもわかったりはします。Goが標準で提供している外部入出力系はこれでだいたいカバーできますが、io.Reader/io.Writerとかこのあたりはある程度暗記は必要かな、と思います。

しかし、例えば、 sort パッケージのsort.Interfaceなんかは、利用者がそのインタフェースを実装するのを期待しています(ので、そのインタフェースを作成してくれる関数はどこにもありません)。また、いくつか、ライブラリが提供する構造体のみが来ることを想定して引数がインタフェースになっていることがあります。作るにしても、ライブラリが提供する構造体を利用するにしても、ライブラリの提供者がきちんと動作可能なサンプルコードをドキュメントとして提供すべきですね。場合によってはソースコードのテストコードを読むとかが必要になるかもしれません。

ドキュメントが充実している文化とはいっても、書く人によって差がでたりわかりやすかったりわかりにくかったりするのはどうしても仕方がない面があります。

型・変数

Q: ウェブアプリケーションを開発しているが、型の恩恵がわかりません

JSONのマッピングのための型など構造体を実装する手間ばかりが多く、面倒です。


A: たしかに、型のメリットを一番体感しやすいのは他人の書いてくれたものを利用するときです。自分で書いたものを自分で利用する場合はメリットを感じにくいこともありますが、後から助かる保険になります。

特にJavaScriptからGoに来ると、今時のVisual Studio Codeが賢すぎて、型情報とか定義しなくても推論してしまうので、Goが面倒に感じる場面もあります。しかし、確実に後から助けてくれます。

  • 他の人が書いたコードを読み解く時に、型情報がヒントになります。Visual Studio CodeやGoLandなどのエディタを使って入れば、「定義元にジャンプ」という機能が使えます。Goはすべての型をきちんと判定するため、確実に定義元にジャンプできます(ただしインタフェースから実装の構造体には飛べません)
  • 1ヶ月以上たって自分のコードを読み解く時に、型情報がヒントになります。
  • コード整理のためにあっちこっちに移動するときに、不一致があるとエディタがその場でエラーを教えてくれます。既存のコードに手を加えるのが楽になります。

あとは、歳をとって記憶力がなくなってくるとか、風邪をひいた、二日酔いがつらい、という状況では短期記憶能力が下がります。まずはビールを何杯か飲んでからコードを書いてみると良いかもしれません(ダメ絶対)。

おまけですが、JSONから構造体へのマッピングを作る場合などは、JSON to Goみたいなツールを使うと楽ができます。

Q: ポインタの記号がよくわかりません

*&があって使い方がよくわかりません。


A: ポインタなのか、インスタンス(実体)なのかをまず区別することが大切です。

インスタンスはメモリ上に確保されているデータの本体です。100バイトの文字列であれば100バイトぶんのメモリを消費しています。一方、ポインタはインスタンスの場所情報です。64ビット機であれば8バイトです。

インスタンスがメモリ上にあれば、そのメモリのアドレスはかならず1つあるので、インスタンスからポインタを作ることができます。また、ポインタは特定のアドレスを指しているので、ポインタからインスタンスを取り出すこともできます。相互に変換できる、というのは大切な特性です。

&は、インスタンスからポインタを取り出す操作です。下のコードのうち、下の方には&がついています。これはインスタンスをメモリ上に作った後にポインタを取り出して変数に入れています。

// personIにはインスタンスが入る
personI := Person{
Name: "小動物",
}
// personPはインスタンスを作ってからポインタを取り出してそれを格納
personP := &Person{
Name: "小山駅",
}

*には2つの意味があります。1つはポインタから、インスタンスを取り出す「操作」です。&の反対の操作です。

var name = "小椋佳"
// namePはポインタ
var nameP = &name

// そのまま表示するとポインタ値が表示される
fmt.Println(nameP) // 0x40c128
// *でインスタンスに戻すときちんと表示される
fmt.Println(*nameP) // 小椋佳

もう1つはポインタを表す型です。変数、引数の型につきます。

var person  Person // personはPerson構造体のインスタンスが入る
var person *Person // personはPerson構造体のインスタンスのポインタが入る

ポインタに関する記号にはもうひとつあります。それが.です。構造体はインタフェースのメンバーへのアクセスで使いますが、この場合はポインタだろうがインスタンスだろうが気にしないで「使える」という特別な特性があります。C/C++の場合はポインタのメンバーにアクセスする場合は->を使っていましたが、Goはどちらも.でOKです。

Q: :==の使い分けがわかりません

代入に記号が2つありますが、いまいち使い分けでいつも悩んでしまいます。


A: 何が必要かはエディタが教えてくれます。エディタに従いましょう。

明示的に型を指定して変数を作りたい場合はvarを使って=を使います。ここで:=を書くとエラーになります。

// 右辺は文字列だけど、interface{}にしたい
var name interface{} = "小中大"

変数宣言なしで宣言と代入を同時にやろうとすると:=を使います。ここで=を書くとエラーになります。

既存の変数に代入するときは=を使います。ここで:=を書くとエラーになります。

間違ったらコンパイラが教えてくれますし、エディタやIDEも赤線をひいいてくれますので、悩む前に手を動かしてしまうのが楽です。

注意すべきは新しいスコープを作る場合です。次のコードはif文のところの記号は:=でも=でも動作します。ifの条件節は新しいスコープの中になるため、新しい変数を重複してもエラーになりません。また、親のスコープで同名の変数があれば、=にしても動作します(ただし、親側の変数が書き換わる)。

package main

import (
"fmt"
)

func test() bool {
return true
}

func main() {
ok := false

// ここ
if ok := test(); ok {
fmt.Println("a", ok)
}

fmt.Println("b", ok)
}

Q: 返り値の宣言で変数名を入れるのはどういった意味がありますか?

返り値の宣言で変数名を入れる記法がありますが、横に長くなってメリットを感じません。どういったご利益がありますか?


A: メソッドの引数をn番目でアクセスしないで、名前でアクセスするのと同じで、返り値の意味を利用者や実装者に伝えるためのものです。

特に、データを外部から読み込みをするが、データ、行、列、エラーと返り値がたくさんある、みたいなケースで数が多くなってくるとプログラムのreturn文がわかりにくくなってきます。ドキュメントも読みやすくなります。

名前付きの返り値は関数開始時にゼロ値で初期化されますので、文字列の場合は空文字列、数値系の型はゼロ、bool型はfalse、errorなどのインタフェースやポインタはnilになります。エラーなんかは発生しなければそのままreturnすれば問題ありません。

Q: 関数の引数に型名が書かれていないものがあるのですが、どう解釈すればいいですか?

func Func(a, b, c []byte)のような宣言がありました。aとbの型はなんでしょうか?


A: Goでは省略した引数は後ろに宣言した型が自動で入ります。

変数宣言で次のような宣言があった場合に違和感を感じる人はあまりいないのではないでしょうか?これが引数のところでも使える、と考えればOKです。もちろん、最後の変数に宣言がないとエラーになります。

var a, b int

Q: immutableなコーディングがしたいのですがどうすればいいでしょうか?

最近のプログラミング言語では変更不可能であると宣言することで、デバッグなどがしやすくなっており、Goでもやりたいと思っています。


A: Goにはあまりimmutableに実現する手法はありません。諦めてください。

TypeScriptのconstは再代入禁止なので、結構気軽に使えました。変数宣言をすべてconstに揃えるという方法で機械的にimmutableスタイルに近づけます。Goのconstは整数や文字列などのプリミティブには使えますが、スライス、配列、map、構造体のインスタンス、構造体のポインタなどには使えません。

また、map、スライスなどは一部の要素を変更するたびに全コピーというのは遅いためコードレビューで集中砲火を浴びることになるでしょう。

構造体のメソッドのレシーバをポインタではなくてインスタンスにすると、変更した内容がインスタンスには伝搬しなくなるため、予期せぬ変更を防げるぐらいの機能はあります(が、これも変更したつもりで変わっていないというわかりにくい挙動になるので注意)。

構文

Q: 三項演算子が使いたい

条件付きの初期化処理などで、三項演算子が使いたいです。


A: Goにはありませんので、if文を書いてください。

リテラル・スライス・map

Q: “sss” + 1とか、暗黙的型変換してくれない

他の言語だと、文字列と数値の結合とかをしても、適切に変換してくれます。Goの場合は文字列と数値の結合はおろか、整数と小数の計算もエラーになって不便です。


A: 暗黙の型変換は予想外のバグを産むことがあるため、すべて明示的に書くのがGoの考え方です。つまり、これは実現不可能です。

fmt.Sprintfで文字列に変換する

結果が文字列であれば、fmt.Sprintfを使ってあげるのが簡単です。%vはどんな型でもそれなりに変換してくれます。細かい指定が必要であれば他のフラグを使って指定もできます。

fmt.Sprintf("%v%v", "abc", 1)
// abc1

文字列→数値、数値→文字列なら strconv パッケージを使う

strconvパッケージには文字列と他のプリミティブ型の相互変換の関数がもろもろ定義されています。Formatで始まる関数は文字列への変換、Parseで始まる関数は文字列から変換です。fmt.Sprintfよりはコードは長くなりがちですが、こちらの方が高パフォーマンスが期待できます。

キャストで数値にする

数値型同士の変換はキャストします。誤差がどうなるかも考えて、実装者が責任を持って選ぶ必要があります。

price := 1000
taxRate := 0.08
totalPrice := int(float64(price) * taxRate)

Q: 配列を返す関数を実装するときに、空の配列のreturnが面倒ですが簡単に書けませんか?

例えば、検索してマッチした要素のインデックスの一覧を返す関数を作るとします。通信エラーの場合に空スライスとエラーを返したいのですが冗長になってしまいます。

func FindIndexes(name string) ([]int, error) {
:
if err != nil {
return []int{}, errors.New("Network error")
}
}

A: Goではnilが空スライスとして使えるようになっています。

append()やforループにnilの空スライスを渡してもメモリアクセスエラーになったりせずに、空のスライスとして振る舞うようにGoのランタイムはデザインされています。そのため、初期化時はわざわざ空スライスを作ってあげる必要はありません。

var indexes []int             // この段階では何も代入してないのでnil
indexes = append(indexes, 10) // nilだとこの時に配列が自動で作られて帰ってくる

if err != nil {
// nilを返せば空スライスに
return nil, errors.New("Network error")
}

単なるnilでは型情報がないのでエラーになりますので、returnで型が決まっている場合以外は変数宣言は最低限必要です。

index = append(nil, 10)  // first argument to append must be typed slice; have untyped nil

なお、[]int{}と初期化すると、空とはいえ実態が作られますので、nilと比較するとfalseになります。空スライスかどうかの判定はlen()を使いましょう。

indexes := []int{}

if indexes == nil { // 常にfalse
}

if len(indexes) == 0 { // 期待通りの動作
}

Q: GoにはJavaやPythonやJavaScriptにあるSetがありませんが、どうするんでしょうか?


A: 一番簡単な(他への依存がない)方法はmapで代用する方法です。

例えば、キーが文字列であれば、map[string]boolみたいにするのがもっとも簡単でしょう。

// 作成
set := make(map[string]bool)

// セット
set["exists"] = true

// ある?
if set["exists"] {
fmt.Println("exists")
}

// ない?
if !set["not exists"] {
fmt.Println("not exists")
}

// 削除
delete(set, "exists")

もし、和集合とか積集合とか差集合が必要であればgithub.com/golang-collections/collectionsパッケージが使えるでしょう。

あるいは、順序の維持も必要であれば、ソート済み配列とsort.Searchなどを駆使する手もあります(楽ではありませんが)。ソート済み配列を対象にしたアルゴリズムのコードジェネレータもあります。

関数・ロジック

Q: クロージャって何ですか?何がうれしいんですか?

クロージャという言葉をよく聞きます。クロージャとはなんですか?ループの変数でトラブルが起きやすいとも聞きます。使うとどのようなメリットがあるんでしょうか?


A: クロージャというのは、自分が作られた環境の外の変数への参照を保持した(閉じ込めた)関数で、主に2つの用途があります。

多くの言語では無名関数やラムダと呼ばれる文法を使って作られます。Goは無名関数(funcで作る名前のない関数)です。関数の中で関数を定義すると、その関数は当然外の変数にアクセスできて当然である、と誰しもが思うでしょう。しかし、これはコンパイラが気を利かせて、関数に隠れた引数を追加して、中からアクセスしている変数のポインタを渡すようにコードを改変しているのです(Pythonは親の名前空間として持っていて、ローカルで参照できない場合は親の名前空間に順番に探しに行くことで解決)。

たとえ、親の関数を抜けて、クロージャだけが存在する状態になっても、ローカル変数のnameが残り続け、あとからクロージャを実行してもその変数が残ります。

func Function() func() {
name = "小太刀"

// これがクロージャ
closure := func() {
// クロージャの外の変数が扱える
fmt.Printf("name=%v\n", name)
}
return closure
}

closure := Function()
closure()
// name=小太刀

要素に分けてこれから説明しますが、普段使うときはここまで考える必要はあまりないでしょう。大抵のイディオムの中で知らずに使っていることが多いです。

クロージャを使って遅延実行・コールバックをする

クロージャは定義されたタイミングと実行されるタイミングが少しずれます。何か実行の準備が整ったタイミング、何かイベントがあったタイミングでコールバックされます。

単なる関数を別に定義しても結果は同じですが、その定義したところのスコープの変数へのアクセスが保持されるため、引数リストを短くできます。

// スコープを抜けたタイミングで後から実行されるクロージャ
defer func() {
fmt.Println("関数実行が終了しました")
}()

// goroutineを作成して並列動作が可能になったときに実行されるクロージャ
go func() {
fmt.Println("並列動作しています")
}()

// フォルダを探索し、ディレクトリやファイルを見つけるたびに実行されるクロージャ
err := filepath.Walk("/path/to/count/files", func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
fmt.Printf("path=%s is-dir=%v\n", path, info.IsDir())
return nil
})

クロージャの中に変数やデータを閉じ込める

乱暴な言い方をしてしまえば、構造体を作らずに、構造体のようなものを作ることです。Tour of Goのクロージャの説明はここにフォーカスしていましたね。

func fibonacci() func() int {
prev, next := 0, 1
return func() int {
prev, next = next, prev+next
return next
}
}

f := fibonacci()
for i := 0; i < 10; i++ {
fmt.Println(f())
}
// 1
// 2
// 3
// 5
// 8
// 13
// 21
// 34
// 55
// 89

引数の数が多かったり、呼び出し条件が複雑だったりする場合、なおかつ、定義場所と遠く離れたところで呼び出される場合(ただし、フレームワークにユーザー定義の振る舞いを設定するケース以外)は構造体とメソッドにしてもいいかもしれません。実行場所と定義場所が離れているクロージャの本体を探すよりは、構造体を使った方がIDEで定義を探すのは簡単です。

type fibonacci struct {
prev, next int
}

func (f *fibonacci) calc() int {
f.prev, f.next = f.next, f.prev+f.next
return f.next
}

func newFibonacci() *fibonacci {
return &fibonacci{
prev: 0,
next: 1,
}
}

f := newFibonacci()
for i := 0; i < 10; i++ {
fmt.Println(f.calc())
}

ただ、Tour of Goのような無限配列のようなコードの場合はクロージャ単体よりも、チャネルとgoroutineを使った方が良いでしょう。なお、このコードは、サンプルをシンプルにするために無限ループになっており、外から中断できるようになっていないため、goroutineリークするコードになっていますので、このままコピーはしないでください。非同期処理・並列処理の書き方を参照してください。

func fibonacci() <-chan int {
c := make(chan int)

go func() {
prev, next := 0, 1
for {
// ループ一回ごとにチャネルに書き出し
prev, next = next, prev+next
c <- next
}
}()

return c
}

f := fibonacci()
// チャネルならforループに直接置ける
for next := range f {
fmt.Println(next)
}

Q: タイムアウトのエラーと並列の重い処理をどう組み合わせていいかわかりません

タイムアウトがチャネルになっています。複数の処理の待ち合わせは sync.WaitGroupを使いたいのですが、どのように組み立てれば良いかわかりません。


A: Goには、待ち合わせには関数呼び出しによるブロックと、チャネルと主に2種類あります。まずはすべてをチャネルに集約して、selectに持ち込むのがポイントです。

例えば、sync.WaitGroupを使う場合は、終了した時にそれを通知するチャネルをあらかじめ作り、Wait()完了後にそのチャネルに送信することで、関数呼び出しのブロックをチャネルに変換できます。

func NewWaitGroup(count int) (done func(), allDone <-chan struct{}) {
wait := make(chan struct{})
allDone = wait
var wg sync.WaitGroup
wg.Add(count)
done = wg.Done
go func() {
wg.Wait()
wait <- struct{}{}
}()
return
}

チャネルどうしであればselectが使え、どちらか先に解決したほうが実行される、ということが簡単に実現できます。

done, allDone := NewWaitGroup(1)

timeout := time.After(5 * time.Second)

go func() {
//何か重い処理
done()
}()

select {
case <-allDone:
fmt.Println("全部終了")
case <-timeout:
fmt.Println("タイムアウト")
}

なお、チャネルをラップしたデータ構造としてはcontextがあります。contextは終了通知用に特化したチャネルをラップして、関数間でやり取りをしやすくしたものです。

Q: コンテキストを受け取る関数ってどんなものなんでしょうか?

あと、コンテキストとはどのようなもので、どのように使えばいいのでしょうか?


A: コンテキストを受け取る関数は、処理に長い時間のかかる関数です。もし呼び出し側の都合で中断させたいときにcontextを使います。時間がかかるという点では、他の言語で言う所のasync関数と同じようなものと言えます。

コンテキストは、goroutineを使って非同期に柔軟に仕事を行うGoで、まとめて中断したり、他の言語でいうスレッドローカルな、一つの処理単位に閉じた並列処理用の情報共有の手段としてGo 1.7から導入されました。

ウェブサービスは1つのHTTPリクエストを起点に処理がスタートしますが、その中で多数のAPIリクエストを並行して行ったりします。この「リクエスト単位の処理」を識別し、情報共有やら中断の通知をするために、context.Contextのインタフェースのインスタンスを共有します。他の言語だと、1リクエストは1スレッドとして、スレッドローカルなストレージを使ったりスレッドのIDを使ったりしますが、Goのスタイルの方が柔軟です。

書き方は次の3つに分けて説明します。

  • 作って呼び出す側
  • 外部プロセス起動、APIアクセスなどのOS由来の重い処理を投げる
  • プロセス内の複数のタスク呼び出しの結果取得などの重い処理を扱う

まずは作って呼び出す側の書き方です。

コンテキストを受け取る関数はctxという名前で第一引数で渡すのがGoの流儀です。
コンテキストはデフォルトのcontext.Background()で作り、必要に応じて手動キャンセルがしたい(context.WithCancel())、一定時間経過したら中断したい(context.WithTimeout())、指定した時間になったら中断したい(context.Deadline())でラップします。

func handler(w http.ResponseWriter, r *http.Request) {
// Backgroundはキャンセル処理はせず、情報共有のためだけのcontext
ctx := context.Background()
// WithCancelはキャンセルできるcontext
// cancelは異常時で中断したい時以外にも、正常終了時に最後に呼ぶこと
ctx, cancel := context.WithCancel(ctx)
defer cancel()

// 子供の仕事を実行するときに第一引数で渡す
work1(ctx, r)
work2(ctx, r)
}

次に外部プロセス起動、APIアクセスなどのOS由来の重い処理を投げる書き方の紹介です。標準ライブラリのnet/httpos/execcontextを受け取って、終了通知が来たら通信を中断したり、外部コマンドにシグナルを送って中断させるキャンセル処理ができるAPIも提供しています。net/httpは多少不便なので、非標準のctxhttpを使うと簡単ですし、そのうち標準ライブラリも改善される予定ではあります。

import (
"https://godoc.org/golang.org/x/net/context/ctxhttp"
)

func accessSHS(ctx context.Context) {
// ctxを第一引数で渡す
res, err := ctxhttp.Get(ctx, nil, "https://shs.sh")
}

最後に、自分でコンテキストの中断をハンドリングさせる方法です。コンテキストの中はチャネルですので、重い処理をすべてチャネルとして取り出せるようにしておけば、selectを使ってコンテキストの中断と一緒に扱うことができます。

func launchBatch(ctx context.Context) {
resultChan := make(chan Result)
select {
case result := <-resultChan:
// 正常に終了
case <-ctx.Done():
// 親の都合でキャンセル
}
}

Q: 省略可能な引数はどのように実現するのですか?

Pythonや最近のJavaScriptは引数を省略する、デフォルト値を設定する方法を提供しています。Javaはオーバーロードを駆使すれば変数の数によって似たことを実現できます。Goではどのようにするのでしょうか?


Goには省略可能な引数もオーバーロードもありません。可変長引数や、メンバーが省略可能な構造体を利用してオプション引数を実現します。

なお、可変長引数で型を interface{} にして動的に型アサーションして読み込む方法もありますが、複雑になると破綻しがちなのと、引数の役割が呼び出し側でわかりにくいのでここでは紹介しません。

名前違いの関数をいくつも提供する

たとえば、stringsパッケージには、文字列の前後の指定された文字をカットするstrings.Trim(s, cutset)関数と、文字列の前後のスペースを取り除くstrings.TrimSpace(s)があります。

細かくたくさんの引数を受け取る関数を定義し、それをラップしてデフォルトの引数を付与する便利関数を定義する、というのがGoでよく利用される方法です。

オプション構造体

最近よく見るパターンです。実装が簡単なので実装者にとってはやりやすい方法です。

type Option struct {
Host string
Port int
}

func GetData(o Option) (string, error) {
// 初期値を与える
if o.Host == "" {
o.Host = "localhost"
}
if o.Port == 0 {
o.Port = 65535
}
res, err := http.Get(fmt.Sprintf("http://%s:%d", o.Host, o.Port))
:
}

// 利用時
d, e := GetData(Option{
Host: "example.com",
})

Builderパターンの変形

JavaでおなじみのBuilderパターンの変形もたまにGoでみかけます。Builderパターンは本来は構造体などの初期化で使うパターンですが、関数呼び出しでも使えます。

Google Drive APIでは大々的に使われています。List()の返り値はDrivesListCallという構造体で、この構造体のメソッドを呼ぶたびに、構造体に引数が設定されていきます。最後にDo()を呼び出すと、処理が実行されます。それ以外にもORマッパーのクエリーの組み立てでも使われたりします。

難点としては、実装が多くなりがちなのと、このパターンを知らない人がいきなりドキュメントを見ても使い方が難しいと感じがちな点です。ただ、コード補完はバッチリ効くので、一度なれたら快適でしょう。

driveService, err := drive.NewService(ctx)

list, err := driveService
.List().PageSize(20).Q("name contains 'secret'").Do()

Pythonの疑似コードで例えるなら次のような感じになります(実際のGoogle提供のAPIとは違いますが)。

list = drive_service.list(page_size=20, Q="name contains 'secret'")

設定値をグローバルな構造体に設定して、それを利用する関数を使用

net/httphttp.Get()などが利用しているパターンです。
HTTPアクセスには、アクセス経路(TCP/IPなのか、はたまたローカルのサーバーとUnixドメインソケットで直結なのか)とか、タイムアウトとか、TLSの設定とか、パラメータが大量にあります。簡単関数とそれらを1つずつ受ける関数を作るのも大変ですし、ちょっと高度な使い方を使用としたときに呼び出し先をすべて変更してまわらないといけないのは大変です。

Goのnet/httpパッケージは、DefaultClientというClient構造体のインスタンスがグローバル変数として定義されており、http.Get()などはこれのメソッドを間接的に呼び出します。

func Get(url string) (resp *Response, err error) {
return DefaultClient.Get(url)
}

このDefaultClientに、自作のClientのインスタンスを入れることで、プログラム全体が同じ通信設定を利用できます。大抵、通信設定が複数に必要になるケースはあまりないため、このようなAPIでも問題なることはいまのところ聞いていません。

単なるグローバル変数と何が違うかというと、構造体単体を初期化してそのメソッドを呼ぶと、他の設定に依存せずに独立して利用できます。いざという時に複数の設定が必要になっても機械的に移行できますし、ライブラリ側のテストもしやすいです。

可変長引数を利用した方法

Goでは、すでに存在している型にも別名の型を定義でき、それを制約にすることができます。

Go PatternsのFunctional Optionsはその応用例の一つです。Go Patternsのサンプルの完成形の部分だけ貼ります。

fillerFile, err := file.New(
"/tmp/file.txt",
file.UID(1000),
file.Contents("Lorem Ipsum Dolor Amet"))

難点はコードの量が多くなる、パッケージのドキュメントが散らかる(New関数も引数の関数もフラットにソートされてしまう)ために、読みにくくなるといったことが挙げられます。あまり現実世界では見たことがありません。

Q: goroutineの中から外の変数にアクセスすると値がおかしくなる

ループの中でループ変数などを参照するときになぜか変な値になってしまいます

for i := 0; i < 10; i++ {
go func() {
fmt.Println(i)
}()
}
// 10
// 10
// 10
// 10
// 10
// 10
// 10
// 8
// 10
// 10

A: 関数は定義されている外の情報にアクセスできます。ただし、ポインタを持っているだけなので、値が変更されると呼ばれたときではなく、最新の値を読み込んでしまいます。

たいていのプログラミング言語では、「レキシカルスコープ」と呼ぶこの機能を実装しています。関数の中で関数を定義すると、その関数は当然外の変数にアクセスできて当然である、と誰しもが思うでしょう。しかし、これはコンパイラが気を利かせて、関数に隠れた引数を追加して、中からアクセスしている変数のポインタを渡すようにコードを改変しているのです(Pythonは親の名前空間として持っていて、ローカルで参照できない場合は親の名前空間に順番に探しに行くことで解決)。

Goのgroutineは高速とはいえ、forループが回る速度よりは低速です。そのため、goroutineが起動するころにはほとんどループが終わってしまっています。そのため、ほとんどの上記のgroutineではiが10になっています。

ポインタなのが問題なので、インスタンス化してコピーを保持するのがもっとも安全な解決方法です。コピーは関数の引数として渡す方がよいでしょう。これにより、goroutineが起動したときの変数の状態を固定化して、期待通りの結果が得られます。

for i := 0; i < 10; i++ {
go func(i int) {
fmt.Println(i)
}(i) // ここで引数としてiを入れることでコピーが引数として残る
}
// 5
// 2
// 1
// 3
// 7
// 6
// 0
// 9
// 4
// 8

Q: deferの中で変数の値が期待と違います

deferは終了時に呼ばれるはずですが、変数が終了していない状態のものになってしまっています。

finished := false
defer func(finished bool) {
fmt.Println(finished)
// false
}(finished)

// do something

finished = true

A: 呼び出しはスコープを抜ける時に行われますが、引数の評価は宣言時に行われています。

クロージャの時のケースの逆です。このケースではdefer文のところでfinishedのインスタンスのコピーが作られて固定化されてしまっているため、その後finishedを変更してもdeferの中では呼び出し時の状態に固定化されてしまっています。関数終了時の状態が必要であれば、クロージャにする、ポインタを引数で受け取るなどして最終状態にアクセスできるようにしなければなりません。

finished := false
defer func() {
fmt.Println(finished)
// true
}()

// do something

finished = true

Q: 関数型スタイルのリスト処理がやりたいがGoではどうすれば良いですか?

JavaScriptのArrayのmap/reduce/forEach/filterのメソッドを利用したコーディングが好きです。Javaにもstreamが入りました。Pythonにはリスト内包表記があります。Goではどのようにすればいいでしょうか?


A: Goではそのスタイルをサポートする機能があまりないのであきらめてください。

手続き型的にコードを書くのではなく、リストに入ったデータに対して、その加工方法(関数)を渡して(高階関数)すべての要素にパイプライン的に処理させる機能を充実させている言語は増えていますが、Goではそのようなサポートはありません。あきらめてforループを書きましょう。もちろん、末尾再帰もありません。パターンマッチもありません。

関数が一級オブジェクトではあるので、自分でmap相当の処理を書くことで似たことは実現できますが、あまり強力に推論してくれたりしないので、型アサーションの嵐になるか、リフレクションで頑張らざるを得ないため、必ずしもシンプルには実現できないでしょう。

Q: 外部のAPI呼び出しなどの時間のかかる処理でタイムアウトを実装するにはどうすれば良いでしょうか?

構造体・インタフェース

Q: Goの構造体とかインタフェースの定義の構文は冗長に見えます

Javaとかだと、class { 実装 }だけど、キーワードが多いし順番に違和感を覚えます。


A: Goは構造体もインタフェースもインラインで定義できます。インラインの定義構文+名前の定義の組み合わせになっています。

例えば、テーブル駆動テストはたいてい、インラインで構造体を定義してその配列をその場でインスタンスまで作ってしまいます。1行でやってしまっています。関数内部でしか使われない型はこのようにつくってしまえるのがGoです。

func TestSum(t *testing.T) {
testcases := []struct {
name string
a, b, result int
}{
{name: "p + p", a: 10, b: 10, result: 20},
{name: "p + 0", a: 20, b: 0, result: 20},
{name: "n + p", a: -10, b: 10, result: 0},
}
for _, testcase := range testcases {
t.Run(testcase.name, func(t *testing.T) {
actual := Sum(testcase.a, testcase.b)
if actual != testcase.result {
t.Fatalf("expected: %d, actual %d", testcase.result, actual)
}
})
}
}

一方、既存の型に名前をつけるのがtypeです。type 新しい名前 既存の型で使います。

type ErrorFlag int

Goの構造体定義の書き方はこの2つの合成です。既存の型部分にインラインの構造体定義がくっつているというわけです。

type SmallAnimal struct {
FavoriteDrink string
}

インタフェースのインライン定義も使ったことはありませんが、osパッケージのerror.goで見ることができます。

e, ok := err.(interface{ Is(error) bool })

この一行で、errというポインタ変数がfunc Is(error) boolというメソッドを持っているかどうか、という判断ができます。

Q: 構造体をJSONに書き出そうとしているのですがメンバー変数の値が出力されません。なぜでしょうか?

type Person struct {
name string `json:"name"`
age int `json:"age"`
}

A: 外部パッケージから利用できる名前は大文字スタートでないといけません。encoding/jsonパッケージはリフレクションでデータを読みに行きますが、大文字スタート以外の名前の変数は無視されます。

そのため、name, ageの先頭を大文字にすれば出力されるようになります。

Q: 汎用的なロジックを実装したいが、Goの場合は型が決まってしまうため再利用がしにくい

複数のデータ変換用の構造体に対する処理とかを書くのが大変。どうすれば良いか?


A: 共通化を行うためには、共通化のための仕組みを作り込む必要があります。

Goはなるべく高速に動作し、型のチェックをきちんと行いつつ、すばやくコンパイルが完了するというのを目指して作られた処理系です。柔軟性よりも、存在しないメンバーや変数へのアクセスがないかがすぐにわかって、実行時のメモリレイアウトがカチッと決まることがGoのコンパイラの価値です。

共通化のための仕組みを実現する方法はいくつかあります。

インタフェースを実装する方法

処理対象の構造体の共通インタフェースを定義して、それに対する処理を書きます。インタフェースは構造体のメンバー変数へのアクセスができないため、まず、必要な読み書きのメソッドを用意します。その後、そのメソッドを持つインタフェースを定義して、共通処理をそのメソッドを使って行うようにします。

type Person struct {
Name string
Age int
}
func (p Person) GetName() string {
return p.Name
}

type Dog struct {
Name string
Age int
FurColor Color
}
func (d Dog) GetName() string {
return d.Name
}

type Living interface {
GetName() string
}

// 共通処理
func PrintName(l Living) {
fmt.Println(l.GetName())
}

構造体の埋め込みを使う方法

共通の属性が一意に定まり、なおかつ共通処理はそこの情報にしか絶対にアクセスしないと保証できるなら、共通の属性を構造体として切り出し、それを各構造体に埋め込みます。

ただし、共通処理に対するコードを書く場合は、それ以外の要素にアクセスしようとか、メソッドのオーバーライドをしたいとか、一般的なオブジェクト指向言語のノリで扱うと想定通りに動かなくて時間を取られることになりますので、用法容量を守ってお使いください。

type Living struct {
Name string
}

type Person struct {
Living
Age int
}

type Dog struct {
Living
Age int
FurColor Color
}

// 共通処理
func PrintName(l Living) {
fmt.Println(l.Name)
}

Q: 構造体にメソッドを追加しました。メンバー変数を書き換えようとしても変更されません。なぜでしょうか?

type SmallAnimal struct {
name string
}
func (s SmallAnimal) SetName(name string) {
s.name = name
}

A: メソッドのレシーバーがポインタでないとメンバーへの変更ができません。

読み込み専用にしたい場合はポインタを外してインスタンスにします。基本的には最初は全部ポインタをつけておいて、「このメソッド内部では変更しないな」というのが確定する場合だけ外すというのが良いでしょう。メソッドからメソッドを呼ぶ場合がややこしいので、困ったら全部ポインタにしてしまいましょう。

Q: ある構造体に対する処理を実装する場合は、メソッドにすべきか、それとも構造体を引数に取る関数にすべきか?

Goには構造体の処理の書き方が2通りあります。どちらが良いですか?

// 1: メソッド
func (s *Struct) Method() {
// sに対する処理を書く
}

// 2: 関数
func Func(s *Struct) {
// sに対する処理を書く
}

A: 最初にメソッドで実装してしまえばいいんじゃないでしょうか?

メソッドの方は、使う側からすれば、 s. とドットを打った時点でそれに関連するメソッドが補完されますので、再認でコーディングがしやすいというメリットがあります。また、関数にしても、パッケージ名.関数名となってしまうため、利用するコード上は多少冗長になります。

ただし、処理対象が1つの構造体ではなく、複数の構造体がくる可能性のあるインタフェースになる場合は、関数の方が1つの実装でたくさんの処理対象に対して利用できるので、数が多くなってきたら関数で良いかと思います。

Q: エラーの構造体を作っています。そのエラーがタイムアウトかそうじゃないかを機械的にboolで判断できるようにするメソッドを追加したいのですが、どうすれば良いでしょうか?

標準のエラー構造体は文字列を取り出すError()メソッドしかなく、詳細情報をそこをパースして取り出すのは変更に弱いコードになってしまいます。良い方法はないでしょうか?


A: errorインタフェースを満たす実装以外に別のインタフェースも提供して、型アサーションで別のインタフェースを取り出す方法があります。

まず、公開要素のTimeoutErrorインタフェースと、非公開のtimeoutError構造体を作ります。

type TimeoutError interface {
timeout()
}

type timeoutError struct {
error string
}

// errorインタフェース用のメソッド
func (t timeError) Error() string {
return t.error
}

// TimeoutErrorインタフェース用のメソッド(private)
func (t timeError) timeout() {
}

何かしらの処理がタイムアウトしたときは、timeoutError構造体のポインタ値を返すようにします。これはerrorインタフェースを満たすので、関数の返り値はerror型でOKです。

で、この構造体はerror型以外にも、新規で作ったTimeoutErrorインタフェースも満たしますので、インタフェースからインタフェースの型アサーションも成功します。そのため、次のようにキャストすることでerrorインタフェースが持てない情報を別のインタフェースを通じて提供することができます。

if _, ok := err.(TimeoutError); ok {
// キャストが成功したのでタイムアウトエラーと判定可能
}

一定以上の年齢の人にはCOMのqueryInterfaceと言えば伝わるテクニックです。

エラー処理・例外処理

Q: エラーの種別はどのようにして区別すれば良いでしょうか?

nilと比較することでエラーの有無の確認はわかったのですが、タイムアウトなのかファイルがないのかを区別するにはどうすれば良いでしょうか?


A: ここはGo言語の実装者同士の間でも設計がぶれているところですが、基本的には型アサーションで行うことが多いでしょう。

Go本体のコードを見ても、2種類あります。osパッケージにはエラー種別を区別する関数がいくつか提供されています。IsExists, IsNotExist, IsPermission, IsTimeoutがあります。この方式はコードが一見きれいに見えますが、osパッケージ以外では見ない気がします。

_, err := os.Stats("file")
if os.IsNotExist(err) {
// ファイルが存在しない
}

他のケースでは型アサーションがあります。こちらのほうがサードパーティ製ライブラリでは一般的な気がします。

err := json.Unmarshal(jsonStr, person)
if _, ok := err.(*json.InvalidUnmarshalError); ok {
// unmarshal errorのとき
}

Q: panicはどのような時に使うのか?

Goは例外がない言語で、errorを最後の返り値として渡すのが一般的ですが、panicはどうやって使うんでしょうか?


A: panicとrecoverを使えば例外のようなことができますが、Goでは一般的ではありません。プログラマーの間違いを引っ掛けるのに使う、と考えれば良いでしょう。

Goでよくpanicを使うのは、入力値が固定値だったり、実行時に変動しないデータを扱う場合です。例えば正規表現、テンプレートなど、入力値が文字列で、内部でコンパイルのようなことを行う関数は、Mustで始まる関数も提供しています。このMustな関数は処理が失敗するとpanicになります。

var validID *regexp.Regexp

func init() {
var validID = regexp.MustCompile(`^[a-z]+\[[0-9]+\]$`)
}

Q: log.Fatal()os.Exit() ってどのような時に使うのか?

これらの関数を使うとプログラムが終了できますが、いろいろ副作用があるようです。どこで使うべきですか?


log.Fatal() は内部では os.Exit(1) を呼んでいるので、 os.Exit() ど同等なので後者に絞って説明します。 os.Exit()を呼ぶと、その次の行が実行されずにプログラムが終了します。問題なのは、 deferで設定した後処理が無視されてしまうので、完了時にネットワーク切断とかもろもろ後片付けをする行儀の良いコードが動作しなくなります。また、そのロジックが有用なコードでも、コマンドラインツールのように一回実行して完了するプログラム以外で使用が不可能になります。

基本的には、main関数以外ではerrorを上流に返していき、最後の最後、main関数の中で os.Exitを呼んでステータスコードを0以外にする、という使い方以外で使うことはないでしょう。

Q: 並列処理で複数のAPI呼び出しをしています。どこかでエラーがおきた時にまとめて終了させたいときはどうすればいいですか?

それぞれの処理で途中で継続できなくなった時に、他の処理も中断させたいと思います。どのようにすれば良いでしょうか?


A: contextはまさにその用途で使うものです。contextは並列処理で使える例外のようなものです。

JavaやPythonやJavaScriptはエラー処理機構として例外を持っており、Goはそれを持っていないと言われます。しかし、一般的な言語の例外は、呼び出し先から呼び出し元に戻っていきます。その途中で受け取って後片付けを行ったりしますが、呼び出し元と呼び出し先は1:1の関係です。Goのようにgoroutineをカジュアルにたくさん作って処理を行う場合。どこかで復旧不能なエラーが発生したら、並行で実行されている他のタスクもキャンセルしたいですよね?そのような場合にcontextを使います。

ただし、contextパッケージをそのまま使い、タスクの終了を監視しつつ(sync.WaitGroup)、各ジョブのエラーのレスポンスを監視し、どこかのgoroutineがエラーを返したらcontextのキャンセルを行うというコードを書くのは結構骨が折れます。非標準パッケージのgolang.org/x/sync/errgroupを使うと、1/10ぐらいの行数で実現ができます。

Go()メソッドはerrorを返す関数で、これがerrorを返すと、すべての並列実行タスクを終了します。これは並列じゃなくてもよくて、順次実行されるジョブでも使えます。

package main

import (
"io/ioutil"
"log"
"net/http"

"golang.org/x/sync/errgroup"
)

func main() {
eg := errgroup.Group{}

results := make(chan []byte, 2)

eg.Go(func() error {
res, err := http.Get("http://shs.sh")
if err != nil {
return err
}
defer res.Body.Close()
result, _ := ioutil.ReadAll(res.Body)
results <- result
return nil
})

eg.Go(func() error {
res, err := http.Get("http://sh.shs") // 間違ったドメイン
if err != nil {
return err
}
defer res.Body.Close()
result, _ := ioutil.ReadAll(res.Body)
results <- result
return nil
})

if err := eg.Wait(); err != nil {
log.Fatalln(err)
}
}

ロギング

Q: ログレベルを設定したログ出力はどのように実現するのか?

JavaのLog4J、Pythonのloggingパッケージではinfo/warnのようなログの出し分けができますが?Goではどのようにすればいいですか?


A: Goの標準ライブラリではサポートしていません。logruszapなどのサードパーティ製のロギングライブラリを使うのが良いでしょう

以前はlogrusほぼ一強でしたが、ハイパフォーマンスをうたったzapの人気も高まっています。zapは構造化ログに特化していますので用途に応じて選ぶと良いでしょう。zapはサンプリングレートなども設定できるので、本番環境でログが多すぎて困る、というケースでとりあえず対処するのも簡単です。

Q: ログがChromeに比べて見にくいです


A: Chromeの開発者コンソールはたいていのプログラミング言語のデバッグ環境よりも圧倒的に良いので諦めましょう。

データベース

Q: DBのトランザクション制御はdatabase/sqlBegin()つかう?


直接間接問わず、最終的にはこのメソッドでトランザクションを制御することになるでしょう。

sqlxも内部ではBegin()を使っていますし、gormもsqlパッケージのBeginTx()を使っています。便利ライブラリを使っても最終的にはdatabase/sqlにたどり着きます。

まとめ

何か困ったことはないですか?と自社のチャットやら某コミュニティに投げて飛んで来た質問とか困った事例とかについては一通り回答を書いたりした、というのが今の状況です。コンパイラでエラーになるものはここでは入れてはなくて、文法を学んで、じゃあそれを組み合わせてどう使おう、というものだけにひとまず限定しています。

もちろん、これをまとめたあとにもいくつか質問が飛んだりしていて、入れたいものはたくさんありますが、ウェブで技術ブログという体裁で出すのは分量的にこれ以上は厳しいかなぁ、という気もしますので、今後どうやってまとめていくかはまた考えたいと思います。

参考文献とか他のおすすめ