😎

GoでAPIサーバーを立てよう!(Ruby on Railsと比較しながら)

2023/12/04に公開

概要

Ruby on Rails しかバックエンド触ったことがない筆者がはじめて Go で API サーバーを立てた時、「Rails と比較しながら理解すれば理解しやすいのでは・・・!」と思いせっかくなら記事にしようと思い立って記事にした。

まず最初に、Rails で API サーバーを動かすイメージを・・・

まず、Rails で API サーバーを動かす時、どうやって実装してサーバーを動かしていたかイメージします。
文字にするとこんな感じでしょうか。

  1. rails s すると localhost:3000 にサーバーが立ち上がる
  2. routes.rb にルーティング(エンドポイントと対応するコントローラー, アクション)を定義する
  3. コントローラーのアクションの中身を書く(DB の読み込み、書き込み、レスポンスの返却...etc)

Go で API サーバーを立てるときも基本的にやることは変わらず、この通り進めていけば問題ないです。

じゃあ実際に Go で API サーバーを立ててみようか

んじゃ、実際にやってみましょう。

1. Go の環境を立ち上げる

rails でいうところの rails new ですね。Go だと以下のコマンドを実行します。

プロジェクトルートで

 go mod init github.com/(Githubのユーザー名)/(Goのプロジェクト名)

すると以下のようなファイルが作成されます。

go_mod_—_go-sample-app.png

これが 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 テーブルを作って検証してみましょう

localhost___test_-_MySQL_8_0_33.png

こんな感じの todos テーブルを作って、実際にデータを流しんでみます。

localhost___test_-_MySQL_8_0_33.png

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 型を返すための作業イメージですが

  1. SQL の取得結果を json 形式に変換する
  2. API 通信のレスポンスヘッダーを application/json にする
  3. レスポンスに 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 サーバーの設定をフレームワークに任せることができるので、コードが書きやすくなったり、保守性が高まったりするのかなと思います。

この記事の内容が参考になった方はぜひいいね頂けると嬉しいです。
最後までご覧いただきありがとうございました。

GitHubで編集を提案

Discussion