はじめに
Goの学習として「サイト(URL)のブックマーク一覧」を取得するAPIを作ってみたのですが、
その過程でいろいろなところで躓いて(詰まって)しまいました。
自分の思考の整理として、またこれからGoを触る方の参考になればと思い、
詰まったポイントを中心にまとめてみます。
「こうするともっと良いよ~」といったやさしいツッコミも大歓迎です!
今回実装したもの
SQLiteに保存したブックマークを
一覧取得(GET /items)・1件取得(GET /items/{id})できるシンプルなAPIを実装しました。
Goを使うメリット
開発するにあたって「そもそも何故この技術を使用するのか?」を考えるのは大事なことですよね。
公式ページによると、こんなことが読み取れました。
私は今回、Webページに使用するAPIを実装しておりますが、標準ライブラリには早速助けられているな~と感じています。
クラウド&ネットワークサービス視点でのメリット
- 主要なクラウド プロバイダーのツールと API の強力なエコシステム
コマンドラインインターフェース視点でのメリット
- 人気のオープンソースパッケージを備えている
- 堅牢な標準ライブラリを備えている
ウェブ開発視点でのメリット
- 高速でスケーラブルな Web アプリケーションが実現できる
- 強化されたメモリパフォーマンス
- 複数の IDE のサポート
DevOpsとサイトの信頼性というメリット
- 高速なビルド時間
- 簡潔な構文
- 自動フォーマッタとドキュメント ジェネレーターを備えている
Goを使いたいときに、まずやること
ここは詰まったポイントではなく、今回のお約束をそのままメモしています。
リポジトリを作って、構成を考えます。今回は以下の通り。
backend ディレクトリ内でGoを使います。
/frontend
/backend
/docs
backend ディレクトリ内に main.go ファイルを作成したら、ひとまずそのファイルに
「main をパッケージすること」「標準ライブラリをインポートできるようにすること」を忘れずにやっておきましょう。
ちなみに、下記に記述している標準ライブラリは割とテキトーです。やりたいことによって必要なライブラリが異なるので…ぜひ、標準ライブラリ集を見て必要に応じて修正してください。
package main
import (
// 標準ライブラリ (https://pkg.go.dev/std)
"errors"
"fmt" // ログ出力用
"net/http"
"strconv"
)
実装が完了したら、起動(run)しましょう。Goファイルのあるディレクトリ(今回は backend)下で以下を実行。すると、おそらく http://localhost:8080 が起動するはず。
go run .
コードを読んでいて「これ何?」と思った記法メモ
いろいろなソースコードを参考にしている際、Goならではのお約束みたいなものも発見し調べたので共有します。
package main は「このコードは実行用プログラムです」とGoに伝えるための宣言
main.go ファイルの一行目にやたら書かれているコイツ、本当に必要なの?と思ったら…
「このファイル群は“実行できるプログラム”ですよ」という宣言をしているそうなのです。
というのも、Goでは、すべての .go ファイルは 必ずどこかのパッケージに属する必要があり、
package main
と記述することで「このコードは ライブラリではない」「実行用のプログラム(エントリーポイント)である」という意味を持つのだとか。
もう一段かみ砕く… package main と func main()
Goには特別なルールがあり、package main と func main()の2つが揃ったときだけ
go run .
や
go build
で 実行できるプログラムになる、とのこと。
:= は「変数を作りながら値を入れる」ためのGo独自の省略記法。
db, err := sql.Open("sqlite3", dbPath)
というソースコードを見かけたのですが、:=が私には見慣れなくて。
こちらは変数の宣言と代入を同時にやるGo専用の書き方なんだそうで
上記の1行で、実は3つのことを同時にやってくれているのだとか。
- db という変数を作る
- err という変数を作る
- それぞれに sql.Open(...) の戻り値を代入する
nil は「何も参照していない状態」を表す値で、Goでは使える型が決まっている
コードを読んでいて「nil って null と近いのかな?」と思ったのですが、
「かなり近いけど、完全に同じではない」と判明。
nil=「何も指していない」「値が存在しない」
Java / JavaScript / SQL の null に近しいようなのですが
大きな違いとして、Goではすべての型が nil になれるわけではありません。
⭕nil になれる
ポインタ、slice、map、interface、channel、function
❌nil になれない
int、string、bool、struct
一例
var s []string
fmt.Println(s == nil) // true
var i int
fmt.Println(i == nil) // コンパイルエラー
& は「この構造体そのものではなく、置かれている場所を使う」
srv := &http.Server{ ... }
という記述を読んで、気になったのが & …この子は何をしているのでしょうか?
こちらは、「実体」ではなく「その場所(参照、アドレス)」を渡しているそうです。
srv := &http.Server{
Addr: ":8080",
}
例えば、上記の記述は以下の意味になり、srv は「Serverそのもの」ではなくServerを指すポインタになります。
- http.Server を作る
- その メモリ上の場所(アドレス) を srv に入れる
& を使うメリット
-
大きな構造体をコピーしないため
http.Serverは内部にたくさん情報を持つため、コピーせず「参照」で扱った方が効率がいい!とのこと。 -
メソッドが「ポインタレシーバ」だから
http.Serverの多くのメソッドは、以下のようにポインタ前提で定義されています。
func (s *Server) Shutdown(...)
身近な比喩
-
値渡し:紙に書いた内容をコピーして渡す -
&:紙が置いてある場所(住所)を渡す
deferで「この関数が終わるときに、必ず実行してほしい処理」を予約できる
db, _ := sql.Open(...)
defer db.Close()
上記の記述内にある defer は、この関数を抜けるときに db.Close() を必ず実行するという意味です。
「エラーで途中 return しても」「panic が起きても」必ず実行されるため、以下を実施したいときに最適かも◎
- DB Close
- ファイル Close
- ロック解除
⚠️実行タイミングの注意
以下の注意点があるようです。意識して使用しましょう。
-
deferは 宣言された瞬間に評価 - 実行されるのは 関数の
return直前
詰まったポイント集
ここからは、私がいくつかAPIを実装していて詰まったポイントを記述します。
Goで外部ライブラリを使うには go mod init が必要だと知らなかった
GoでWebサーバを書く際に、方法はいくつかあるようなのですが「(機能は少ないが)軽量、かつ簡単に実装可能」だというライブラリ chi を使用してみることにしました。が、chi をインポートしようとしてもエラーが出る…。
何故なら、chi は標準ではなく、外部ライブラリだから!
では、そういった外部ライブラリをインポートするにはどうすればいいのでしょうか?
解決💡go.mod、go.sumを理解しよう
外部ライブラリを使用するには以下のファイルが必要なようです。
-
go.mod:現ディレクトリの goファイル上 におけるモジュールの依存関係のルートとなるファイル -
go.sum:直接・間接を問わず依存先モジュールのハッシュを記録するためのファイル
それらのファイルが無い? なら、作ってあげましょう。
まずは以下を実行してそれらのファイルを作り(初期化し)ます。backend 部分は、私がGoを backendディレクトリで使用したいからです。
go mod init backend
これで、go.mod、go.sumが作られるはず。
この状態になったら、外部ライブラリ chi を追加することが出来ます。
以下のコマンドで、Goが勝手にダウンロード&管理してくれるそうです(npm install に近い?)
go get github.com/go-chi/chi/v5
使用したいファイル上(今回は main.go)でインポートするのも忘れずに!
import (
"github.com/go-chi/chi/v5"
)
※標準ルーティングと chi を混在させると、どのルーティングが実際に使われているのか分からなくなるため注意が必要です。
※ここまでできるようになったら、go.mod、go.sumに何が書き込まれているか確認してみましょう。理解度が上がるはず◎
キャッシュのせいだと思ったら、実は古いGoサーバが起動していた
DBに関して、「カラム名を間違えて作ってしまったことから新たに作り直した」ということが起きたのですが(不注意でした…)、
作り直した後も、直す前のDBのデータを取得してしまう、ということが発生。
コードを直しても挙動が変わらず、
「これは古いGoサーバが起動しているままなのかも」と仮説を立てて策を実行してみることに。
解決💡古いサーバは止めよう
ターミナルで以下を実行し、
lsof -i :8080
出てきたPIDを以下のコマンドで止めました。
kill <PID>
※雑に全部止めたいなら(Goサーバだけ)、以下のコマンドでも良いみたいです。
pkill -f "go run"
※その後、コマンド lsof -i :8080 を実行して空になってるのを確認すると良いかも。
💡sql.Open は「接続」ではなく「接続プールを作る」もの
書き慣れておらず、ハンドラー(handler)の中で毎回 sql.Open をしていたのですが、
sql.Open は「今すぐ接続する」関数ではなく、「あとで使われる接続プールを準備する」役割を持っているため、毎回呼ぶ必要はないようです。
※「どこでDBを開いて何をしたいのか」を考えられていなかったな~と。
手間ばかりがかかっているコードを、現在は以下のように修正しました。
func main() {
// DBは起動時に1回だけOpen(接続プールとして使う)
db, err := sql.Open("sqlite3", dbPath)
if err != nil {
fmt.Fprintf(os.Stderr, "failed to open db: %v\n", err)
os.Exit(1)
}
defer db.Close()
// DBが本当に開けてるか軽く確認(ファイルパス違いの事故を早期発見)
if err := db.Ping(); err != nil {
fmt.Fprintf(os.Stderr, "failed to ping db: %v\n", err)
os.Exit(1)
}
// 以下省略
// ルーディングや起動ログの出力をしています
}
💡1件取得する際は QueryRow でOK
データを1件取得する際も Query を使っていたのですが、
QueryRow().Scan() という「データを1件取得する関数」があるのを知らず。
QueryRow を使うと、コードを読んでいてその行だけで「1件だけ取得するんだな」と分かるのは便利かも。
おわりに
Goは見慣れない書き方も多く最初は戸惑いましたが、
実際に手を動かしてAPIを作ってみることで、少しずつ理解が進んできた感覚があります。
今後はフロントエンドやインフラ周りも含めて実装していく予定なので、
また何かまとめられそうなネタができたら投稿したいと思います。
一緒に楽しくインプット・アウトプットしていきましょう!
オマケ:Go公式 日本語「A tour of Go」
実際にコードを触りながら学べる A tour of Go 、日本語でもできるようです。私はまだほとんど触っていないのでどれくらいの日本語訳かは保証できませんが、もし良かったら。