20190305のGoに関する記事は9件です。

go言語でhtmlから取得したデータをjson形式で保存する

Goの勉強録

昨日あたりからGoに対するエンジニアの成長曲線の傾きが正に大きい部分に入ったのでは?と思うくらいGoを書くのが開発環境構築したてのころと比べて苦じゃ無くなりました。以前はエラーが出ると何処エラーしてるんやとか、うーんわからんみたいな感情があったのですが、結構早くエラーにならないように記述する事ができるようになりました。(潜在的なバグなどはわかりませんけれど。。)
昨日JavaScriptから送られて来るリクエストからデータを取得してコンソールに出力することができ、取得したこのデータを煮るなり焼くなりと言ったのでまずは手始めに、JSON形式で外部ファイルに保存しようと思いました。
あっさりJSONファイルにデータを保存することができたのでブログに残しておきます。

ちなみに過去のGo勉強録記事は下から飛べるようにしています。

過去の記事

本題

go言語はPHPやruby on rails、Node.js(サーバサイド)と言った言語と同じようにサーバサイドのプログラムを記述することができます。サーバサイドのプログラムを記述できるということは、サーバから必要なデータを引っ張ってきたり、データを書き込んだりするのが主な動作になります。
そこで今回はデータを書き込むという如何にもサーバサイドの動作を実装しました。

ちなみにワークスペースの階層は以下のようになっています。Dataの中にdata.jsonが作られるようにします。(新幹線で作業をしたのでAngularのフレームワークをHTMLの中にぶっこんでいます。)
昨日のものを少しカスタマイズした感じですね。

workspace/
  ├ Data/ 
  │
  ├ HTML/
  │    ├ index.html
  │    ├ test.js
  │    ├ Angular.min.js
  │
  ├ POST/
  │    ├ Process.go
  │
  ├ Host.go

ソースコード

Host.go
サーバを立てて、来るリクエストに対してデータを取得し、jsonに書き込むという処理が記述されています。

Host.go
package main

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

    "./Post"
)

func main() {
    HostingServer()
}

// Json形式でデータを保存するようにHandleを定義
func SaveJson() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        // ファイルの生成
        file, err := os.Create("./Data/data.json")
        if err != nil {
            fmt.Println("ファイルの生成に失敗しました")
        }
        defer file.Close()
        // Process.goからGetDataを呼び出し、リクエストのデータをData形式で取得する
        var data = Post.GetData(w, r)
        // 出力用にData形式のdataをjson形式にencode
        encodedjson, _ := json.Marshal(&data)
        // 正常に生成されたファイルに書き込み
        file.Write(([]byte)(string(encodedjson)))
        // エンコードされたデータをコンソールに表示
        fmt.Println(string(encodedjson))
    })
}

// サーバを立てる処理
func HostingServer() {
    // Handlerの追加
    SaveJson()
    dir, _ := os.Getwd()
    // サーバを立てる為にstripprefixを設定
    fileserver := http.FileServer(http.Dir(dir + "/HTML/"))
    stripprefix := http.StripPrefix("/datatest/", fileserver)
    http.Handle("/datatest/", stripprefix)
    // serve :8080
    log.Fatal(http.ListenAndServe(":8080", nil))
}

Process.go
リクエストのデータをData形式で取得し、それを返す処理をしています。

Process.go
package Post

import (
    "encoding/json"
    "fmt"
    "net/http"
    "reflect"
)

type Data struct {
    Name   string
    Age    int
    Gender string
}

func GetData(w http.ResponseWriter, r *http.Request) Data {
    // parse
    r.ParseForm()
    var data = Data{}
    decoder := json.NewDecoder(r.Body)
    decoder.Decode(&data)
    fmt.Println(reflect.TypeOf(data))
    return data
}

index.html
表示用のスクリプト

index.html
<!DOCTYPE html>
<html ng-app="idx">
    <head>
        <title>FileIO</title>
        <script src="./Angular.min.js"></script>
        <script src="./test.js"></script>
    </head>
    <body ng-controller="form">
        <input type="text" ng-model="name">
        <br />
        <input type="number" ng-model="age">
        <br />
        男:<input type="radio" ng-model="gender" value="man">
        <br />
        女:<input type="radio" ng-model="gender" value="woman">
        <br />
        <input type="button" ng-click="req()" value="送信">
    </body>
</html>

test.js
index.htmlでボタンを押された時に呼び出すイベントリスナーを定義しています。

test.js
var idx = angular.module("idx",[])

idx.controller("form",function($scope,$http){
    $scope.req = function(){
        // 送信用のデータ
        data = {
            name: $scope.name,
            age: $scope.age,
            gender: $scope.gender
        }
        // POSTでhttp通信
        $http({
            method: 'POST',
            url: '../Host.go',
            contentType: 'application/JSON',
            data: data
        }).then(function DoneCallback(res){
          console.log("通信成功");
          console.log(res)
        },function failCallback(res){
            console.log("通信失敗");
        });
    }
})

go run Host.goを実行し、http://localhost:8080/datatest/にアクセスすると
簡易的な入力フォームができています。
そこに値を入力しボタンを押すとData/data.jsonが生成され、中身は

data.json
{"Name":"test","Age":20,"Gender":"woman"}

とJSON形式で保存されます。

まとめ

go側で無事データをJSON形式で保存する事ができました。(ようやく面白くなってきたぞ(-ω- )o)
次はファイルの読み込みなどを書くと思います。後々DBなどの接続や、並列処理と言った所の記事がかけていけたらなと思っています!(勉強することは山積みですな)
まだまだ、go初心者なので間違っている部分があれば、気軽にコメントを描いてください!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GO言語 Macインストールメモ

参考URL

公式サイト:https://golang.org

環境

macOS High Sierra バージョン10.13.6
GO 1.12

インストール

$ brew install go
==> Downloading https://homebrew.bintray.com/bottles/go-1.12.high_sierra.bottle.tar.gz
######################################################################## 100.0%
==> Pouring go-1.12.high_sierra.bottle.tar.gz
�  /usr/local/Cellar/go/1.12: 9,787 files, 449.7MB
brew install go  35.23s user 41.35s system 53% cpu 2:23.62 total

$ go version
go version go1.12 darwin/amd64

動作確認

公式サイトのソースを動かしてみる

$vi hello.go
// You can edit this code!
// Click here and start typing.
package main
import "fmt"

func main() {
    fmt.Println("Hello, 世界")
}

$go run hello.go
Hello, 世界

ビルド

$go build hello.go
$ls
hello         hello.go
$./hello
Hello, 世界
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Go初心者が書くarXiv APIを使って論文リストから論文を取ってくるアプリ

はじめに

自分の分野の有名な学会が開かれました.Accepted Paperのページがあります.とりあえずタイトルだけ見て,面白そうなやつ/自分の立場を脅かしようなやつをチェックしなきゃ...
みたいなことになる人もいるかもしれません.(まぁ普段からアンテナを貼っておけばこんなことにはならないのかもしれませんが,僕みたいにSNSが嫌いな人はこうなります)
とりあえず,タイトルで検索してarXivでダウンロードしてくるわけですが,ポチポチするのは面倒なので途中でやめてしまいます.そこでAccepted Paperのリストからほしい論文のタイトルだけコピペ(全部ブラウザから調べるよりは多少まし)して,タイトルリストからダウンロードしてくれるアプリがあったらいいな,ということで作りました.
僕のモチベーションはあくまでGoの勉強です.

何を作ったの?

dpxgoというクロスプラットホーム CLIアプリ (コマンド) を作りました.
リストにある論文のアブストラクトとpdfをarXiv APIをつかってダウンロードします.
arXivにない場合は自分で調べてください.
ちなみにdpxgoDownload Papers from arXiv with GO app.の略です.

特徴

  • Windows, mac, Linuxに対応しています.(Windowsとmacは動作未確認)
  • 実行ファイルで配布しているので,ダウンロードすればすぐに利用できます
  • アブストラクトだけの保存も可能です.
  • 関連研究のサーベイをするときに便利かもしれません.
  • Goroutineによって並列処理しているので,ほしい論文がたくさんあっても,多分ある程度速くダウンロードできます.

似たようなの

好きなのを使ってください.みんな素晴らしいと思います.

どう使うの?

まず,gitlabからダウンロードしてください.

使用例

  • dpxgo paperlist : paperlistにある論文(アブストラクトとpdf)をダウンロード
  • dpxgo -d out/ paperlist: 保存先ディレクトリの指定
  • dpxgo -w 20 paperlist: 並列数(Goroutine)の指定
  • dpxgo -a paperlist: アブストラクトだけ取ってくる.

paperlistの書き方は以下の通りです.Plain textならファイル名は何でもいいです(多分).

TITLE of PAPER1
TITLE of PAPER2
TITLE of PAPER3

TITLE of PAPER4

タイトルごとに改行で区切ってください.空の行があっても読み飛ばします.

オプション

  • -d: 保存先の指定 (default ./)
  • -w: Goroutineの並列数 (default 10)
  • -a: アブストラクトのみ保存 (default false)

Goコード

全文は,gitlabを参照してください.特に大事そうな部分を説明します.

メイン関数

func main() {
    // parse command line variables
    parseArg()

    // download papers with ?Goroutine?
    ch := make(chan string)
    wg := sync.WaitGroup{}
    // make workers
    for i:=0; i<NumWorkers; i++{
        wg.Add(1)
        go downloadPapers(&wg, ch)
    }

    paperTitles := parsePaperList(ListPath) 
    for _, fileName := range paperTitles {
        ch <- fileName
    }
    close(ch)
    wg.Wait()
}

並列化の方法は,ここを参考にしています.

コマンドライン引数のパース

func parseArg() {
    targetDir  := flag.String("d", "./", "directory path to save")
    numWorkers := flag.Int("w", 10, "number of workers")
    onlyAbst   := flag.Bool("a", false, "only save abstract")
    flag.Parse()
    TargetDir   = *targetDir
    NumWorkers  = *numWorkers
    OnlyAbst    = *onlyAbst

    // is TargetDir exist?
    if _, err := os.Stat(TargetDir); os.IsNotExist(err) {
        panic(TargetDir+" is not exist")
    }

    endStr := string(TargetDir[len(TargetDir)-1])
    if endStr != "/" {
        TargetDir = TargetDir+"/"
    }

    args := flag.Args()
    if flag.NArg() == 0 {
        fmt.Println("dpxgo [options] list")
        return
    } else {
        // it does not support multiple paper lists
        ListPath = args[0]
    }
}

Goではflagによってコマンドライン引数をパースするみたい(これ以外にもやり方はある)ですが,この処理をfunc main()でやるとなんか嫌なので,関数にしてまとめて,コマンドライン引数で扱いたい変数は全てグローバル変数にしました.
すこし,苦戦したのがflag.Parse()の位置です.flag.Type関数のすぐあとで行うべきです.つまり,グローバル変数に代入した後でもエラーにはならないのですが,コマンドライン引数がすべてdefaultのものになります.

arXiv APIの使い方

UrlHead   = "http://export.arxiv.org/api/query?search_query="
UrlFoot   = "&start=0&max_results=1" // toriaezu 1 ken dake
strQuery := url.QueryEscape(paperTitle)
resp, _  := http.Get(UrlHead+strQuery+UrlFoot)
defer resp.Body.Close()
html, _  := ioutil.ReadAll(resp.Body)

downloadPapers (*wg, ch)の中のこの4行の部分でarXiv APIを利用しています.UrlFootmax_resultsを変更するとたくさんの論文がヒットして,キーワードで検索したいときは便利だと思います.でも今回の目的はタイトルリストからの論文取得なので,結果件数は1件で十分です.
ここでhtml[]byte型の変数です.ここから必要な情報をパースしていきます.ちなみにstring型に変換すると普通のhtml形式の文字列です.

HTMLのタグの中身の取得

func isTagExist(html []byte, tagStr string) (bool) {
    regStr  := `<`+tagStr+`>[\s\S]*</`+tagStr+`>`
    regTag  := regexp.MustCompile(regStr)
    return regTag.Match(html)
}

func fetchTag(html []byte, tagStr string) ([]byte) {
    // <tag>[\s\S]</tag>
    regStr  := `<`+tagStr+`>[\s\S]*</`+tagStr+`>`
    regTag  := regexp.MustCompile(regStr)
    content := regTag.FindSubmatch(html)[0]
    content  = bytes.Replace(content, []byte("<"+tagStr+">"),  []byte(""), -1)
    content  = bytes.Replace(content, []byte("</"+tagStr+">"), []byte(""), -1)
    return content
}

// short version
func isTagExist(html []byte, tagStr string) (bool) {
    return regexp.MustCompile(`<`+tagStr+`>[\s\S]*</`+tagStr+`>`).Match(html)
}

func fetchTag(html []byte, tagStr string) ([]byte) {
    content := regexp.MustCompile(`<`+tagStr+`>[\s\S]*</`+tagStr+`>`).FindSubmatch(html)[0]
    return regexp.MustCompile(`<`+tagStr+`>|</`+tagStr+`>`).ReplaceAll(content, []byte(``)) 
}

この2つの関数を実装しました.Goのライブラリにgoqueryというものがあり,これが便利らしいのですが,この存在に気づいたのは書いている途中だったため,見なかったことにしました.
isTagExistではhtml形式の[]byte型変数がtagStrタグを含むかどうかをチェックします.arXiv APIでは,検索がヒットしたとき,<entry>タグがつくので,この関数で,arXivに論文が投稿されているかどうかをチェックします.
fetchTagではtagStrタグの中身を取ってきます(タグは含まない).論文のタイトルやアブストラクトは先に取得したhtml変数に含まれている(もちろん検索がヒットした場合に限る)ので,この関数で取ってきます.
ただし,このfetchTag関数ではタグが入れ子になっている場合は,おそらく思った通りに動作しません.しかしながら,arXiv APIのレスポンスにはそのような構造はありませんのでこれで十分です.

タイトルの一致判定

func matchTitle(titlePred string, titleTrue string) (bool) {
    sp := strings.Split(titlePred, " ")
    st := strings.Split(titleTrue, " ")

    matchNum := 0.0
    for _, tp := range  sp {
        for _, tt := range st {
            if tp == tt {
                matchNum += 1.0
                break
            }
        }
    }
    if  threshold:=0.7; matchNum/float64(len(st))>threshold {
        return true
    } else {
        return false
    }
}

すこし使って気づいたのですが,arXiv APIの検索は,ある程度タイトルが一致している論文を提示するため,結構違う論文がダウンロードされることがあったので,タイトルの一致判定を行う関数を追加しました.これは,以下の式でタイトルの類似性を測っています(類似性が大きいほど似ている).

\text{similarity} = \frac{|\text{{検索されたタイトルの単語集合}}\cap\text{{指定したタイトルの単語集合}}|}{|\text{{指定したタイトルの単語集合}}|}

まぁ,Recallというか,コサイン距離っていうやつですかね.

pdfファイルの保存

// save pdf (error ハンドリングは省略)
urlPdf   := fetchTag(fetchTag(html, `entry`), `id`)
urlPdf    = bytes.Replace(urlPdf, []byte("abs"), []byte("pdf"), 1)
urlPdf    = bytes.Replace(urlPdf, []byte("http"), []byte("https"), 1)
resp, err = http.Get(string(urlPdf)+".pdf")
defer resp.Body.Close()
pdf, _ := ioutil.ReadAll(resp.Body)
err     = ioutil.WriteFile(TargetDir+string(title)+".pdf", pdf, 0666)

pdfの保存はhtmlの取得と同じ要領でhttp.Get()を使えばできます.

おわりに

とりあえず,動いてかつ自己満足にひたれるものができました.
一番苦戦したのはタグのパターンマッチを行うための正規表現でした(普段正規表現をまったくつかないため).

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

golangのembeddingによる継承(っぽい操作)で今更ながら思ったこと

今更ながら、こういう仕様だったのか・・・とハマったのでメモ。

goってextendがない代わりにembeddingによる継承っぽいことができますよね。

その際の継承先structの初期化で、 えっ!?この操作できなかったんだ・・・ と今更ながら知ったのでメモです。

まずエラーになるパターン

package main

import (
    "fmt"
)

// 基のstruct。NameとDescriptionを持っている
type Base struct {
    Name        string
    Description string
}

// 拡張?したstruct。ExtraInformationというプロパティを足した
type Extended struct {
    Base
    ExtraInformation string
}

func main() {
    ext := Extended{
        Name:             "name",
        Description:      "description",
        ExtraInformation: "extra",
    }
    fmt.Printf("Struct: %#v", ext)
}

これだと、 cannot use promoted field Base.Description in struct literal of type Extended というエラーになります。

embeddingされている方のフィールドを promoted field と呼ぶようですね。

うまくいくパターン

こうしないと駄目らしい。

package main

import (
    "fmt"
)

// 基のstruct。NameとDescriptionを持っている
type Base struct {
    Name        string
    Description string
}

// 拡張?したstruct。ExtraInformationというプロパティを足した
type Extended struct {
    Base
    ExtraInformation string
}

func main() {
    ext := Extended{
        Base: Base{
            Name:        "name",
            Description: "description",
        },
        ExtraInformation: "extra",
    }
    fmt.Printf("Struct: %#v", ext)
}

もしくはこう。struct literal なるものから脱出さえしていればどうにでもなる系。

package main

import (
    "fmt"
)

type Base struct {
    Name        string
    Description string
}

type Extended struct {
    Base
    ExtraInformation string
}

func main() {
    ext := Extended{}
    ext.Name = "name"
    ext.Description = "description"
    ext.ExtraInformation = "extra"

    fmt.Printf("Struct: %#v", ext)
}

普通にPythonとかで書いている人にとっては意外な仕様だったのでメモしておきました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

cgo で構造体配列を引数に取る C 関数を呼び出す方法の一例 (Go pointer to Go pointer エラー回避方法)

環境

  • go: 1.12 linux/amd64

問題の概要

C では関数の引数として構造体を渡す場合, 通常ポインタで渡す. この構造体のメンバーの中にポインタ変数が含まれている場合に cgo でこの関数を何も考えずに直接呼び出すと “Go pointer to Go pointer” エラーが発生する.

具体的な例を挙げてみる. 例えば下記のような C のコードがあったとして, cgo から printMyStructs() 関数を呼び出したいとする.

#include <stdlib.h>
#include <stdio.h>
#include <string.h>

typedef struct mystruct {
    int  i;
    int* pi;
} mystruct;

/* cgo から呼び出したい関数 */
void printMyStructs(int num, mystruct* strctList) {
    int j;
    for(j = 0; j < num; j++) {
        printf("%d: i=%d, pi=%d\n", j, strctList[j].i, *(strctList[j].pi));
    }
}

このとき, GO 側のコードは何も考えずに書くと例えば下記のようになるのだが...

    length := 10
    strctArray := make([]C.mystruct, length)
    // building strctArray
    pi_ := C.int(1000)
    strctArray[0].i  = C.int(10)
    strctArray[0].pi = (*C.int)(unsafe.Pointer(&pi_))
    ....
    C.printMyStructs(C.int(length), (*C.mystruct)(unsafe.Pointer(&strctArray[0])))

これをコンパイルすると, 末尾の C.printMyStructs() を呼び出す箇所で下記のような Go pointer to Go pointer エラーとなる.

$ go run cstructarray.go
panic: runtime error: cgo argument has Go pointer to Go pointer

goroutine 1 [running]:
main.main.func1(0x1, 0xc000074010, 0x1, 0x1)
        /home/endoh/go/src/test/cstructarray.go:31 +0x52
main.main()
        /home/endoh/go/src/test/cstructarray.go:31 +0xa3
exit status 2

構造体単体で渡す場合ならばポインタ渡しを諦めて実体渡しができるようなラッパー関数を C 側で用意するという手もあるが, 構造体の配列を渡す場合はそうもいかない.

解決方法の一例

C 側にヘルパー関数を 2 つ用意して, Go からはこれらを使いつつ最終的に目的の C 関数を呼び出すという手順を取った.

C 側の準備

C 側には次の 2 つの関数を準備する.

(1) 構造体のメモリ確保を行ってポインタを返す関数

mystruct* allocMystruct(int num) {
    return malloc(num * sizeof(mystruct));
}

(2) 構造体配列の任意の要素に引数から渡された構造体をコピーする関数

/* offset が配列の何個目かを表す */
void convertMystructData(mystruct* strctArray, int offset, mystruct src) {
    memcpy(strctArray + offset, &src, sizeof(mystruct));
}

Go 側の手順

Go 側では次のような手順を踏む.
1. make で C.mystruct の配列を作成
2. 1 で作成した配列の各要素に値を入れる
3. C の「構造体のメモリ確保を行ってポインタを返す関数」を呼び出す
4. C の「構造体配列の任意の要素に引数から渡された構造体をコピーする関数」を上記 2 と 3 で得た変数を引数として呼び出す

3 で得たポインタは C ポインタなので構造体のメンバーに Go ポインタが含まれていても Go pointer to Go pointer エラーは発生しない. やっていることは実体渡しと同様, 構造体配列をせっせとコピーしているので全くもってスマートとは言えないが, とりあえずこれで動作する.

  • go側のコード
    length := 10
    strctArray := make([]C.mystruct, length) // これは GO pointer
    // building strctArray
    pi_ := C.int(1000)
    strctArray[0].i  = C.int(10)
    strctArray[0].pi = (*C.int)(unsafe.Pointer(&pi_))
    ...
    cStrctArray := C.allocMystruct(C.int(length)) // こっちは C pointer
    defer C.free(unsafe.Pointer(cStrctArray))
    for i, strct := range strctArray {
        C.convertMystructData(cStrctArray, C.int(i), strct)
    }
    C.printMyStructs(C.int(length), cStrctArray)

実行結果は下記の通り. 問題なく動いている模様.

$ go run cstructarray.go
0: i=10, pi=1000
(以下略)

コード全体

コード全体も載せておきます.

cstructarray.go
package main

//#include <stdlib.h>
//#include <stdio.h>
//#include <string.h>
//
//typedef struct mystruct {
//    int  i;
//    int* pi;
//} mystruct;
//
//void printMyStructs(int num, mystruct* strctList) {
//    int j;
//    for(j = 0; j < num; j++) {
//        printf("%d: i=%d, pi=%d\n", j, strctList[j].i, *(strctList[j].pi));
//    }
//}
//
//mystruct* allocMystruct(int num) {
//    return malloc(num * sizeof(mystruct));
//}
//
//void convertMystructData(mystruct* strctArray, int offset, mystruct src) {
//    memcpy(strctArray + offset, &src, sizeof(mystruct));
//}
//
import "C"

import (
    "unsafe"
)

func main() {
    length := 1
    strctArray := make([]C.mystruct, length)
    pi_ := C.int(1000)
    strctArray[0].i  = C.int(10)
    strctArray[0].pi = (*C.int)(unsafe.Pointer(&pi_))
    cStrctArray := C.allocMystruct(C.int(length))
    defer C.free(unsafe.Pointer(cStrctArray))
    for i, strct := range strctArray {
        C.convertMystructData(cStrctArray, C.int(i), strct)
    }
    C.printMyStructs(C.int(length), cStrctArray)
}

感想

cgo つらい

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Go言語のモック(gomock)を触ってみた

はじめに

Goでモックを使ったテストを書く際によく使用されるgomockを触ってみました。
モックとは何かなどの説明はこの記事では対象外です。

gomockとは

https://github.com/golang/mock

Goのモックライブラリ。
モックにできるのはインターフェースとそのメソッドのみであり、なんでもモックにできるわけではない。
testing(標準パッケージ)と組み合わせて使用するケースが多い。

インストール

$ go get github.com/golang/mock/gomock
$ go install github.com/golang/mock/mockgen

使ってみる

モックにされる対象のインターフェース

今回は、Barというメソッドを持つFooインターフェースを例にします。

foo.go
type Foo interface {
    Bar(x int) int
}

モックを自動生成する

mockgenコマンドでモックファイルを自動生成します。
-sourceオプションでモック対象のファイルを指定し、-destinationオプションでモックファイルの出力先を指定します。
mockgenコマンドには他にも色々オプションがあるみたいです。詳しくはこちら
mockファイルは mock_<package_name>/ ディレクトリ配下に mock_<package_name>.go という名前で作るのが通例ぽいです。

それでは作っていきましょう。

$ mkdir mock_foo
$ mockgen -source foo.go -destination mock_foo/mock_foo.go

生成されたファイルが以下です。

mock_foo/mock_foo.go
// Code generated by MockGen. DO NOT EDIT.
// Source: foo.go

// Package mock_foo is a generated GoMock package.
package mock_foo

import (
    gomock "github.com/golang/mock/gomock"
    reflect "reflect"
)

// MockFoo is a mock of Foo interface
type MockFoo struct {
    ctrl     *gomock.Controller
    recorder *MockFooMockRecorder
}

// MockFooMockRecorder is the mock recorder for MockFoo
type MockFooMockRecorder struct {
    mock *MockFoo
}

// NewMockFoo creates a new mock instance
func NewMockFoo(ctrl *gomock.Controller) *MockFoo {
    mock := &MockFoo{ctrl: ctrl}
    mock.recorder = &MockFooMockRecorder{mock}
    return mock
}

// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockFoo) EXPECT() *MockFooMockRecorder {
    return m.recorder
}

// Bar mocks base method
func (m *MockFoo) Bar(x int) int {
    m.ctrl.T.Helper()
    ret := m.ctrl.Call(m, "Bar", x)
    ret0, _ := ret[0].(int)
    return ret0
}

// Bar indicates an expected call of Bar
func (mr *MockFooMockRecorder) Bar(x interface{}) *gomock.Call {
    mr.mock.ctrl.T.Helper()
    return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Bar", reflect.TypeOf((*MockFoo)(nil).Bar), x)
}

自動生成されたファイルの中身を全て理解するのは難しそうですが、大体いつも同じなので、使い方さえわかっていればとりあえずよさそう。

テストファイル

モックを使う簡単な例を示します。
実際にはモックを呼び出すことが目的ではなく、あくまでモックによって外部的要因を固定化して、本当にテストしたい部分をテストする使い方になるはずですが、今回はgomockを理解するためだけなのでご理解ください。

foo_test.go
func TestFoo(t *testing.T) {
    // コントローラーの生成
    ctrl := gomock.NewController(t)

    defer ctrl.Finish()

    // モックの生成
    m := mock_foo.NewMockFoo(ctrl)

    // モックの対象メソッドと引数と返り値を設定する
    m.
        EXPECT().
        Bar(99).
        Return(101)

    // モックの呼び出し
    m.Bar(99)
}

上記テストファイルを実行すると以下の実行結果になります。

成功結果
ok      study/mock  (cached)
Success: Tests passed.

例えば、 m.Bar(100) でモックに設定していない引数で実行すると以下のように失敗します。

失敗結果
--- FAIL: TestFoo (0.00s)
    /Users/XXX/go/src/study/mock/foo_test.go:25: Unexpected call to *mock_foo.MockFoo.Bar([100]) at /Users/XXX/go/src/study/mock/mock_foo/mock_foo.go:38 because: 
        Expected call at /Users/XXX/go/src/study/mock/foo_test.go:22 doesn't match the argument at index 0.
        Got: 100
        Want: is equal to 99
    asm_amd64.s:522: missing call(s) to *mock_foo.MockFoo.Bar(is equal to 99) /Users/XXX/go/src/study/mock/foo_test.go:22
    asm_amd64.s:522: aborting test due to missing call(s)
FAIL
FAIL    study/mock  0.007s
Error: Tests failed.

参考

https://qiita.com/tenntenn/items/24fc34ec0c31f6474e6d

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Go言語のDBモック(sqlmock)を分かりやすくまとめてみた

はじめに

Go言語のDBモックライブラリ(sqlmock)が体系的にまとまっている日本語ドキュメントがほとんどないようなのでまとめました。

gopher君.png

sqlmockとは

本物のDBの代わりにSQLドライバのような振る舞いをしてくれるモックのライブラリ。
テスト時にわざわざ本物のDBを使う必要がなくなるため、「DBに入っているデータをバックアップ→テストに必要な前提データを挿入→テスト終わったらバックアップしたデータを元に戻す」みたいなことが必要なくなる。
実際にデータが挿入されたり検索されたりするわけではなく、SQLドライバの返り値にのみ注目して正常性を確認しているというところを理解するのがポイント。
ちなみに、もし実際のデータ処理の正常性まで見たいなら、テスト用DBをコンテナなりVMなりで立てて、テストデータをパターンごとにたくさん用意してテストを書くことになるでしょう。

インストール

$ go get github.com/DATA-DOG/go-sqlmock

いきなり具体例

公式サイトの具体例です。
この後、sqlmockの各関数などを解説します。

package main

import "database/sql"

// テスト対象関数。関数内ではUPDATE文とINSERT文を実行している。
func recordStats(db *sql.DB, userID, productID int64) (err error) {
    tx, err := db.Begin()
    if err != nil {
        return
    }

    defer func() {
        switch err {
        case nil:
            err = tx.Commit()
        default:
            tx.Rollback()
        }
    }()

    if _, err = tx.Exec("UPDATE products SET views = views + 1"); err != nil {
        return
    }
    if _, err = tx.Exec("INSERT INTO product_viewers (user_id, product_id) VALUES (?, ?)", userID, productID); err != nil {
        return
    }
    return
}

肝心のテストファイルがこちら

package main

import (
    "fmt"
    "testing"

    "github.com/DATA-DOG/go-sqlmock"
)

// 正常系
func TestShouldUpdateStats(t *testing.T) {
    // DBモック用意
    db, mock, err := sqlmock.New()
    if err != nil {
        t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
    }
    defer db.Close()
    mock.ExpectBegin()
    mock.ExpectExec("UPDATE products").WillReturnResult(sqlmock.NewResult(1, 1))
    mock.ExpectExec("INSERT INTO product_viewers").WithArgs(2, 3).WillReturnResult(sqlmock.NewResult(1, 1))
    mock.ExpectCommit()

    // モック化されたDBを用いてテスト対象関数を実行
    if err = recordStats(db, 2, 3); err != nil {
        t.Errorf("error was not expected while updating stats: %s", err)
    }

    // 使用されたモックDBが期待通りの値を持っているかを検証
    if err := mock.ExpectationsWereMet(); err != nil {
        t.Errorf("there were unfulfilled expectations: %s", err)
    }
}

// 異常系
func TestShouldRollbackStatUpdatesOnFailure(t *testing.T) {
    // DBモック用意
    db, mock, err := sqlmock.New()
    if err != nil {
        t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
    }
    defer db.Close()
    mock.ExpectBegin()
    mock.ExpectExec("UPDATE products").WillReturnResult(sqlmock.NewResult(1, 1))
    mock.ExpectExec("INSERT INTO product_viewers").
        WithArgs(2, 3).
        WillReturnError(fmt.Errorf("some error"))
    mock.ExpectRollback()

    // モック化されたDBを用いてテスト対象関数を実行
    if err = recordStats(db, 2, 3); err == nil {
        t.Errorf("was expecting an error, but there was none")
    }

    // 期待通りの返り値かを照合
    if err := mock.ExpectationsWereMet(); err != nil {
        t.Errorf("there were unfulfilled expectations: %s", err)
    }
}

解説

上記の具体例で使用されているsqlmock特有の関数やメソッドについて簡単に説明します。

New

New関数で空のモックを生成する。
戻り値の1つ目(db)はモックDB接続、戻り値の2つ目(mock)はモック設定を行うために使用する。

db, mock, err := sqlmock.New()

ExpectBegin

ExpectBegin関数でモックの設定を開始する。

mock.ExpectBegin()

ExpectExec

SQLクエリがUPDATE、INSERT、DELETE文の時はExpectExec関数を使用する。
ExpectExec関数の引数に想定するSQLクエリを指定する。
WillReturnResultメソッドの引数にSQLドライバとしての返り値をモックに設定する。
NewResult関数で引数に指定した値で返るように設定する。第一引数に主キーの自動生成ID、第二引数はSQLクエリによって影響を受けるカラムの数を指定する。

mock.ExpectExec("UPDATE products").WillReturnResult(sqlmock.NewResult(1, 1))

ExpectCommit

ExpectCommit関数でDBがコミットされるように設定する。

mock.ExpectCommit()

ExpectRollback

ExpectRollback関数で、DBをコミットしようとした際にロールバックされるように設定。

mock.ExpectRollback()

ExpectationsWereMet

ExpectationsWereMet関数で、モックDBが利用された後に、モックDBが期待通りに動作していたかを確認する。

if err := mock.ExpectationsWereMet(); err != nil {
        t.Errorf("there were unfulfilled expectations: %s", err)
    }

上記例にないけどよく使うやつ

ExpectQuery

SQLクエリがSELECT文の時はExpectQuery関数を使用する。
ExpectQuery関数の引数に想定するSQLクエリを指定する。
WithArgs関数の引数に想定するクエリパラメータの値を指定する。
WillReturnRowsメソッドの引数にNewRows関数で作成した想定する返り値の構造体を指定する。
AddRowメソッドの引数に想定するカラムに存在する値を指定する。

mock.ExpectQuery("SELECT * FROM students WHERE id = ?").
    WithArgs("1").
    WillReturnRows(NewRows([]string{"id", "name"}).AddRow("1", "james"))

さいごに

まだあれば追記していきます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Go言語のバリデーションチェックライブラリ(ozzo-validation)を分かりやすくまとめてみた

はじめに

APIサーバーの開発ではリクエストパラメータのバリデーションチェックは必須です。
Go言語ではバリデーションチェックのためのライブラリとして、 go-playground/validatorgo-ozzo/ozzo-validationが有名なようです。
この記事では、go-ozzo/ozzo-validationについてまとめます。
go-playground/validatorに関しては、こちらの記事が分かりやすかったです。

ozzo-validationとは

Go言語のバリデーション用ライブラリ。
go-playground/validatorよりは知名度が劣るものの、openapiとの親和性が良いためそこでの優位性があります。

インストール

$ go get github.com/go-ozzo/ozzo-validation
$ go get github.com/go-ozzo/ozzo-validation/is

いきなり具体例とその解説

変数の例

stringの変数をバリデーションする最もシンプルな例です。

package main

import (
    "fmt"

    validation "github.com/go-ozzo/ozzo-validation"
    "github.com/go-ozzo/ozzo-validation/is"
)

func main() {
    data := "example"
    err := validation.Validate(data,
        validation.Required,       // 空を許容しない
        validation.Length(5, 100), // 長さが5から100まで
        is.URL,                    // URL形式のみ
    )
    fmt.Println(err)
}
実行結果
$ go run main.go
must be a valid URL

Validate 関数でバリデーション実行する。第一引数に対象の変数を指定し、第二引数以降にルールを指定する。バリデーションを全て成功すればnilを返し、1つでも失敗すればエラーを返す。
ルールについては後述します。
上記の例では、変数dataがURL形式ではないので、バリデーションエラーとなっています。

構造体の例

実際には使用頻度は構造体のバリデーションの方が高いと思います。

package main

import (
    "fmt"
    "regexp"

    validation "github.com/go-ozzo/ozzo-validation"
)

type Address struct {
    Street string
    City   string
    State  string
    Zip    string
}

func (a Address) Validate() error {
    return validation.ValidateStruct(&a,
        // Streetは空を許容せず、5から50までの長さ
        validation.Field(&a.Street, validation.Required, validation.Length(5, 50)),
        // Cityは空を許容せず、5から50までの長さ
        validation.Field(&a.City, validation.Required, validation.Length(5, 50)),
        // Stateは空を許容せず、大文字2つの文字列
        validation.Field(&a.State, validation.Required, validation.Match(regexp.MustCompile("^[A-Z]{2}$"))),
        // Stateは空を許容せず、数字5つの文字列
        validation.Field(&a.Zip, validation.Required, validation.Match(regexp.MustCompile("^[0-9]{5}$"))),
    )
}

func main() {
    a := Address{
        Street: "123",
        City:   "Unknown",
        State:  "Virginia",
        Zip:    "12345",
    }

    err := a.Validate()
    fmt.Println(err)
}
実行結果
$ go run main.go
State: must be in a valid format; Street: the length must be between 5 and 50.

ValidateStruct 関数で構造体のバリデーションを行う。バリデーションを全て成功すればnilを返し、1つでも失敗すればエラーを返す。
Field 関数でフィールドのバリデーションを行う。呼び出す際は、第一引数に構造体のポインタを渡し、第二引数以降でルールを指定する。
ルールに関しては後述します。
もしバリデーションに失敗したフィールドがあれば、エラーは保存され、次のフィールドのバリデーションを行います。

よく使うvalidatinルール

  • In(...interface{}): リスト中に値があるかのチェック
validation.Field(&c.Gender, validation.In("Female", "Male")),
  • Length(min, max int): strings/slices/maps/arrays 型の長さが指定範囲内かのチェック
validation.Field(&c.Name, validation.Required, validation.Length(5, 20)),
  • Min(min interface{}) and Max(max interface{}): int/uint/float/time.Time型の値が指定範囲内かのチェック
validation.Field(&a.Value, validation.Min(1), validation.Max(3)),
  • Match(*regexp.Regexp): 正規表現にマッチしているかのチェック
大文字2つで構成されているかどうか
validation.Field(&a.State, validation.Required, validation.Match(regexp.MustCompile("^[A-Z]{2}$"))),
  • Date(layout string): 指定日付フォーマット形式かのチェック
validation.Field(&a.ShippedAt, validation.Date("2018-01-01")),
  • Required: 空でないことのチェック。nil/""/0を許容しない。
validation.Field(&a.Street, validation.Required)
  • NotNil: ポインタの値がnilじゃないことのチェック。""/0は許容する。
validation.Field(&a.SampleNotNil, validation.NotNil),

ここからisルール。
空の場合はエラーにならない。
注意点として、対象は全て文字列である必要がある。

  • Email: Eメール形式かどうかをチェック。
validation.Field(&c.Sample, is.Email),
  • URL: URL形式かどうかをチェック。
validation.Field(&c.Sample, is.URL),
  • Alpha: 英字(a-zA-Z)のみで構成されているかチェック。
validation.Field(&c.Sample, is.Alpha),
  • Digit: 数字(0-9)のみで構成されているかチェック。
validation.Field(&c.Sample, is.Digit),
  • Alphanumeric: 英数字(a-zA-Z0-9)のみで構成されているかチェック。
validation.Field(&c.Sample, is.Alphanumeric),
  • LowerCase: 小文字のみで構成されているかチェック。
validation.Field(&c.Sample, is.LowerCase),
  • UpperCase: 大文字のみで構成されているかチェック。
validation.Field(&c.Sample, is.UpperCase),
  • Int: int型の文字のみで構成されているかチェック。
validation.Field(&c.Sample, is.Int),
  • Float: float型の文字のみで構成されているかチェック。
validation.Field(&c.Sample, is.Float),
  • UUID: UUID形式かどうかをチェック。
validation.Field(&c.Sample, is.UUID),
  • CreditCard: クレジットカード形式かどうかをチェック。
validation.Field(&c.Sample, is.CreditCard),
  • JSON: json形式かどうかをチェック。
validation.Field(&c.Sample, is.JSON),
  • MAC: MACアドレス形式かどうかをチェック。
validation.Field(&c.Sample, is.MAC),
  • IP: IPアドレス形式かどうかをチェック。
validation.Field(&c.Sample, is.IP),

その他

バリデーション中のinternal server error

バリデーション処理中のエラーがバリデーションルール違反なのかinternal server errorなのかを切り分けられるようにするためには、以下のように実装する。

if err := a.Validate(); err != nil {
    if e, ok := err.(validation.InternalError); ok {
        log.Println(e.InternalError())
    }
}

参考

https://github.com/go-ozzo/ozzo-validation

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Goを読んでDockerの抽象構文木の構造をサクッと理解する

こんにちはpo3rinです。Dockerfile の抽象構文木(以降 AST と呼ぶ)ってどうなっているんだろうと思い調べてみました。

Dockerfile の AST を所得する

下記の Dockerfile の AST をみてみます。

FROM golang:latest

WORKDIR /go
ADD . /go

CMD ["go", "run", "main.go"]

mody の buildkit が Dockerfile の Parser を提供しているのでそれを使います。

package main

import (
    "fmt"
    "log"
    "os"

    "github.com/moby/buildkit/frontend/dockerfile/parser"
)

func main() {
    f, _ := os.Open("./Dockerfile")
    r, _ := parser.Parse(f)
    ast := r.AST.Dump()
    fmt.Printf("%+v", ast)
}

これで文字列化された Dockerfile の AST の Dump がみれます。

$ go run main.go
(from "golang:latest")
(workdir "/go")
(add "." "/go")
(cmd "go" "run" "main.go")

かなり、簡略化して表示されるので、AST の構造までは覗けません。そこはコードを読んでいく必要がありそです。

DockerfileのASTの構造を知る

github.com/moby/buildkit/frontend/dockerfile/parser を読むと下記の構造体があります。

// Node is a structure used to represent a parse tree.
type Node struct {
    Value      string          // actual content
    Next       *Node           // the next item in the current sexp
    Children   []*Node         // the children of this sexp
    Attributes map[string]bool // special attributes for this node
    Original   string          // original line used before parsing
    Flags      []string        // only top Node should have this set
    StartLine  int             // the line in the original dockerfile where the node begins
    endLine    int             // the line in the original dockerfile where the node ends
}

Node は解析木を表すために使用される構文コードです。基本的に Value、 Next、 Children の3つのフィールドを使います。Value は現在のトークンの文字列値です。 Next は次のトークンで、Children はすべての子Nodeのスライスになっています。

他の言語のASTをみたことある人はかなりシンプルな構造だと感じると思いますが、実は公式も「素直に言ってかなりお粗末」だと言っています。しかし、Dockerfile はプログラミング言語よりもシンプルなので、Node もシンプルであることはむしろ効果的だと言っています。

This data structure is frankly pretty lousy for handling complex languages, but lucky for us the Dockerfile isn't very complicated. This structure. works a little more effectively than a "proper" parse tree for our needs.

さてNodeが実際にどのように構成されているのか見てみましょう。ネストした構造体を覗くときはgithub.com/kr/prettyが便利なのでこれを使いましょう。まずは大枠を掴みます。

package main

import (
    "os"

    "github.com/kr/pretty"
    "github.com/moby/buildkit/frontend/dockerfile/parser"
)

func main() {
    f, _ := os.Open("./Dockerfile")
    r, _ := parser.Parse(f)
    pretty.Print(r.AST)
}

出力はこうなります。

&parser.Node{
    Value:    "",
    Next:     (*parser.Node)(nil),
    Children: {
        &parser.Node{
            Value: "from",
            Next:  &parser.Node{
                Value:      "golang:latest",
                Next:       (*parser.Node)(nil),
                Children:   nil,
                Attributes: {},
                Original:   "",
                Flags:      nil,
                StartLine:  0,
                endLine:    0,
            },
            Children:   nil,
            Attributes: {},
            Original:   "FROM golang:latest",
            Flags:      {},
            StartLine:  1,
            endLine:    1,
        },
        &parser.Node{
            Value: "workdir",
            Next:  &parser.Node{
                Value:      "/go",
                Next:       (*parser.Node)(nil),
                Children:   nil,
                Attributes: {},
                Original:   "",
                Flags:      nil,
                StartLine:  0,
                endLine:    0,
            },
            Children:   nil,
            Attributes: {},
            Original:   "WORKDIR /go",
            Flags:      {},
            StartLine:  3,
            endLine:    3,
        },
        &parser.Node{
            Value: "add",
            Next:  &parser.Node{
                Value: ".",
                Next:  &parser.Node{
                    Value:      "/go",
                    Next:       (*parser.Node)(nil),
                    Children:   nil,
                    Attributes: {},
                    Original:   "",
                    Flags:      nil,
                    StartLine:  0,
                    endLine:    0,
                },
                Children:   nil,
                Attributes: {},
                Original:   "",
                Flags:      nil,
                StartLine:  0,
                endLine:    0,
            },
            Children:   nil,
            Attributes: {},
            Original:   "ADD . /go",
            Flags:      {},
            StartLine:  4,
            endLine:    4,
        },
        &parser.Node{
            Value: "cmd",
            Next:  &parser.Node{
                Value: "go",
                Next:  &parser.Node{
                    Value: "run",
                    Next:  &parser.Node{
                        Value:      "main.go",
                        Next:       (*parser.Node)(nil),
                        Children:   nil,
                        Attributes: {},
                        Original:   "",
                        Flags:      nil,
                        StartLine:  0,
                        endLine:    0,
                    },
                    Children:   nil,
                    Attributes: {},
                    Original:   "",
                    Flags:      nil,
                    StartLine:  0,
                    endLine:    0,
                },
                Children:   nil,
                Attributes: {},
                Original:   "",
                Flags:      nil,
                StartLine:  0,
                endLine:    0,
            },
            Children:   nil,
            Attributes: {"json":true},
            Original:   "CMD [\"go\", \"run\", \"main.go\"]",
            Flags:      {},
            StartLine:  6,
            endLine:    6,
        },
    },
    Attributes: {},
    Original:   "",
    Flags:      nil,
    StartLine:  1,
    endLine:    6,
}

全体像は下記のようになります。

node.png

1行の中でもtokenごとにNodeに分けられます。一番上のNodeがルートNodeと呼ばれます。ルートNode自身はValueを持たずChildren Nodeの一覧を保持します。こう見るとルートNodeのChildrenの数はイメージのレイヤ数と基本的に一致します。Next Nodeは同じ行の中の次のtokenのNodeです。Flagsは --from=builderなどのDockerfile上で使われるFlagか格納されます。StartLineとendlineは文字通り、そのノードのDockerfileにおける行数です。Originalは解析前に使用された元の行を格納しています。

PrintWarnings を使って Dockerfile に対する Warning を見る

Dockerfile をビルドする際に稀に出る Warning は Dockerfile の parse 時に検知しています。mody は下記のようなparseした後のASTに対してWarningをだすメソッドもあります。

func (r *Result) PrintWarnings(out io.Writer)

このような空白行があると記載がDockerfileにあると

RUN echo "Hello" && \
    # (空行)
    echo "Docker AST"

このようなDockerfileに対して PrintWarnings メソッドを使うと下記のような Warning を吐きます。

[WARNING]: Empty continuation line found in:
    RUN echo "Hello" &&     echo "Docker AST"
[WARNING]: Empty continuation lines will become errors in a future release.

おまけ : ASTを使ったDockerfileのLint

Dockerfile の AST が手に入ったので簡単なlintツールも作れそうです。Dockerfile におけるベストプラクティスの一つはレイヤの数を最小にすることです。つまり、二回連続でRUNを使っている Dockerfile は一つのコマンドに統合すべきです。下のコードは RUN を二連続で呼び出している部分を検知します。

package main

import (
    "log"
    "os"

    "github.com/kr/pretty"
    "github.com/moby/buildkit/frontend/dockerfile/parser"
)

func main() {
    f, _ := os.Open("./Dockerfile")
    r, _ := parser.Parse(f)
    pretty.Print(r.AST)

    var valueToken string
    for _, child := range r.AST.Children {
        if valueToken == child.Value {
            log.Fatal("RUN is used in two consecutive layers")
        }
        valueToken = child.Value
    }
}

こんな Dockerfile は

FROM golang:latest

WORKDIR /go
ADD . /go

RUN echo "Hello"
RUN echo "Docker AST"

CMD ["go", "run", "main.go"]

こういう風に検知できますね。

$ go run main.go
2019/03/04 23:36:10 RUN is used in two consecutive layers

Dockerfile の AST はシンプルゆえ複雑なことはできませんが、これくらいの検知なら十分です。

終わりに

簡単に Dockerfile の AST を追ってみました。今後このASTを使ってどのようにビルドしているのか追ってみて、暇ならまた記事にします。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む