LoginSignup
20
13

More than 3 years have passed since last update.

GoでREST APIを作る(HTTP通信からDB接続まで)

Posted at

Goはいいぞ。

  • ビルドやテストなどのツール類が集約されている。
  • バイナリにコンパイルできる。
  • クロスコンパイル可能。
  • 構文がシンプル。
  • 学習コスト低め(個人的にはPythonくらいの学習コストのように感じます)。

のが良いところかなと思います。

この記事は

  • 基本的な構文とかはなんとなくわかったけど、そっからどうするか…
  • とりあえずGoで手を動かしたい
  • もう少し実践的っぽいものを作りたい
  • こまけぇこたぁはいいんだよ!とにかくざっくりGoの雰囲気を知りたい

みたいな人を想定して書いてます。

サンプルで使用するDBはMySQLです。
また、今回はgorilla/muxとgormという外部ライブラリを使用します。

最終的に出来上がったものはこちら

なにはともあれHelloWorld

GoのHelloWorldはこんなんです。
とりあえず写経しましょう。

main.go
package main

import "fmt"

func main() {
    fmt.Println("Hello World")
}

これであなたもGoデビューです!!

HelloWorldをブラウザに出す

localhostでサーバーを立ててブラウザからアクセスしてみましょう。

Goは外部ライブラリやFWが無くとも比較的お手軽にWebアプリが作れるのですが
少し楽するために今日はgorilla/muxってのを使います。
それになんだかウホウホしてきそうじゃないですか :8]

ということでgo getでgorillaを落としてきましょう。
go getはライブラリを落として依存関係を解決してくれる便利なやつです。
(Pythonで言うところのpipやJavaでいうmaven、gradleみたいな)

go get -u github.com/gorilla/mux

落としたらmain.goを書き換えましょう。

main.go
package main

import (
    "fmt"
    "log"
    "net/http"

    "github.com/gorilla/mux"
)

func main() {
    // 1. Routerを作成
    router := mux.NewRouter().StrictSlash(true)
    // 2. URLと処理を紐付ける
    router.HandleFunc("/", home)
    // 3. ポートを指定して起動
    log.Fatal(http.ListenAndServe(":8080", router))
}

func home(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello World")
}

これで起動して、localhost:8080にアクセスするとHello Worldと表示されるはずです。

Routerを作って、URLと実行される処理を紐づけをし、
8080ポートで起動するという結構シンプルな流れですね。
ちなみに3.でlog.Fatal()の中にhttp.ListenAndServeを書いてるのは、
ListenAndServeで起動に失敗したときに(ポートが被ってる等で)
errorを返すので、それを出力するためです。
Go界隈では割とよく見る気がします。Go感出てます。

RouterのHandleFuncの第二引数は
関数を取りますが、この関数はhttp.ResponseWriterと*http.Requestが引数になります。
HTTPレスポンスを返す場合は、第一引数のwに対して何かしら書き込んであげればそれを返してくれるので、Fprintfで文字列を書き込んでます。

構造体をJSONにして返す

次は構造体をJSONにして返します。

main.go
package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"

    "github.com/gorilla/mux"
)

// User構造体
type User struct {
    ID        int
    FirstName string
    LastName  string
}

func main() {
    router := mux.NewRouter().StrictSlash(true)
    router.HandleFunc("/", home)
    // 新しくURLと処理を追加
    router.HandleFunc("/users", findAllUsers)
    log.Fatal(http.ListenAndServe(":8080", router))
}

func home(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello World")
}

// 新しく追加した関数
func findAllUsers(w http.ResponseWriter, r *http.Request) {
    // JSON化するデータ(後でDBから取得するように修正する)
    var userList = []User{
        User{ID: 1, FirstName: "Taro", LastName: "Yamada"},
        User{ID: 2, FirstName: "Jiro", LastName: "Sato"},
    }

    // 構造体(のスライス)をJSONにする
    response, _ := json.Marshal(userList)
    // ヘッダー等設定
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusOK)
    w.Write(response)
}

GoでJSONを扱う場合はecoding/jsonってやつが標準ライブラリとしてあって
そいつを使ってやると構造体=>JSON、JSON=>構造体みたいな変換をうまいことやってくれます。
とっても便利ですよね。

次はDB編です。

DB接続してみる

DB関係もdatabase/sqlって標準でついてるものがありますが
gormってORMを使うと楽ができるのでそっちを使います。
ちなみにgorm自体はdatabase/sqlをラップして作られているみたいです。

てなわけで再びgo getしましょう。

go get -u github.com/jinzhu/gorm
go get -u github.com/jinzhu/gorm/dialects/mysql

DBはこんな感じで作ってください。

ddl.sql
CREATE DATABASE sample;
USE sample;
CREATE TABLE users(
    id int PRIMARY KEY AUTO_INCREMENT, 
    first_name VARCHAR(100) NOT NULL, 
    last_name VARCHAR(100) NOT NULL
);
INSERT INTO users(
    first_name, 
    last_name
) 
VALUES 
(
    'Taro', 
    'Yamada'
), 
(
    'Jiro', 
    'Sato'
);

main.goに追記します。

main.go
package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"

    "github.com/gorilla/mux"
    // 追加でインポート
    "github.com/jinzhu/gorm"
    // _ でインポートすることで使用しなくてもコンパイルエラーにならない。
    _ "github.com/jinzhu/gorm/dialects/mysql"
)

// User構造体
type User struct {
    ID        int
    FirstName string
    LastName  string
}

func main() {
    router := mux.NewRouter().StrictSlash(true)
    router.HandleFunc("/", home)
    router.HandleFunc("/users", findAllUsers)
    log.Fatal(http.ListenAndServe(":8080", router))
}

func home(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello World")
}

// 修正する
func findAllUsers(w http.ResponseWriter, r *http.Request) {

    // DB接続する(user、passwordは適宜修正)
    db, _ := gorm.Open("mysql", "user:password@/sample?charset=utf8&parseTime=True&loc=Local")
    defer db.Close()
    // ロガーを有効にすると、詳細なログを表示します
    db.LogMode(true)

    // 空のスライス
    var userList []User
    // SELECT文が発行されて結果がuserListに入る
    db.Find(&userList)

    response, _ := json.Marshal(userList)
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusOK)
    w.Write(response)
}

gormはちょっと細かめに補足します。

gorm.Open()とするとDB接続し、Connectionを取得します。
接続文字列でホスト名を省略すると自動的にlocalhostに接続します。
ちゃんと書きたい場合は以下のような形で書きます。

    db, _ := gorm.Open("mysql", "user:password@tcp(localhost:3306)/sample?charset=utf8&parseTime=True&loc=Local")

deferでConnectionを閉じるのも忘れないようにしましょう。
そのスコープの処理がすべて終わるときにdeferに書かれた処理が自動で実行されます。素敵ですよね。
(defer使うとGo書いてる感じして個人的には好き)

また、gormはデフォルトでは渡す構造体の名前の複数形のテーブルに対して操作を行います。
明示的に操作対象のテーブルを指定したい場合は

    // SELECT文が発行されて結果がuserListに入る
    db.Table("users").Find(&userList)

のようにTableを追加で書くとテーブルを指定できます。

構造体のフィールドはキャメルケースで、DBのカラム名はスネークケースで設定しておくと自動でマッピングしてくれます。

(ポインタ慣れないうちはあまり深く考えずにこういうもんだと思うのが良いです。Goに入りてはGoに従えという言葉もありますので。。。)

1件だけ取得する

main.go

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "strconv"

    "github.com/gorilla/mux"
    "github.com/jinzhu/gorm"
    _ "github.com/jinzhu/gorm/dialects/mysql"
)

type User struct {
    ID        int
    FirstName string
    LastName  string
}

func main() {
    router := mux.NewRouter().StrictSlash(true)
    router.HandleFunc("/", home)
    router.HandleFunc("/users", findAllUsers)
    // gorilla/muxを使うと、こう書くことでパスの文字列が取得できる
    router.HandleFunc("/users/{id}", findById)
    log.Fatal(http.ListenAndServe(":8080", router))
}

// homeやらfindAllUsersやら略

// IDで検索する
func findById(w http.ResponseWriter, r *http.Request) {

    // {id}の部分を取得する
    vars := mux.Vars(r)
    // 数値に変換してる
    id, err := strconv.Atoi(vars["id"])
    if err != nil {
        log.Fatal(err)
    }

    // DB接続
    db, _ := gorm.Open("mysql", "user:password@/sample?charset=utf8&parseTime=True&loc=Local")
    db.LogMode(true)

    var user User
    // IDで検索しに行く
    db.Where("id = ?", id).Find(&user)

    response, _ := json.Marshal(user)
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusOK)
    w.Write(response)
}

パスからIDを取るみたいな処理(mux.Vars(r)ってやってる部分)がGo標準のものでは若干手間が掛かりそうなのでgorilla/muxを使いました。

ここらで一旦コネクション取得する部分とかJSONで返す部分とか共通化しちゃいます

処理を共通化

JSON形式でレスポンスを返す処理、
DB接続の処理、
エラーレスポンスを返す処理を共通化します。
レスポンス返す系の処理はこちらの記事を参考にさせていただきました。

まずは共通処理をまとめておくutils.goから
main.goと同じ階層にutilsフォルダを作り、さらにその中にutils.goを作ってください。

utils.go
package utils

import (
    "encoding/json"
    "log"
    "net/http"
    "strconv"

    "github.com/gorilla/mux"
    "github.com/jinzhu/gorm"
)

// RespondWithError エラー情報をJSONで返す
func RespondWithError(w http.ResponseWriter, code int, msg string) {
    RespondWithJSON(w, code, map[string]string{"error": msg})
}

// RespondWithJSON JSONを返す部分を共通化
func RespondWithJSON(w http.ResponseWriter, code int, payload interface{}) {
    response, _ := json.Marshal(payload)
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(code)
    w.Write(response)
}

// GetConnection DBとのコネクションを張る
func GetConnection() *gorm.DB {
    db, err := gorm.Open("mysql", "user:password@/sample?charset=utf8&parseTime=True&loc=Local")
    // 接続に失敗したらエラーログを出して終了する
    if err != nil {
        log.Fatalf("DB connection failed %v", err)
    }
    db.LogMode(true)

    return db
}

// GetID リクエストからIDを取得する
func GetID(r *http.Request) (id int, err error) {
    vars := mux.Vars(r)
    return strconv.Atoi(vars["id"])
}

次にmain.go

main.go
package main

import (
    "fmt"
    "log"
    "net/http"

    "./utils"

    "github.com/gorilla/mux"
    _ "github.com/jinzhu/gorm/dialects/mysql"
)

// User User構造体
type User struct {
    ID        int
    FirstName string
    LastName  string
}

func main() {
    router := mux.NewRouter().StrictSlash(true)
    router.HandleFunc("/", home)
    router.HandleFunc("/users", findAllUsers)
    router.HandleFunc("/users/{id}", findByID)
    log.Fatal(http.ListenAndServe(":8080", router))
}

func home(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello World")
}

func findAllUsers(w http.ResponseWriter, r *http.Request) {
    // DB接続
    db := utils.GetConnection()
    defer db.Close()

    var userList []User
    db.Find(&userList)

    // 共通化した処理を使う
    utils.RespondWithJSON(w, http.StatusOK, userList)
}

func findByID(w http.ResponseWriter, r *http.Request) {

    id, err := utils.GetID(r)
    if err != nil {
        utils.RespondWithError(w, http.StatusBadRequest, "Invalid parameter")
        return
    }

    // DB接続
    db := utils.GetConnection()
    defer db.Close()

    var user User
    db.Where("id = ?", id).Find(&user)

    // 共通化した処理を使う
    utils.RespondWithJSON(w, http.StatusOK, user)
}

main.goがちょっとスッキリしたような気がしますよね。

INSERT

次はユーザーの追加です。

main.go
package main

import (
    "encoding/json"
    "fmt"
    "io/ioutil"
    "log"
    "net/http"

    "./utils"

    "github.com/gorilla/mux"
    _ "github.com/jinzhu/gorm/dialects/mysql"
)

// User構造体
type User struct {
    ID        int
    FirstName string
    LastName  string
}

func main() {
    router := mux.NewRouter().StrictSlash(true)
    router.HandleFunc("/", home)
    // SELECT系にもリクエストメソッドを追加
    router.HandleFunc("/users", findAllUsers).Methods("GET")
    router.HandleFunc("/users/{id}", findById).Methods("GET")
    // INSERTはPOSTで受ける
    router.HandleFunc("/users", createUser).Methods("POST")
    log.Fatal(http.ListenAndServe(":8080", router))
}

// 他の関数は省略

// ユーザー追加処理
func createUser(w http.ResponseWriter, r *http.Request) {
    // リクエストボディ取得
    body, err := ioutil.ReadAll(r.Body)
    defer r.Body.Close()
    if err != nil {
        utils.RespondWithError(w, http.StatusBadRequest, "Invalid request")
        return
    }

    var user User
    // 読み込んだJSONを構造体に変換
    if err := json.Unmarshal(body, &user); err != nil {
        utils.RespondWithError(w, http.StatusBadRequest, "JSON Unmarshaling failed .")
        return
    }

    // DB接続
    db := utils.GetConnection()
    defer db.Close()

    // DBにINSERTする
    db.Create(&user)

    utils.RespondWithJSON(w, http.StatusOK, user)

}

curlでJSONデータをPOSTしてみましょう。

curl -XPOST -d '{ "FirstName": "Mike", "LastName": "Brown" }' http://localhost:8080/users

うまくいけばMikeさんが追加されているはずです。

UPDATE

次はユーザーの更新です。
INSERTとほとんど同じように書けるのでなんてことはないはず。

main.go
package main

import (
    "encoding/json"
    "fmt"
    "io/ioutil"
    "log"
    "net/http"

    "./utils"

    "github.com/gorilla/mux"
    _ "github.com/jinzhu/gorm/dialects/mysql"
)

// User構造体
type User struct {
    ID        int
    FirstName string
    LastName  string
}

func main() {
    router := mux.NewRouter().StrictSlash(true)
    router.HandleFunc("/", home)
    router.HandleFunc("/users", findAllUsers).Methods("GET")
    router.HandleFunc("/users/{id}", findById).Methods("GET")
    router.HandleFunc("/users", createUser).Methods("POST")
    // 追加部分 PUTで受ける
    router.HandleFunc("/users", updateUser).Methods("PUT")
    log.Fatal(http.ListenAndServe(":8080", router))
}

// 他の関数は省略

// ユーザー更新
func updateUser(w http.ResponseWriter, r *http.Request) {
    // リクエストボディ取得
    body, err := ioutil.ReadAll(r.Body)
    defer r.Body.Close()
    if err != nil {
        utils.RespondWithError(w, http.StatusBadRequest, "Invalid request")
        return
    }

    // 読み込んだJSONを構造体に変換
    var user User
    if err := json.Unmarshal(body, &user); err != nil {
        utils.RespondWithError(w, http.StatusBadRequest, "JSON Unmarshaling failed .")
        return
    }

    // DB接続
    db := utils.GetConnection()
    defer db.Close()

    // Update実行
    db.Save(&user)
    // gormはSaveメソッドで主キーの部分をUpdateしてくれる。また、存在しないキーだったらINSERTされる

    utils.RespondWithJSON(w, http.StatusOK, user)
}


// 共通処理は省略

curlでJSONデータをPUTしてみましょう。
今回は更新を試したいのでJSONデータにIDを含めるようにしましょう。(無かったらINSERTされます)

curl -XPUT -d '{"ID":3, "FirstName": "John", "LastName": "Green" }' http://localhost:8080/users

うまくいけばMikeさんがJohnさんになっているはずです。

DELETE

最後にDELETEです。
今回は論理削除ではなく物理削除で実装します。
こちらもINSERTやUPDATEと基本的には同じ形で書けます。

main.go
package main

import (
    "encoding/json"
    "fmt"
    "io/ioutil"
    "log"
    "net/http"
    "strconv"

    "./utils"

    "github.com/gorilla/mux"
    "github.com/jinzhu/gorm"
    _ "github.com/jinzhu/gorm/dialects/mysql"
)

// User構造体
type User struct {
    ID        int
    FirstName string
    LastName  string
}

func main() {
    router := mux.NewRouter().StrictSlash(true)
    router.HandleFunc("/", home)
    router.HandleFunc("/users", findAllUsers).Methods("GET")
    router.HandleFunc("/users/{id}", findById).Methods("GET")
    router.HandleFunc("/users", createUser).Methods("POST")
    router.HandleFunc("/users/{id}", updateUser).Methods("PUT")
    // 追加部分 DELETEで受ける
    router.HandleFunc("/users", deleteUser).Methods("DELETE")
    log.Fatal(http.ListenAndServe(":8080", router))
}

// 他の関数は省略

// ユーザー削除
func deleteUser(w http.ResponseWriter, r *http.Request) {
    // リクエストボディ取得
    body, err := ioutil.ReadAll(r.Body)
    defer r.Body.Close()
    if err != nil {
        utils.RespondWithError(w, http.StatusBadRequest, "Invalid request")
        return
    }

    // 読み込んだJSONを構造体に変換
    var user User
    if err := json.Unmarshal(body, &user); err != nil {
        utils.RespondWithError(w, http.StatusBadRequest, "JSON Unmarshaling failed .")
        return
    }

    // IDがセットされていない場合はエラーを返す
    if user.ID == 0 {
        utils.RespondWithError(w, http.StatusBadRequest, "ID is not set .")
        return
    }

    // DB接続
    db := utils.GetConnection()
    defer db.Close()

    // DELETE実行
    db.Delete(&user)

    utils.RespondWithJSON(w, http.StatusOK, user)
}

これでcurlを叩いてみましょう。

curl -XDELETE -d '{"ID": 3, "FirstName": "John", "LastName": "Green" }' http://localhost:8080/users

Johnさんが消えていれば成功です。
ちなみにIDさえあっていればレコードは削除できます。
また、gormのDELETEはIDがない場合は全レコード削除してしまうので途中でIDのチェックをしています。

UPDATEとDELETEに関しては1件取得のようにURLに{id}を含めて、
そちらで対応する方が良い場合もあるかもしれません。今回はやりません。

最後におまけ

リクエストボディを受け取ってから構造体にパースする部分が共通化できそうなのでやっておきましょう。

utils.go
package utils

import (
    "encoding/json"
    "io/ioutil"
    "log"
    "net/http"
    "strconv"

    "github.com/gorilla/mux"
    "github.com/jinzhu/gorm"
)

// 他の関数は省略

// GetStruct JSONリクエストを構造体にパースする。エラーの場合はレスポンスするメッセージを返す。
func GetStruct(r *http.Request, i interface{}) string {

    // リクエストボディ取得
    body, err := ioutil.ReadAll(r.Body)
    defer r.Body.Close()
    if err != nil {
        return "Invalid request"
    }

    // 読み込んだJSONを構造体に変換
    if err := json.Unmarshal(body, i); err != nil {
        return "JSON Unmarshaling failed ."
    }

    return ""
}
main.go

package main

import (
    "fmt"
    "log"
    "net/http"

    "./utils"

    "github.com/gorilla/mux"
    _ "github.com/jinzhu/gorm/dialects/mysql"
)

// INSERT, UPDATE, DELETE以外の部分は省略

func createUser(w http.ResponseWriter, r *http.Request) {
    // 修正部分 共通処理で構造体を取得する
    var user User
    msg := utils.GetStruct(r, &user)
    if msg != "" {
        utils.RespondWithError(w, http.StatusBadRequest, msg)
        return
    }

    db := utils.GetConnection()
    defer db.Close()

    db.Create(&user)

    utils.RespondWithJSON(w, http.StatusOK, user)

}

func updateUser(w http.ResponseWriter, r *http.Request) {
    // 修正部分 共通処理で構造体を取得する
    var user User
    msg := utils.GetStruct(r, &user)
    if msg != "" {
        utils.RespondWithError(w, http.StatusBadRequest, msg)
        return
    }

    db := utils.GetConnection()
    defer db.Close()

    db.Save(&user)

    utils.RespondWithJSON(w, http.StatusOK, user)
}

func deleteUser(w http.ResponseWriter, r *http.Request) {
    // 修正部分 共通処理で構造体を取得する
    var user User
    msg := utils.GetStruct(r, &user)
    if msg != "" {
        utils.RespondWithError(w, http.StatusBadRequest, msg)
        return
    }

    if user.ID == 0 {
        utils.RespondWithError(w, http.StatusBadRequest, "ID is not set .")
        return
    }

    db := utils.GetConnection()
    defer db.Close()

    db.Delete(&user)

    utils.RespondWithJSON(w, http.StatusOK, user)
}

お疲れ様でした。
エラーハンドリング等改善できる部分はもっとあると思いますが
ここまで来れば一通りのCRUD操作をGoで実装できるようになったはずです。

参考

GORMガイド
Gorilla web toolkit
golangでシンプルなRESTful APIを作ってみよう!

20
13
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
13