概要
Ruby on Railsしかバックエンド触ったことがない筆者がはじめてGoでAPIサーバーを立てた時、「Railsと比較しながら理解すれば理解しやすいのでは・・・!」と思いせっかくなら記事にしようと思い立って記事にした。
まず最初に、RailsでAPIサーバーを動かすイメージを・・・
まず、RailsでAPIサーバーを動かす時、どうやって実装してサーバーを動かしていたかイメージします。
文字にするとこんな感じでしょうか。
- rails sするとlocalhost:3000にサーバーが立ち上がる
- routes.rbにルーティング(エンドポイントと対応するコントローラー, アクション)を定義する
- コントローラーのアクションの中身を書く(DBの読み込み、書き込み、レスポンスの返却...etc)
GoでAPIサーバーを立てるときも基本的にやることは変わらず、この通り進めていけば問題ないです。
じゃあ実際にGoでAPIサーバーを立ててみようか
んじゃ、実際にやってみましょう。
1. Goの環境を立ち上げる
railsでいうところのrails newですね。Goだと以下のコマンドを実行します。
プロジェクトルートで
go mod init github.com/(Githubのユーザー名)/(Goのプロジェクト名)
すると以下のようなファイルが作成されます。
これがrailsでいうGemfileみたいなもので、goのプロジェクトのパッケージを管理してくれます。
2. mainファイルの作成
GoでAPIサーバーを動かす際のエントリーポイントとなるファイルを作成します。
イメージとしてはAPIサーバーの全ての処理がこのファイルを起点に動くような感じです。
touch main.go
中身はこんな感じにしてみましょう。
package main
import (
"fmt"
)
func main() {
fmt.Println("Hello world 🍣")
}
この状態で、以下のコマンドを実行すると fmt.PrintIn
の引数の内容が出力されるはずです。
❯ go run main.go
Hello world 🍣
いくつかポイントがあるので解説します。
まずファイル一番上の package main
という宣言はこのファイルの内容をmainというパッケージで扱うよ、という宣言です。goのソースコードはファイルをパッケージとして扱うことで別のファイルからソースコードの内容を参照可能になります。(イメージとしてはrailsのモジュールのincludeや、TSのimport宣言などに近いかもしれないです)
また、goのソースコードは必ずmain.goのmain関数から実行される必要があります。
試しに関数名を変えて実行してみると以下のようなエラーになるはずです。
❯ go run main.go
# command-line-arguments
runtime.main_main·f: function main is undeclared in the main package
3. localhostを起動する
次にこのmain.goでローカルサーバーを起動できるようにします。
いくつか方法があるのですが、goでは net/http というパッケージがあるので初学の際はそれを使うのが良いかと思います。
goに標準で搭載されているので特にインストールしなくても使えます
まず、httpサーバーを立ち上げるだけのコードがこんな感じです
package main
import (
"net/http"
)
func main() {
server := http.Server{
Addr: ":8080",
Handler: nil,
}
server.ListenAndServe()
}
server.ListenAndServe()
でサーバーを起動、server変数に代入しているのが起動するサーバーの設定です。ポート番号や起動時に実行する関数などの指定ができます。
実際に起動時に関数が動くようにしてみましょう
import (
"fmt"
"net/http"
)
type HelloHandler struct{}
// *HelloHandler がインターフェース http.Handler を実装
func (h *HelloHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Hello, world!")
}
func main() {
// HelloHandler 型の変数を宣言
handler := HelloHandler{}
server := http.Server{
Addr: ":8080",
Handler: &handler,
}
server.ListenAndServe()
}
サーバー起動後にcurlコマンドを実行すると↓のような感じで fmt.Fprint
の出力結果が表示されます。
❯ curl http://localhost:8080
Hello, world!
だいぶrailsのAPIサーバーに近づいてきました。
4. ルーティングを設定する
ただ、ここまでの内容だとエンドポイントを一つしか用意できません。
railsのroutes.rbに記載している内容のようなエンドポイントごとに実行する処理を指定できるようにしたいです。
type HogeHandler struct{}
type FugaHandler struct{}
func (h *HogeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "hoge")
}
func (h *FugaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "fuga")
}
func main() {
hoge := HogeHandler{}
fuga := FugaHandler{}
server := http.Server{
Addr: ":8080",
Handler: nil, // DefaultServeMux を使用
}
// DefaultServeMux にハンドラを付与
http.Handle("/hoge", &hoge)
http.Handle("/fuga", &fuga)
server.ListenAndServe()
}
こんな感じで http.Handle
を使うと、pathとpathに対応する処理を指定することができます。
railsだとcontrollerという名前を使うことが多かったですがgoだとhandlerという名前を使うことが多いです。
実際にリクエストを送ってみると
❯ curl http://localhost:8080/hoge
hoge
~/projects/go-sample-app main*
❯ curl http://localhost:8080/fuga
fuga
こんな感じでリクエストしたpathに対応する処理が返ってきます。
だいぶそれっぽくなってきたでしょ?
5. データベースと接続できるようにする
ここまでの内容でAPIサーバー自体は作れているのですが、実際のAPIサーバーはデータベースとの接続やDB操作を行うことが多いはずです。
なので次に、DBに接続できるようにします。
railsでいう config/database.yml
に書いてあるような内容をgoのファイルに記述していきます。
goでは database/sql
というパッケージがあるので、これを使ってDBに接続します。
import (
"database/sql"
"fmt"
"net/http"
_ "github.com/go-sql-driver/mysql"
)
dsn := fmt.Sprintf(
"%s:%s@tcp(%s:%s)/%s?charset=utf8&parseTime=true",
"root",
"",
"localhost",
"3306",
"test",
)
connection, err := sql.Open("mysql", dsn)
こんな感じでdsn(Data Source Name)を記述して、sql.OpenとするとDBに接続できます。
上記はlocalhost上のデータベースにrootユーザーでPWなしでアクセスする例です。
実際には環境変数等から接続情報を引っこ抜いて環境ごとに接続先を変えられるようにすると思います。
6. DBに読み書きする
これでデータベースに接続できるようになったので、次はDBの読み書きをやってみます。
goのsqlパッケージはsqlを直接記載する形でDBへの読み書きが行えます。
connection, err := sql.Open("mysql", dsn)
connection.Query("select * from todos;")
例えばこんな感じで、 connection.Query
を使うとSQLを使ってDBからデータの読み込みを行うことができます。実際にtodosテーブルを作って検証してみましょう
こんな感じのtodosテーブルを作って、実際にデータを流しんでみます。
main.go
を実行してデータが取得できるか試してみます。その際、データの出力のために少しコードをいじります。
var (
id *int
title * string
)
rows, err := connection.Query("select * from todos;")
for rows.Next() {
rows.Scan(&id, &title)
fmt.Printf("id: %d\n", *id)
fmt.Printf("title: %s\n", *title)
}
rowsに、SQLのクエリの実行結果が入りますが、これはそのままだと人間の目に見える形にならないので、実行結果からidとtitleだけをScan()を使って抜き出して出力します。
実際の出力結果が↓のような感じです。
go run main.go
id: 1
title: test
id: 2
title: test2
今回は記事の長さの都合上省略しますが、書き込みの場合も同じsqlパッケージを使って行うことができます。
7. 取得した結果をjson形式のレスポンスとして返す
ここまでで実際にDBから値を取得することができたので、次はこれをAPIのレスポンスとして返却できるようにします。
まず、APIのレスポンスとしてjson型を返すための作業イメージですが
- SQLの取得結果をjson形式に変換する
- API通信のレスポンスヘッダーをapplication/jsonにする
- レスポンスに1で変換したjsonを書き込む
という感じになります。
1. SQLの実行結果をjson形式に変換
type Todo struct {
ID int `json:"id"`
Title string `json:"title"`
}
まずAPIレスポンスの型を定義します。この時に json:id
のように宣言することで構造体の各フィールドがjsonのどこに紐づくのかを指定することができます。
rows, err := connection.Query("select * from todos;")
todos := []Todo{}
if err != nil {
panic(err)
}
for rows.Next() {
todo := Todo{}
rows.Scan(&todo.ID, &todo.Title)
todos = append(todos, todo)
}
rows.Close()
実際にSQLからの取得結果を上記で定義した構造体に変換する処理です。
rows.Next()
で1行ずつsqlの実行結果を処理することができます。
空の構造体todoという変数に対して、rows.Scan()を実行することで、sqlの行からID, Titleを取り出して、構造体の中に入れることができます。
空配列todosを定義して、appendで構造体を配列の中に追加していきます。
rows.Next()のfor文が終了したら、rows.Close()でrowsに対する処理を終了します。
import (
"database/sql"
"encoding/json"
response, _ := json.Marshal(todos)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write(response)
最後に変換した構造体の配列をjson形式に変換します。 encoding/json
というパッケージがgoでは標準で使えるので、これを使ってjson形式に変換します。
w
はHogeHanlderの引数で http.ResponseWriter
で、wが持つ関数を使うことでレスポンスヘッダーやレスポンスボディを指定することができます。
ここではレスポンスヘッダーに "Content-Type", "application/json"
と status200を指定しつつレスポンスボディに先ほど変換したjsonを載せています。
こうすると、実際にAPIから変換したjsonがレスポンスとして返ってきます。
❯ curl http://localhost:8080/hoge
[{"id":1,"title":"test"},{"id":2,"title":"test2"}]
main.go全体図
即席で作ったので不要なコードもあるかもしれませんが、ここまで進めて以下のような感じのコードになっています。
package main
import (
"database/sql"
"encoding/json"
"fmt"
"net/http"
_ "github.com/go-sql-driver/mysql"
)
type Todo struct {
ID int `json:"id"`
Title string `json:"title"`
}
type HogeHandler struct{}
type FugaHandler struct{}
func (h *HogeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
dsn := fmt.Sprintf(
"%s:%s@tcp(%s:%s)/%s?charset=utf8&parseTime=true",
"root",
"",
"localhost",
"3306",
"test",
)
connection, err := sql.Open("mysql", dsn)
if err != nil {
fmt.Println(err)
panic("failed to connect database")
}
rows, err := connection.Query("select * from todos;")
todos := []Todo{}
if err != nil {
panic(err)
}
for rows.Next() {
todo := Todo{}
rows.Scan(&todo.ID, &todo.Title)
todos = append(todos, todo)
}
rows.Close()
response, _ := json.Marshal(todos)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write(response)
}
func (h *FugaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "fuga")
}
func main() {
hoge := HogeHandler{}
fuga := FugaHandler{}
server := http.Server{
Addr: ":8080",
Handler: nil, // DefaultServeMux を使用
}
// DefaultServeMux にハンドラを付与
http.Handle("/hoge", &hoge)
http.Handle("/fuga", &fuga)
server.ListenAndServe()
}
実際には以下のような形で関数やファイルを分割することが多いような気がします。
- DB接続処理は別ファイルへ
- ルーティング定義も別ファイルへ
- Hanlderはhandlersディレクトリを切って別ファイルへ
- main.goにはサーバーの起動処理だけが書かれている
この後にやること
ここまでできるとかなりAPIサーバーっぽくなってきたかと思います。
次にやることとしては以下のあたりになるかと思います。
- ORMを使ってみる
→今回はgo標準のSQLパッケージを使いましたが、goにはいくつか便利なORMが存在します(SQLBoiler, GoORMなど。railsでいうActiveRecord) - フレームワークを使ってみる
→今回はgo標準のhttpパッケージを使いましたが、goのhttpフレームワーク(Gin, echoなど)を使うとより簡単にリクエスト/レスポンスの設定ができます。
ORMやフレームワークを使うと複雑なSQLやhttpサーバーの設定をフレームワークに任せることができるので、コードが書きやすくなったり、保守性が高まったりするのかなと思います。
この記事の内容が参考になった方はぜひいいね頂けると嬉しいです。
最後までご覧いただきありがとうございました。