Help us understand the problem. What is going on with this article?

Rubyの会社のフロントエンドエンジニアが、GolangでAPIサーバーを開発してみる。

More than 3 years have passed since last update.

初めまして。マネーフォワードでフロントエンドエンジニアをしている高山です。

Money Forward Advent Calendar 2016と言う事なんですが、
Advent Calendarを書くのが今回初めてでもあり、Qiitaに投稿するのも初めてだったりします。
「え。なに、アウトプットしてないの」と、思われがちですが、Web帳と言う意識の低いゆるいブログをかれこれ6年程書いております。
気になった事とか、メモ程度に不定期に書いていたりします。

また、それとは別に、今年の頭くらいに 新たなCSS Architecture として、APBCSSを提唱させていただきました。
CSS Architectureが、「お。良いなぁ。」と思った方は是非導入して頂ければ幸いです。

と言う訳で、自己紹介はそのぐらいで、今回はタイトルである通り、CSSでもなく、JavaScriptでもなく、Rubyでもなく、Golangについて書くと言う暴挙に出てみました。

長年、Golangを書いて来た訳でもなく書き始めて1ヶ月も満たないのですが、
「え。何これ楽しいやん。」
と、ついつい思ってしまって、書かずにはいられなくなく今回に至りました。

楽しいは正義です。

と言う訳で、「これからGolangで何か作りたいなぁ」と思われている方が、何か参考になれば幸いです。
すでにGolangでゴリゴリ開発されている方には何も参考にならないかもです。 ><

と、弊社でのGolangの導入事例ではなく、全くの趣味でのサンプル作成程度になりますのでご了承頂ければと思います。

環境

  • Mac OS X EL Capitan 10.11.6
  • go 1.6.2 darwin/amd64

はじめに

Golangって何

Go(lang)を知らない方の為に、ざっとではありますが要点をまとめると、

  • Goは、Google社によって開発され、2009年に発表された比較的新しいプログラム言語
  • Github利用したオープンソース github
  • CやJavaなどの静的型付け言語
  • オブジェクト指向機能を持たないが関数型言語でもない。例外もない。
  • コンパイル言語でCPUとOSに適合したネイティブコードを生成する
  • 実行速度も、もちろん早い
  • 保守性が高く、とにかくシンプル
  • 楽しい(大事)

と、言った感じでしょうか。
詳細を書いていくと、さすがに莫大となりますので、ところどころは端折らせていただきます mm

公式ページはこちらとなります。
The Go Programming Language

こちらのページでは、ブラウザ上でgolangを実行することができます。

また、チュートリアルページもこちらにあります。
こちらもブラウザ上でgolangを実行しながら、チュートリアル進めていけてかなり網羅されているので大変オススメです。

チュートリアル

インストール

それでは、インストールしていきましょう。
こちらから各OS環境向けのバイナリファイルをダウンロードできますのでダウンロードしましょう。

ダウンロード

パッケージファイルをダウンロードしたらインストーラーを起動しインストールを行います。

go1.png

手順通り進めてインストールを完了させましょう。
インストール後、goパッケージは以下のフォルダに格納されます。

/usr/local/go/

Goコマンドが利用できるようにパスを通します。

$ export PATH="/usr/local/go/bin:$PATH"

GOPATH

Goは外部ライブラリを格納するための場所の指定でGOPATH が必要となってきますので、GOPATHの指定を行いましょう。
ホームディレクトリに配置が一般的の様です。

$ mkdir $HOME/go
$ export GOPATH=$HOME/go

goコマンドでバージョンを確認。

$ go version

このように表示すればインストール成功です。

go version go1.6.2 darwin/amd64

GOPATHの確認もしておきましょう。

$ env | grep GOPATH

以下の様に設定したPathが表示すれば成功です。

GOPATH=/Users/[ユーザー名]/go

一通りの設定はこれで完了です。
お疲れ様でした。

Hello World!

それでは、定番のHello World!を表示させましょう。

goのファイルの拡張子は .go となります。
Hello World! を表示させる簡単なプログラム。

main.go

package main

import "fmt"

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

いやぁ。簡単ですね。

それでは、実行してみましょう。

go run コマンドでプログラムを実行する事が可能です。

$ go run main.go

Hello World! が表示すれば成功です。

Hello World!

Goはコンパイルを行うのが前提なのですが、go run コマンドでビルドプロセスを隠蔽しつつ実行することが可能です。

ビルド

プログラムをビルドするのは、 go build コマンドを使います。

$ go build

ビルド後の名前を指定したい場合は「-o」オプションを付与すればOKです。

$ go build -o hello main.go

パッケージ

import と書かれているのが、パッケージをimportする箇所となります。

気をつけないといけないのが、使用していないパッケージをimportに記述すると、コンパイルエラーが発生し、ビルドを行う事ができません。

上記のプログラムでは、文字列の入出力に便利な機能が含まれている fmt パッケージを利用しました。

fmt.Println("Hello World!") は、fmt パッケージの公開関数 Println で、文字列の最後に改行を追加して実行しております。

Println

このように関数の頭文字を大文字にする事によって、公開、非公開にする事ができるのも Goの特徴の一つでもあります。

HTTP web framework

Hello World! を表示させるだけではつまんないですよね。

Go言語用に作られた web framework が、いくつか有志の手によって作られていますので、使ってみることにしましょう。

Go言語の主要な、web frameworkをざっとまとめたのが、以下のような感じとなります。

名前 Github Star数 軽く説明
beego 8987 Sinatraを参考に作られている。
martini 8928 シンプルに使用でき一番人気
Gin 8229 HttpRouterがMartini よりも 40 倍速いらしい。
Revel 7631 Play Frameworkを参考に作られている。
echo 5980 パフォーマンスとminimalistなところを売りにしている。
goji 3194 シンプルな作り。Middlewareの仕組みが用意されている。

※12月10日付時点 Github Star数順

今回、viewの箇所の作成なども特に行わないので、軽量な framework で問題ないのですが、「Gin 」 と言う名前が気に入りましたのでw (それ以外の理由もありますが) 「Gin 」 を利用する事にしました。

実際にプロジェクトに導入する際は、要件などにあったframeworkを選定して頂ければと思います。

それでは、早速使っていきましょう。

Githubなどで公開されている、外部パッケージのダウンロードとインストールをまとめて実行してくれる go get コマンドで外部パッケージをインストールしていきます。

$ go get github.com/gin-gonic/gin

Gin 」 パッケージをインストール完了したら、

ブラウザで、http://localhost:8080/ にアクセスして Hello world! が表示できるようにしましょう。

main.go

package main

import "github.com/gin-gonic/gin"

func main() {
    r := gin.Default()

    r.GET("/", func(c *gin.Context) {
        c.String(200, "Hello world!")
    })

    r.Run(":8080")
}

ブラウザで、http://localhost:8080/ にアクセス。
Hello world! と、ブラウザに表示。

おめでとうございます!
APIサーバーができましたね!

終わり!

といきたいのですが、これではさすがに面白くないですよね。。。
何か、データベースからデータを取得できるようにしましょう。

ORM

Go言語にはデフォルトでSQLやSQLなどのデータベースに接続するためのインターフェースがデフォルトで用意されています。

Package sql

各データベース用のドライバーを追加する事によって、データベースに接続する事が可能となります。

package main

import (
  "database/sql"
  _ "github.com/go-sql-driver/mysql"
)

func main() {
  db, err := sql.Open("mysql", "root:@/database")
  if err != nil {
    panic(err.Error())
  }
  defer db.Close()

  rows, err := db.Query("SELECT * FROM users")
  if err != nil {
    panic(err.Error())
  }
  ...
}

といった具合にqueryを発行して使用できるのですが、慣れている方は良いかと思いますが、生SQLを書くのはしんどかったりしますよね。。

安心してください。

履いて.. じゃなくて、これもまたGo言語で使用できるORMが有志の手によっていくつか作られていて、Github上に公開されています。

名前 Github Star数 軽く説明
gorm 4693 高機能。 RailsのActiveRecordに近く、RailsサーバーをGoに置き換えるには便利。
gorp 2286 StructとDBのマッピングをするだけのシンプルな機構。
xorm 1541 比較的シンプルで、has_manyなどのアソシエーションのサポートもない。
genmai 126 機能はシンプルでクエリビルダに近い。

※12月10日付時点 Github Star数順

gorm が、RailsのActiveRecordに近く、RailsのConventionに従っているところもあり、gormを使用してみる事にしました。

GORM インストール

それでは、早速GORMをインストールして使っていきましょう。
まずは、公式ドキュメントのソースを見て使っていきます。

GORM Document

go getコマンドで、gormパッケージをインストール。

$ go get -u github.com/jinzhu/gorm

import文にgormパッケージを追加します。

import (
    "github.com/jinzhu/gorm"
    _ "github.com/jinzhu/gorm/dialects/sqlite"
)

Productの構造体を確認。

と言いますか、構造体が出てきましたね。

C言語やSwift同様、structを定義する事が可能です。
Goでは、Classが存在しないため、主にモデリングもstruct(構造体)を用いて作成します。

構造体に関しては、主に struct { フィールドの定義 }となり、
以下の場合は、Productの構造体に、typeを用いて型付けしていることになります。
(すみません。。詳細は割愛させていただきます。)

type Product struct {
  gorm.Model
  Code string
  Price uint
}

ここで、指定しているgorm.Model を確認してみると、以下のように、 ID, CreatedAt, UpdatedAt, DeletedAt のフィールドを持つ structが確認できます。
なので、gorm.Model を含むことによって、これらが含まれます。

package gorm

import "time"

// Model base model definition, including fields `ID`, `CreatedAt`, `UpdatedAt`, `DeletedAt`, which could be embedded in your models
//    type User struct {
//      gorm.Model
//    }
type Model struct {
    ID        uint `gorm:"primary_key"`
    CreatedAt time.Time
    UpdatedAt time.Time
    DeletedAt *time.Time `sql:"index"`
}

それでは、プログラムを実行する main関数を見ていきます。

func main() {
  db, err := gorm.Open("sqlite3", "test.db")
  if err != nil {
    panic("failed to connect database")
  }
  defer db.Close()

  // Migrate the schema
  db.AutoMigrate(&Product{})

  // Create
  db.Create(&Product{Code: "L1212", Price: 1000})

  // Read
  var product Product
  db.First(&product, 1) // find product with id 1
  db.First(&product, "code = ?", "L1212") // find product with code l1212

  // Update - update product's price to 2000
  db.Model(&product).Update("Price", 2000)

  // Delete - delete product
  db.Delete(&product)
}

それでは、上から見ていきましょう。 db, err := gorm.Open("sqlite3", "test.db") で、sqlite3のtest.dbを読み込んでいるのがわかります。
:= とありますが、goではこちらの記法で型推論を行ってくれます。
便利ですね!

err が発生した際、panicを実行します。(panicについては割愛)
defer db.Close() で、main関数終了後に閉じる処理となります。

続いて、 db.AutoMigrate() でテーブルを作成しています。

db.AutoMigrate(&Product{})

関数を呼び出し先を見ると、 (values ...interface{}) と、可変長引数になっているのがわかりますので、複数指定する場合は(hoge, hoge,...)とカンマ区切りで渡せば大丈夫そうですね。

また、ポインタ型で受け取ってますので、参照渡しで渡さないといけません。
(こちらも、静的型付け言語ということで詳細は割愛させていただきます。)

func (s *DB) AutoMigrate(values ...interface{}) *DB {
...

その下に、Create, Read, Update, Deleteとあります。
Createでデータを作成し、Readで呼び出し、Updateで更新しているのがわかります。

db.Create(&Product{Code: "L1212", Price: 1000})
...
db.Model(&product).Update("Price", 2000)

デモを記述して実行してみましょう。

$ go run main.go

サンプルでは、sqlite3を指定しているので、dbファイルがない場合は、sqliteファイルが生成されるかと思います。

$ sqlite3 test.db 

select文を叩いてみると。

select * from products
1|2016-12-10 06:31:54.251389791+09:00|2016-12-10 06:31:54.252403367+09:00|2016-12-10 06:31:54.252916761+09:00|L1212|2000

と、こんな感じでテーブルのなかにデータができているのが確認できます。
それでは、APIサーバー用のデータを作っていきましょう。

データベース設計

go_db2.png

今回は、本当にシンプルにユーザーの名前の入った usersテーブルと、userが評価点をつけたアイテムを管理するitemsテーブルを用意します。
本当は、中間テーブルとかあった方が良かったりしますが、あくまでサンプルなので比較的雑な仕様で。

Userデータ作成
それでは、Userのstructから作っていきます。
Userのstruct(構造体)に対して、typeで型付けを行っております。
また、サンプルであった gorm.Model は、シンプルにするため外させていただきました。

type User struct {
    ID    uint   `gorm:"primary_key" json:"id"`
    Name  string `json:"name"`
    Items []Item `json:"items"`
}

フィールドを見ると、uint型のID、string型のName、Item型のslice Itemsの変数が定義しているのがわかります。

変数 型 の後に gorm:"primary_key" json:"id" とありますが、これは「タグ(Tag)」と言われ、文字列リテラルや、Raw文字列リテラルで、フィールドにメタ情報を持たせることができます。

今回、 gorm:"primary_key" と、Raw文字列リテラルでタグつけを行っていますので、gormでstructを参照し、テーブル作成した際、UserのIDがprimary_keyとして扱われるように作成されます。

同じ様にItemの構造体を定義していきます。
Itemには、ユーザーが評価した点数が保持できる様に作成します。

type Item struct {
    ID     uint   `gorm:"primary_key" json:"id"`
    UserID uint   `json:"user_id"`
    Name   string `json:"name"`
    Score  int    `json:"score"`
}

それでは、データが作成できる様に作ります。
今回は、ユーザーが、ゲームに対して評価を行い点数付けするとし、データを作っていきます。

main()関数が呼ばれる前に初期化を行いたいので、main()関数よりも先に呼ばれるinit()関数に記述していきます。

今回はサンプルと言う事もあり、がっつりハードコーディングしていきます。

func init() {
    db.AutoMigrate(&User{}, &Item{})

    itemNames := []string{
        "ゲーム1",
        "ゲーム2",
        "ゲーム3",
        "ゲーム4",
        "ゲーム5",
        "ゲーム6",
        "ゲーム6",
        "ゲーム7",
    }

    userNames := []string{
        "ユーザー1",
        "ユーザー2",
        "ユーザー3",
        "ユーザー4",
        "ユーザー5",
        "ユーザー6",
        "ユーザー7",
        "ユーザー8",
        "ユーザー9",
        "ユーザー10",
    }

    users := CreateUsers(userNames, itemNames)
    ...
}

itemとuserをそれぞれ複数、slice型で生成し、CreateUsers関数に渡します。
(Goでは配列とslice型で異なってきますので注意。)

func CreateUsers(userNames []string, itemSlice []string) []User {
    users := make([]User, len(userNames))

    for i, name := range userNames {
        users[i] = NewUser(name, itemSlice)
    }

    return users
}

users := make([]User, len(userNames)) で、User型のsliceを生成します。
len(userNames)で、数を算出し、メモリを確保します。

users[i] = NewUser(name, itemSlice) で、User型を生成。

どうも、Goではstructから生成する際は、NewStructName という形の関数名にするのがGoの慣用表現みたく、「Goに入ればGoに従え」精神でそれに従います。

func NewUser(name string, itemSlice []string) User {
    return User{
        Name:  name,
        Items: CreateItems(itemSlice),
    }
}

NewUser関数は、Userを生成する関数で、Userが保持するItemsも引数itemSliceから生成します。

擬似乱数を生成

CreateItemsはCreateUsersと変わらないのですが、NewItemに関してはItem自身がScoreを保持する必要がありますので、ランダムに生成したいと思います。

func NewItem(name string) Item {
    rand.Seed(time.Now().UnixNano())

    return Item{
        Name:  name,
        Score: rand.Intn(10) + 1,
    }
}

Scoreはランダムの値としたいので、 math/rand パッケージを importし生成することにします。

import (
    "math/rand"
    "time"
)

この時、rand.Intn()で擬似乱数を生成するのですが、C言語では「入力する値が同じであればrand関数は同じ乱数列を出力する。」 といったアルゴリズムの為、異なる乱数列が欲しい時には seed値を与える必要があります。

現在時刻をseed値として擬似乱数を生成

rand.Seed(time.Now().UnixNano())

また、 rand.Intn(10) + 1と、1を加算しているのは、1〜10の値とする為です。

と、いった感じで、Item型のsliceを保持したUser型のsliceのUsersの生成ができましたので、db.Create(&user) で、テーブルにデータを作って完了。

データがすでに存在する場合は生成したくないので、usersテーブルのCountが0の場合のみ実行します。

count := 0
db.Table("users").Count(&count)
if count == 0 {
    for _, user := range users {
        db.Create(&user)
    }
}

usersの数だけfor文で回しているのですが、indexは必要ないので _で変数を破棄しております。
これを行わないと、使用していない変数を生成している事になるのでコンパイルも通りません。

for _, user := range users

Underscore.jsと間違えそうですが、異なります。(間違えないか。)
それでは、ブラウザでアクセスして、JSONが返却されるようにしましょう。

router作成

今回は、サンプルなので、GETでhttp://localhost:8080/usersにアクセスしたらJSON文字列が表示するようにするだけにします。

router.GET("/users", func(c *gin.Context) {
    var (
        users   []User
        items   []Item
        jsonMap map[string]interface{} = make(map[string]interface{})
    )

    db.Find(&users)

    for i, user := range users {
            db.Model(&user).Related(&items)
            user.Items = items
            users[i] = user
    }

    jsonMap["users"] = users
    c.JSON(200, jsonMap)
})
jsonMap map[string]interface{} = make(map[string]interface{})

変数jsonMapは、usersをキーとするようにmapした、interface型のmap変数となります。
http://localhost:8080/users でアクセスすると、以下のようなJSONが表示されると思います。

{
  "users": [
    {
      "id": 1,
      "name": "ユーザー1",
      "items": [
      {
        "id": 1, 
        "name": "ゲーム1", 
        "score": 5, 
        "user_id": 1
      }, 
      {
        "id": 2,
        "name": "ゲーム2", 
        "score": 0, 
        "user_id": 1
      }, 
      ...          
      ] 
    }, 
    {
      "id": 2,
      "name": "ユーザー1",
      "items": [
      {
        "id": 9, 
        "name": "ゲーム3", 
        "score": 1, 
        "user_id": 2
      }, 
      ...
      ] 
    },
    ...
  ]
}

よし完成!!
と、行きたいのですが。
もうちょっと何か欲しいですよね。。。

ということで、各ユーザーがゲームに対して評価点を付けていますので、それぞれのユーザーが誰と評価が似ているかのマッチングアルゴリズムを追加してみたいと思います。

マッチングアルゴリズム

あくまで、サンプルということなので、今回はマッチングアルゴリズムの中でも最も簡単な「ユークリッド距離の定義」を基にアルゴリズムの作成を行ってみたいと思います。

ユークリッド距離の定義

d(p,q)=d(q,p)=\sqrt{(q_1-p_1)^2+(q_2-p_2)^2+\cdots+(q_n+p_n)^2}

wikipedia - ユークリッド距離の定義

定義式を見ても簡単なのがわかりますね。

上記の定義式より更に1を加えることによって、0から1を返す式にします。
こうすることによって、より類似性の高いユーザーは1に近くなり、類似性の低いユーザーは0に近くなります。

distance := 1 / (1 + math.Sqrt(math.Pow(float64(score - score1, 2)));

こんな感じですかね。
では、実際に入れていきます。

userと、otherUserを引数とした、GetDistanceScore関数を作成します。
どちらも、User型を参照します。

func GetDistanceScore(user *User, otherUser *User) float64 {
    var matchItems map[string]float64 = make(map[string]float64)

    for _, item := range user.Items {
        for _, otherItem := range otherUser.Items {
            if item.Name == otherItem.Name {
                matchItems[item.Name] = math.Pow(float64(item.Score-otherItem.Score), 2)
            }
        }
    }

    if len(matchItems) == 0 {
        return 0
    }

    var sumOfSquares float64
    for _, matchItem := range matchItems {
        sumOfSquares += matchItem
    }

    return (1 / (1 + math.Sqrt(sumOfSquares)))
}

userと、otherUserがお互い持っているアイテムだけを計算したいので、user.Itemsをfor文で回します。
マッチするアイテムのみ計算し、

matchItems[item.Name] = math.Pow(float64(item.Score-otherItem.Score), 2)

それ以外は、0を返却。

if len(matchItems) == 0 {
    return 0
}

計算結果の和をmath.Sqrtし1を足し、さらに1を割ります。

return (1 / (1 + math.Sqrt(sumOfSquares)))

と、シンプルではありますがこれで、お互い同じアイテムを評価したものだけの近似値を算出することができます。(スコアリングの精度は高いものではありませんのであしからず。)
評価後の変数を保持したいので、MatchingUserの構造体も作成します。

type MatchingUser struct {
    ID    uint    `json:"id"`
    Name  string  `json:"name"`
    Score float64 `json:"score"`
}

routingは /users/:id と、idを受け取ったら該当するユーザーの情報の表示と、それ以外のユーザーの情報と、近似値を算出します。

router.GET("/users/:id", func(c *gin.Context) {
    var (
        user         User
        otherUsers   []User
        matchingUser []MatchingUser
        items        []Item
        jsonMap      map[string]interface{} = make(map[string]interface{})
    )

    db.First(&user, c.Param("id"))
    user = setUserItem(user, items, db)

    db.Not("id", c.Param("id")).Find(&otherUsers)
    for i, otherUser := range otherUsers {
        otherUsers[i] = setUserItem(otherUser, items, db)
    }

    for _, otherUser := range otherUsers {
        matchingUser = append(matchingUser, NewMatchingUser(otherUser, GetDistanceScore(&user, &otherUser)))
    }

    sort.Sort(sort.Reverse(ByScore(matchingUser)))

    jsonMap["user"] = user
    jsonMap["matching_users"] = matchingUser
    c.JSON(200, jsonMap)
})

idを受け取り、該当するuserを取得しております。

db.First(&user, c.Param("id"))

また、それ以外のユーザーを、otherUsers変数に入れております。

db.Not("id", c.Param("id")).Find(&otherUsers)
for i, otherUser := range otherUsers {
    otherUsers[i] = setUserItem(otherUser, items, db)
}

scoreが高いユーザー順に並び替えたいので、sortパッケージをimportし、ソートします。

import "sort"

scoreを元に、sortを適応したいので、ByScoreの型を定義し、Goのメソッドを定義します。
(メソッド使うところないかな。。と思っていたところあって良かったです。)

sort.Sort(sort.Reverse(ByScore(matchingUser)))
type ByScore []MatchingUser

func (a ByScore) Len() int           { return len(a) }
func (a ByScore) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
func (a ByScore) Less(i, j int) bool { return a[i].Score < a[j].Score }

Goの「メソッド(Method)」は、一般的なオブジェクト指向プログラム言語によくあるメソッド(関数)とは異なり、任意の型に特化した関数を定義する特徴的な機能です。

Methods、Sort Packageなどの詳細についてはこちらなどを参照頂ければと思います。
A Tour of Go - Methods continued
Package sort

順序としては、「降順」に並べ替えたいので、sort.Reverse() で順序を逆にしております。
それでは、http://localhost:8080/users/2 など、IDNumberを含めた、URLをアクセスしてみましょう。

{
  "user": {
    "id": 2,
    "name": "ユーザー2",
    "items": [
      {
        "id": 9, 
        "name": "ゲーム1", 
        "score": 1, 
        "user_id": 2
      }, 
      {
        "id": 10, 
        "name": "ゲーム2", 
        "score": 1, 
        "user_id": 2
      }, 
      {
        "id": 11, 
        "name": "ゲーム3", 
        "score": 3, 
        "user_id": 2
      },
      ...
    ]
  },
  "matching_users": [
    {
      "id": 1, 
      "name": "ユーザー1", 
      "score": 0.10886898139200682
    }, 
    {
      "id": 9, 
      "name": "ユーザー9", 
      "score": 0.1
    }, 
    {
      "id": 7, 
      "name": "ユーザー7", 
      "score": 0.08462632608958592
    }, 
    {
      "id": 4, 
      "name": "ユーザー4", 
      "score": 0.08270931562630669
    },
    ...
  ]
}

idを入力したユーザーと、その他のユーザーの近似値が高いユーザーがマッチングされたJSONが返却されれば成功です。

お疲れ様でした!

テスト

GoのTestは、標準パッケージとしてtestingが用意されていますので、至って簡単にTestを行うことができます。
関数名は、「Test関数名()」とTest用の関数を作成し、*testing.T型の引数を1つだけ取るように定義します。
testを実行するコマンドは、以下のコマンドとなります。

$ go test [build/test flags] [packages] [build/test flags & test binary flags]

それでは、先ほどのプログラムに対してTest関数を作っていきます。

package main

import "testing"

func TestNewUser(t *testing.T) {
    user := NewUser("test user", []string{ "test item 1", "test item 2" })

    if len(user.Items) != 2 {
        t.Error("Item数が異なる")
    }

    if user.Items[0].Name != "test item 1" {
        t.Fatalf("ItemNameが異なる", user.Items[0])
    }
}

テストを失敗させたい場合は、func (*T) Fail を使用するのですが、format構造や変数を渡してメッセージを表示させたい場合は、t.Errorや、t.Fatalf で、失敗処理を行います。

$ go test

を実行し、そっけないのですが、以下の様に表示すれば test成功です。

PASS
ok      _/Users/user名/development/package名  0.018s

オプション -vを付与することによって、実行した関数の表示が行えます。

=== RUN   TestNewUser
--- PASS: TestNewUser (0.00s)

その他、カバレッジ率など表示できますが、今回は割愛させていただきます。

ベンチマーク

Goでは、Test以外にも、標準パッケージでベンチマークを測れることも可能です。
Test同様に「Benchmark関数名()」の関数を作成し、*testing.B型の引数を1つだけ取るように定義します。
benchmarkを実行するコマンドは、go test コマンドにオプション-bench を付与して実行します。

例:

go test -bench GetDistanceScore

全て実行したい場合は . を付与することによって、Benchmark関数名()を全て実行できます。

go test -bench .

b.N でベンチマークを取りたい箇所をfor文で回します。
回数は、自動的に数値の取れる回数が設定されるようです。

func BenchmarkGetDistanceScore(b *testing.B) {
    var matchingUser []MatchingUser

    itemNames := []string{
        "ゲーム1",
        "ゲーム2",
        "ゲーム3",
        "ゲーム4",
        "ゲーム5",
        "ゲーム6",
        "ゲーム6",
        "ゲーム7",
    }

    userNames := []string{
        "ユーザー1",
        "ユーザー2",
        "ユーザー3",
        "ユーザー4",
        "ユーザー5",
        "ユーザー6",
        "ユーザー7",
        "ユーザー8",
        "ユーザー9",
        "ユーザー10",
    }

    user := NewUser("test user", itemNames)
    otherUsers := CreateUsers(userNames, itemNames)

    for i := 0; i < b.N; i++ {
        for _, otherUser := range otherUsers {
            matchingUser = append(matchingUser, NewMatchingUser(otherUser, GetDistanceScore(&user, &otherUser)))
        }
    }

    sort.Sort(sort.Reverse(ByScore(matchingUser)))
}

benchmarkを行った結果、100000回実行され、1回あたり 16176 ns(0.016176ミリ秒)の処理スピードでした。
クソはやいな。とか思うのですが、他の言語と比較してないので、早いのか遅いのかがそもそもわからないですw

BenchmarkGetDistanceScore-8   100000         16176 ns/op

最後に

と言った訳で、簡潔に端的に要点だけまとめ書くつもりだったのですが、そこそこのボリュームになってしまいましたw
当たり前ですが、これが全てではなく、Go言語の目玉の並行処理が行える、goroutinechannelなどもあるのですが、今回使用するところもなかったので(入れようとしたらできましたが)また追々、どこかで書ければと思っております。

恥ずかしながら2016年になって、ちゃんとGo言語を扱ったの(書き始めて1か月未満)ですが、(しばらく動的言語と勘違いしてましたw)
触った印象としましては、今までのプログラム言語の逆のアプローチをとり、無駄な機能は排除し言語仕様もコンパクトにまとまって言語が故に、書きやすく読みやすい、且つ静的型付言語なのでパフォーマンスも素晴らしい印象でした。
classもenum(iotaがありますが)もないのはビックリしましたが、すぐに慣れました。

まだまだ、歴史の浅い言語ではありますが、様々なパッケージが作成されてきたりと、着々と「使える言語」になってきている気がします。
有名なところでは、docker が採用したり、国内サービスでも重たい処理があるところが徐々にGo言語に置き換えていっている。と言う話を最近よく耳にし、着々と広がっていっているのを肌で感じます。

Go言語を触ったきっかけですが、「ここは重い処理になるからGoとかで書けないかな。。」と思ったのがきっかけでした。
個人趣味の週末開発で、コストをかけず、楽に実装したいとなるとフレームワーク周りは、こう言った感じになるのかな。。。(あくまで個人的見解です。)

go_layer.png

「サイト作るだけだからなぁ。あんまり関係ないかな。」って方は、静的サイトジェネレーターである「HUGO」を使ってみてはいかがでしょうか?
Go言語で作成された、静的サイトジェネレーターなのですが早さに感動してしまうかもしれません。

また、「型苦手なんですよね」って方は、「TypeScript」で慣れるも手かもしれませんね。一度、型を入れると型がないと不安になってしまうかもしれません。

と、今回簡単なマッチングアルゴリズムを入れたAPIサーバーのサンプルとして書いたのですが、
「Go触りたいけど、どこからやれば。。」
と思っている方に何か参考になればこれ幸いです。

今回のデモソースに関しては、こちらに公開しておりますので、参考にして頂ければ幸いです。
Github サンプルソース

ではでは。

cocone_inc
アバターアプリ『ポケコロ』や『猫のニャッホ』をはじめとした、1,600万人の女性に愛されるサービス開発
https://www.cocone.co.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away