$ emacs memo.md

Emacsが主戦場。学生エンジニアの技術系ブログ

【Go言語(golang)入門】golangに入門してみた話

Goがいけてるらしい

結構前からずっと「golangがいけてる👍」ってよく聞きます。(よね?)

でも、これまでgolangって触ったことがありませんでした。

だから、golangで簡単なAPIサーバを作って、どんな感じの言語なのか感じておこうと思います!

Go言語(golang

言語仕様

golangの特徴として以下のようなことが、よく挙げられていました。

  • 速い
  • コンパイル型言語
  • 平行処理のサポートが良い
  • シンプルな記述
  • 高い安全性

golang における Web Application Framework(WAF)

golangにおけるWAFは基本的にマイクロフレームワークのようです。

もちろん、フルスタックフレームワークも存在します。

代表的なマイクロフレームワーク

代表的なフルスタックフレームワーク

いろいろありますが、

こちらのサイトを参考にすると、

golang標準ライブラリの net/http 互換であるものがいいらしい!

ということで、

上記参考サイトでおすすめされているものの中で最もGitHubのスター数が多かった

mux を使ってみることにしました。

今回作るもの

mux を使って、TODOを管理するAPIを作っていきます。

完成版は下記リポジトリにあります。

github.com

golang + mux でTODO管理API作成

動作環境

いざ、実装

1. main.go を作成

まずはじめに、muxではルーティングおよびハンドラ(処理)を

どのように設定するのか確認するために

1ファイルで簡単なAPIを実装してみます。

// Filename: main.go

package main

import (
    "fmt"
    "log"
    "net/http"
    "github.com/gorilla/mux"
)

func main() {
    const Host = "localhost:5000" // Go的慣例:定数には型を指定しない

    router := mux.NewRouter()
    router.HandleFunc("/users/{id:[0-9]+}", usersHandler).Methods("GET")
    fmt.Println("Server Start >> " + Host)
    log.Fatal(http.ListenAndServe(Host, router))
}

func usersHandler(w http.ResponseWriter, r *http.Request) {
    vars := mux.Vars(r)
    w.WriteHeader(http.StatusOK)
    fmt.Fprintf(w, "User No.%v", vars["id"])
}

上記コードについて説明します。

main関数

  • ルーティング設定

/users/{id:[0-9]+} はパスパラメータで指定したユーザの情報を取得するためのパスを設定しています。

/users/{id:[0-9]+} のパスに来たリクエストを処理するのは usersHandler です。

(usersHandler関数については後述)

  • サーバ設定

今回は localhost:5000 でリクエストを受け付けるようにします。

usersHandler関数

本関数は先程述べたとおり

/users/{id:[0-9]+} のパスに来たリクエストに対する処理内容を記述します。

ビジネスロジックとかって言い方しますよね。

2. 起動

$ go run main.go

上記コマンドでサーバを起動します。

実際に実行してみると

$ go run main.go
Server Start >> localhost:5000

このように表示されるでしょう。

では、実際にリクエストを投げてみます。

3. テスト

Postmanでもなんでもいいので

GETメソッドで http://localhost:5000/users/1 にリクエストを投げます。

すると

User No.1

というレスポンスが返って来たと思います!

http://localhost:5000/users/2 にリクエストを投げれば

User No.2 になりますよね!

4. より実践的なプログラムに変更

僕的、実践的プログラムとは、

  • ハンドラごとにファイルを分離
  • DBからデータを取得

上記のような処理を内包するプログラムです。

では、まずファイルの分離からやっていきましょう。

ディレクトリ構成

├── handler
│   └── users_handler.go
└── main.go

ルーティングとハンドラ(ビジネスロジック)を分けてみます。

コードの中身

// Filename: main.go

package main

import (
    "fmt"
    "log"
    "net/http"
    "github.com/gorilla/mux"
    "api-server-tutorial/handler" // ★ここ!handlerディレクトリ配下をインポート
)

func main() {
    const Host = "localhost:5000"

    router := mux.NewRouter()
    router.HandleFunc("/users/{id:[0-9]+}", handler.UsersHandler).Methods("GET")
    fmt.Println("Server Start >> " + Host)
    log.Fatal(http.ListenAndServe(Host, router))
}
// Filename: handler/users_handler.go

package handler

import (
    "fmt"
    "github.com/gorilla/mux"
    "net/http"
)

func UsersHandler(w http.ResponseWriter, r *http.Request) {
    vars := mux.Vars(r)
    w.WriteHeader(http.StatusOK)
    fmt.Fprintf(w, "User No.%v", vars["id"])
}

main.goにて、

api-server-tutorial/handler配下のハンドラをインポートします。

これでhandlerディレクトリ配下に配置したハンドラを利用できるようになります。

各自レスポンスが正しく返ってくるか試してみてください。

5. DBからデータを取得

Webサービスを作成するにあたってDBは必須ですよね!

今回は、GoでDB操作するさいによく使われているらしい

Gormを使ってみます。

GormとMySQL用パッケージのインストール

$ go get -u github.com/jinzhu/gorm
$ go get -u github.com/go-sql-driver/mysql

-u オプションは最新バージョンをインストールするっていう意味です。

今回はMySQLを使用します。

テーブル作成

テスト用にUsersテーブルを作成します。

CREATE TABLE users ( 
    id INT AUTO_INCREMENT  NOT NULL PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    age INT NOT NULL,
    created_at DATETIME,
    updated_at DATETIME
);
mysql> describe users;                                                                                                                                                               +------------+--------------+------+-----+---------+----------------+
| Field      | Type         | Null | Key | Default | Extra          |
+------------+--------------+------+-----+---------+----------------+
| id         | int(11)      | NO   | PRI | NULL    | auto_increment |
| name       | varchar(255) | NO   |     | NULL    |                |
| age        | int(11)      | NO   |     | NULL    |                |
| created_at | datetime     | YES  |     | NULL    |                |
| updated_at | datetime     | YES  |     | NULL    |                |
+------------+--------------+------+-----+---------+----------------+

DB操作機能を追加

package main

import (
    "api-server-tutorial/handler"
    "fmt"
    "github.com/gorilla/mux"
    _ "github.com/jinzhu/gorm/dialects/mysql"
    "log"
    "net/http"
)

func main() {
    const Host = "localhost:5000"

    router := mux.NewRouter()
    router.HandleFunc("/users/{id:[0-9]+}", handler.UsersShowHandler).Methods("GET") // 【変更箇所】
    router.HandleFunc("/users", handler.UsersCreateHandler).Methods("POST") // 【変更箇所】

    fmt.Println("Server Start >> " + Host)
    log.Fatal(http.ListenAndServe(Host, router))
}

今回はDBからIDを指定してユーザを取得するAPI

名前と年齢を指定して新しいユーザを作成するAPIを作成します。

したがって、 main.go は ルーティング部分(【変更箇所】)が変わっています。

また、各エンドポイントごとにハンドラを設定しています。

// Filename: handler/users_handler.go

package handler

import (
    "encoding/json"
    "fmt"
    "github.com/gorilla/mux"
    "github.com/jinzhu/gorm"
    _ "github.com/jinzhu/gorm/dialects/mysql"
    "net/http"
    "time"
)

// Model共通分を表す構造体
type Model struct {
    ID        uint `gorm:"primary_key" json:"id"`
    CreatedAt time.Time
    UpdatedAt time.Time
}

// Userを表す構造体
type User struct {
    Model
    Name string
    Age  int
}

// User作成用のパラメータを表す構造体
type UserParams struct {
    Name string
    Age  int
}

func UsersShowHandler(w http.ResponseWriter, r *http.Request) {
    // DBに接続
    db := gormConnect()
    defer db.Close() // dbを使い終わったらクローズ

    vars := mux.Vars(r)

    // 指定IDのユーザを取得
    var user User
    db.First(&user, vars["id"])

    w.WriteHeader(http.StatusOK)
    fmt.Fprintf(w, "Show this user -> %v", user.Name)
}

func UsersCreateHandler(w http.ResponseWriter, r *http.Request) {
    // DBに接続
    db := gormConnect()
    defer db.Close() // dbを使い終わったらクローズ

    // リクエストBodyをJSONにパース
    decoder := json.NewDecoder(r.Body)
    var userParams UserParams
    error := decoder.Decode(&userParams) // userParamsとbody内の対応するキーの値を入れてくれる
    if error != nil {
        w.Write([]byte("json decode error" + error.Error() + "\n"))
    }
    defer r.Body.Close() // bodyを使い終わったらクローズ

    //INSERT実行部分
    var user User
    user.Name = userParams.Name
    user.Age = userParams.Age
    db.Create(&user)

    w.WriteHeader(http.StatusOK)
    fmt.Fprintf(w, "Create this user -> %v", user.Name)
}

func gormConnect() *gorm.DB {
    DBMS     := "mysql"
    USER     := "root"
    PASS     := "mysql"
    PROTOCOL := "tcp(127.0.0.1:3306)"
    DBNAME   := "go_api_db"

    db,err := gorm.Open(DBMS, USER+":"+PASS+"@"+PROTOCOL+"/"+DBNAME)

    if err != nil {
        panic(err.Error())
    }
    return db
}

<>部分は各自変更してください)

handler/users_handler.goの方は

  • UsersShowHandler():指定IDのユーザを取得
  • UsersCreateHandler():名前と年齢を指定してユーザを作成

上記2つのハンドラを作成ました。

URLの中からパラメータ (リクエストクエリ)を取得する方法と

リクエストBodyからパラメータを取得する 方法が異なるので注意が必要です。

defer というのを使っていますが、

これは 特定の処理を関数の一番最後に実行する という機能を持っています。

したがって、今回の場合だと dbを使い終わったらコネクションを破棄することができます。

本来であればDB設定は別ファイルに切り出すべきでしょうが、今回は簡略化のためにusers_handler.goの中に記述します。

6. テスト

新規ユーザの作成

ユーザがいなければ取得もできないので、まずはユーザを作成してみましょう。

http://localhost:5000/usersPOSTリクエストを投げてみてください。

なお、 Bodyは以下のようにします。

{
    "name": "hogehoge",
    "age": 20
}

リクエストを投げると…

Create this user -> hogehoge

このようなレスポンスがきましたね!

では、DBを見てみましょう。

SELECT
  *
FROM
  users
WHERE
  id = 1;

結果は

+----+----------+-----+---------------------+---------------------+
| id | name     | age | created_at          | updated_at          |
+----+----------+-----+---------------------+---------------------+
|  1 | hogehoge |  20 | 2019-02-07 16:02:17 | 2019-02-07 16:02:17 |
+----+----------+-----+---------------------+---------------------+

作成できていますね!

指定ユーザの取得

先程作成したユーザを取得してみましょう。

ID は 1 でしたね。

したがって、以下のエンドポイントに対して GETリクエストを送信します。

http://localhost:5000/users/1

結果は

Show this user -> hogehoge

はい、指定ユーザの名前を取得できました!

唐突のまとめ

普段僕は Spring Boot や Rails, FuelPHP など、いわゆるフルスタックフレームワークによる開発をメインで行っています。

なので、今回初めてマイクロフレームワークを使うと、不便だと感じることが多々ありました。

でも、フルスタックフレームワークだと、細かいところに手が届かないときがあるので、そういう点ではマイクロフレームワークの方がいいなと思います。小回りが効きますね!

(コード書くのが大好きな僕からすると、いろいろ処理書けるので、ただただおもしろい!)

マイクロサービス主流のこのご時世、マイクロフレームワークという選択肢を持っておくことは必須ですよね。

是非ともマスターしたい技術です!

P.S.

ところどころ手抜きがある & 僕自身golang初心者なので、「golang + mux ってこんな感じなんだ」 程度に見てくださいね笑