20191129のGoに関する記事は11件です。

Go Modules 依存パッケージを一括アップデートする

はじめに

Goのパッケージ管理はGo Modulesを使っていますが、何もわからない・・・
最新版に追従するためnpm updateみたいなことをしたいけどわからない・・・

アップデート方法

@propellaさんの記事を参考にgo get -uを実行してみました。

$ go get -u
go get .: path /Users/naoki/go/src/github.com/oke-py/usn-api is not a package in module rooted at /Users/naoki/go/src/github.com/oke-py/usn-api

どうやらうまくいっていません。検索して見つけたIssueコメントを参考にします。

$ go get -u all
go: finding github.com/gofrs/uuid v3.2.0+incompatible
go: finding github.com/aws/aws-lambda-go v1.13.3
go: finding github.com/jmespath/go-jmespath latest
go: finding github.com/guregu/dynamo v1.4.1
go: finding golang.org/x/net latest
go: finding github.com/cenkalti/backoff v2.2.1+incompatible
go: finding github.com/aws/aws-sdk-go v1.25.43
go: downloading github.com/aws/aws-sdk-go v1.25.43
go: downloading github.com/guregu/dynamo v1.4.1
go: downloading github.com/aws/aws-lambda-go v1.13.3
go: extracting github.com/aws/aws-lambda-go v1.13.3
go: extracting github.com/guregu/dynamo v1.4.1
go: downloading github.com/cenkalti/backoff v2.2.1+incompatible
go: downloading golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933
go: extracting github.com/cenkalti/backoff v2.2.1+incompatible
go: downloading gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405
go: extracting gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405
go: extracting golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933
go: extracting github.com/aws/aws-sdk-go v1.25.43
go: finding gopkg.in/yaml.v2 v2.2.7
go: finding gopkg.in/check.v1 latest
go: downloading gopkg.in/yaml.v2 v2.2.7
go: extracting gopkg.in/yaml.v2 v2.2.7
go: downloading gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15
go: extracting gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15

git diffgit statusを実行するとgo.modgo.sumが更新されたことがわかります。

おわりに

これで正しいのか自信がありませんが、やりたいことはできました。

参考

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

Go: DepからGo Modulesへの移行

はじめに

Go modulesはGo1.13(August 2019)から正式に導入されたGo言語公式の依存パッケージ管理ツールです。Go modules以前は、depが依存パッケージ管理のツールとしてはデファクトでした。Go modulesはGo1.11から使用は可能でしたが、環境変数(ENV GO111MODULE=on)を設定したり色々と手間がかかる事前準備が必要でした。Go1.13からはデフォルトでGo modulesが組み込まれています。

そもそもGo Modulesとは何?

Goの新しい依存管理システムである。
モジュールは、一つのユニットとしてバージョン管理されている関連Goパッケージの集まり。1.11からはmoduleモードとGOPATHモードが使えるようになっていましたが、1.13からはmoduleモードがデフォルトでONになっています。Goのオープンソースモジュールのエコシステムをより良く活用できるように、moduleモードがデフォルトで動作するようにしているそうです。
Golang.org: modules
Golang.org: Wiki-Modules

モジュールとは

モジュール対応モードでは,標準ライブラリを除くパッケージを「モジュール(module)」として管理する。 パッケージが git 等のバージョン管理ツールで管理されている場合はバージョン毎に異なるモジュールと見なされる。 つまりモジュールの実体は「パッケージ+バージョン」ということになる。
ただしコード上ではパッケージとモジュールの間に区別はなく,したがってソースコードを書き換える必要はない。 モジュールはソースコードではなくgo.modファイルで管理される。

depとGo Modulesの機能差マトリクス

項目 dep go modules
実行する場所 GOPATH以下である必要がある *Goプログラムのルートディレクトリ
*GOPATH以下にあるソースコードでもgo.modファイルがあればモジュール対応モードで管理が可能
管理ファイル Gopkg.tomlGopkg.lock go.modgo.sum
Vendoring depがvendoring対応のツール go mod vendorというコマンドでvendoring対応ができる dockerの場合はvendoring利用がおすすめ
Gitでの管理 Gopkg.tomlGopkg.lockをGit管理する 通常は、go.modgo.sumは共にGit管理する
パッケージ管理 リポジトリの最新リビジョンのみが対象 リポジトリのバージョンタグまたはリビジョン毎に管理。Semantic Versioningに対応 Semantic Versioningとは、vX.X.Xというようなバージョン番号の定義方法
依存packageの場所 $GOPATH/src以下プログラムルートのvendorディレクトリ $GOPATH/pkg/mod以下 *go modで取得したバイナリなどは$GOPATH/pkg/mod/以下にキャッシュされている
*CIの高速化でビルドキャッシュをするときはこのディレクトリをキャッシュする必要がある
*キャッシュを削除するときはgo clean -cacheコマンドで削除
*go mod vendorコマンドでdepのようにvendorディレクトリに依存関係を保存することができる
*go mod tidyでgo.modから不要な依存関係を削除

パッケージ管理で、GOPATHの依存がなくなる点くらいで双方の機能差はほぼ無し。
「$GOPATHからの呪縛からの解放」という最大のメリットで、何よりも代えがたいポイントのように思えます。

GOPATHモード(GOPATH mode)とモジュールモード(module-aware mode)

バージョン1.11以降からは、Go言語コンパイラは以下の2つのモードのどちらかで動作するようになっていました。

モード
GOPATHモード バージョン 1.10 までの動作モード。標準ライブラリを除く全てのパッケージの管理とビルドを $GOPATH 以下のディレクトリで行う。パッケージの管理はリポジトリの最新リビジョンのみが対象となる
モジュール対応モード 標準ライブラリを除く全てのパッケージをモジュールとして管理する。モジュールの管理とビルドは任意のディレクトリで可能で,モジュールはリポジトリのバージョンタグまたはリビジョン毎に管理される

環境変数 $GO111MODULE

モード切替は$GO111MODULEという環境変数で切り替えます。1.12まではautoが規定値になっている。 なお1.13 からはモジュール対応モードが既定になります。

設定値

設定値
auto $GOPATH 以下のディレクトリにあるパッケージは GOPATH モードで,それ以外はモジュール対応モードで動作する
off 常に GOPATH モードで動作する
on 常にモジュール対応モードで動作する

depからの移行

プロジェクトルートのGopkg.lockファイルを自動に読み込んで、moduleの初期設定を行ってくれる設計になっています。(depから移行するこが前提になっていますね :innocent:
Migrating to Go Modules などが参考になると思います。

  1. 既存のパッケージに go.mod ファイルを追加する
  2. Gopkg.lock ファイルを読んで go.mod ファイルに組み込んでくれる
$ go mod init github.com/my-repo/nice-project
go: creating new go.mod: module github.com/my-repo/nice-project
go: copying requirements from Gopkg.lock

*もちろん、まっさらな状態からgo moduleで初期化と依存パッケージのダウンロードを最初から行ってもいいです。

補足:Dockerでの設定

dockerで開発している場合は、ビルド前に依存パッケージをダウンロード・インストールする必要があります。ポイントだけをピックアップしたDockerfileのイメージです。

# Goのバージョンは1.13以上
FROM golang:1.13-alpine as golang-build

# Goビルドに必要なアプリケーションをインストールする
RUN .....

WORKDIR /go/src/github.com/my-repo/nice-project

# go moduleで依存パッケージを取得してダウンロードする
# ローカルの、go.modとgo.sumをコピー
COPY go.mod go.sum ./
# 依存ライブラリをダウンロードする
RUN go mod download

# Goアプリケーションのビルド
ADD . /go/src/github.com/my-repo/nice-project
RUN GOOS=linux GOARCH=amd64 CGO_ENABLED=1 go build main.go

ビルド前に、go moduleを使って依存ライブラリ・モジュールをダウンロードしておきます。
とても少ない記述量で実現できてしまいます。

まとめ

depを使っているときはは、CIのビルドの時間を短縮のためにdep ensure -vendor-only=trueとして新しいパッケージを追加する際に、Gopkg.lockとGopkg.tomlを更新していたり手間がかかっていました。modulesでキャッシュディレクトリを設定しておけば、ビルド時間も短縮できるためトリッキーなファイルの運用がなくなりました。1.13がリリースされて4ヶ月位経ちますが、depで苦労していたり、切替をお考えの方がいれば、modulesへの移行の参考になれば幸いです。

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

[Go]context.WithTimeoutとcontext.WithDeadlineの違い

context.WithDeadline

コメントを引用すると、以下のように書いてある

WithDeadline returns a copy of the parent context with the deadline adjusted to be no later than d. If the parent's deadline is already earlier than d, WithDeadline(parent, d) is semantically equivalent to parent. The returned context's Done channel is closed when the deadline expires, when the returned cancel function is called, or when the parent context's Done channel is closed, whichever happens first.
Canceling this context releases resources associated with it, so code should call cancel as soon as the operations running in this Context complete.

context.WithTimeout

WithTimeout returns WithDeadline(parent, time.Now().Add(timeout)).

つまりcontext.WithTimeoutはWithDeadline(parent, time.Now().Add(timeout)を読んでいるだけのよう
https://golang.org/src/context/context.go#L463

すこし違うのは、context.WithDeadlineではtime.Time型を引数にとるのに対して、context.WithTimeoutではtime.Duration型を引数にとる。

つまり、
- context.WithDeadline => キャンセルされる時間を指定する
- context.WithTimeout => 現在時間からキャンセルされるまでの時間を指定する
の違いがある。

// 以下の二つは同じ

d := time.Now().Add(time.Second)
ctx, cancel := context.WithDeadline(ctx, d)

// ----------

d := time.Second
ctx, cancel := context.WithTimeout(ctx, d)

 余談

context.WithDeadlineのコメントに書いてあるように親のcontextのDeadlineはWithTimeoutでも同じなので、子のcontextが親のcontextのDeadline以降のDeadlineを指定することはできない
https://golang.org/src/context/context.go#L395

試しに書いたコード

package main

import (
    "context"
    "fmt"
    "time"
)

func main() {
    ctx := context.Background()
    ctx, cancel := context.WithTimeout(ctx, time.Second)
    defer cancel()

    start := time.Now()

    if err := parentFunc(ctx); err != nil {
        fmt.Printf("error: %v\n", err)
    }

    fmt.Printf("time passed %v\n", time.Since(start))
    /*
        error: parent error: childFunc error: heavyFunc error: context deadline exceeded
        time passed 1.001596379s
    */
}

func parentFunc(ctx context.Context) error {
    // Deadline of parent is 1 sec.
    d := time.Second
    ctx, cancel := context.WithTimeout(ctx, d)
    defer cancel()

    if err := childFunc(ctx); err != nil {
        return fmt.Errorf("parent error: %w", err)
    }

    return nil
}

func childFunc(ctx context.Context) error {
    // Deadline of child is 2 sec.
    d := 2 * time.Second
    ctx, cancel := context.WithTimeout(ctx, d)
    defer cancel()

    if err := heavyFunc(ctx); err != nil {
        return fmt.Errorf("childFunc error: %w", err)
    }

    return nil
}

func heavyFunc(ctx context.Context) error {
    select {
    case <-time.After(10 * time.Second):
        fmt.Println("finish calculation")
        return nil
    case <-ctx.Done():
        return fmt.Errorf("heavyFunc error: %w", ctx.Err())
    }
}

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

ニフクラ用の Terraformカスタムプロバイダを書いてみた(未完)

概要

先日記事に書いたニフクラ用Terraform Provider について、中途半端なところではありますが開発を終了しました。
ニフクラを利用する職場を離れたため、今後は動作検証もできないので、ここまでとなりました。

ニフクラ用Terraform Provider

予定通りロードバランサー、付替IPアドレスまでは追加できたので、作成可能なリソースは最終的に以下のようになりました。

リソース ステータス 備考
サーバー ok インポートやコピーは作っていません
ディスク ok
ネットワーク・セキュリティ ok SSHキーインポートのみ
OSイメージ ok
ロードバランサー ok SSL関連は未検証
マルチロードバランサー n/a
ファイアウォール ok
SSL証明書 n/a
付替IPアドレス ok
追加NIC n/a
オートスケール n/a
基本監視 n/a
プライベートLAN ok
ルーター ok dhcp関連とNAT関連とWebプロキシは作っていません
拠点間VPNゲートウェイ ok
サーバーセパレート n/a
バックアップ ok
RDB ok イベント通知は作っていません
NAS n/a

サンプルイメージ

examples_v0.12.14+ のコードを実行すると、とりあえず以下のような環境ができあがる状態になっています。
disableにしていますが、RDB やバックアップ、カスタマイズイメージも作成可能です。
nifcloud_examples_001.png

備忘録

作成中はずっと Terraform v0.12.9 と examples のサンプルでテストしていました。
無事予定していたところまで完成したので、終了しようと思ったところ、Terraform のバージョンがいつのまにか v0.12.16 まで上がっていました。
せっかくなので最新バージョンで動作確認しようと思ったところ、エラーの嵐が・・・。
v0.12.14以降は変数の記載の仕方が厳格になったのですね。
そのため急遽 examples を修正して、 examples_v0.12.14+ を作りました。
これで動く・・・と思いきや、 Error: rpc error: code = Unavailable desc といったエラーが大量にでました。
「今さらどうしよう・・・なぜ・・・」と思いましたが、hashicorp 関連といえばもうひとつあるぞ、と気づきました。
作成しているコード側で、 github.com/hashicorp/terraform-plugin-sdk/ を使っているんですよね。
go.mod を見ると v1.1.1 でしたが、最新版は v1.4.0 まで上がっていました。
go.mod の記載を修正して再度 build したものを利用したところ、問題は無くなりました。
今後利用される方がいらっしゃった場合に、このあたりも気にしていただくと、しばらくはバージョンが上がってもちゃんと動くのかな、と思っています。

今後について

私の開発は終了ですが、もし続きを作ろうかなとか、修正したいなとお考えになる方がいらっしゃったときのために、環境を分ける方法を簡単に記載しておきます。
以下のように自分の環境を作っていただいたらよいのかな、と思っています。

  1. github等で nifcloud-sdk-go と terraform-provider-nifcloud のリポジトリを作成
  2. 以下のようにして、自分の環境を作成
mkdir -p $GOPATH/src/github.com/YOURACCOUNT
cd $GOPATH/src/github.com/YOURACCOUNT
git clone https://github.com/shztki/nifcloud-sdk-go.git
cd nifcloud-sdk-go
git checkout -b XXXXXX
find ./ -type f | xargs sed -i "s/shztki/YOURACCOUNT/g"
git add .
git commit -m "path change"
git push -u origin XXXXXX

cd $GOPATH/src/github.com/YOURACCOUNT
git clone https://github.com/shztki/terraform-provider-nifcloud.git
cd terraform-provider-nifcloud
git checkout -b XXXXXX
find ./ -type f | xargs sed -i "s/shztki/YOURACCOUNT/g"
git add .
git commit -m "path change"
git push -u origin XXXXXX
  • branch を作成している部分は、分けるつもりが無ければ不要です。
  • sed での置換については、すべてのファイルが変更になるので注意。 .git 以下も変わるので、 git remote set-url origin https://~ が不要になります。
  • github等のデフォルトブランチは自分が利用するブランチにしてください。

最後に

お読みいただきありがとうございました。
中途半端なところで終了してしまいましたので、業務に取り入れる、などは開発を継続できるような方でないと難しいと思いますが、個人でこっそり構築タスクを効率化する、程度であれば、ちょっとはお役に立てそうかな、と考えたりもしております。
私の試行錯誤した流れが、多少でもお役に立てれば幸いです。
どうぞよいニフクラライフを!

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

ASTでドキュメントコメント(Docフィールド)を取得する際にハマった

"go/parser" でパースしたASTから、Docフィールドを取ろうとしてちょっとはまったのでメモ

GitHub のIssueによると 1
type の前につけたドキュメントコメントは、ast.TypeSpec ではなく、ast.GenDeclDocフィールドにつくよという話。

// doc comment
type Foo struct {
}
// このコメントは GenDecl.Doc につく
type A struct {
}

// このコメントは GenDecl.Doc につく
type (
    // このコメントが TypeSpec.Doc につく
    B struct {
    }
)

動作確認のソース Gist

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

Github actionsでgo mod download, test, lint

Github Actions

https://github.co.jp/features/actions
GithubのCI/CDサービス

モチベーション

現職ではほとんどcircleciで回しているが、Github actionsが便利ということを聞いたので試した。
go.modのキャッシュの問題などあり、実際にやってみて記事としてこれ持って来ればOKみたいのが見当たらず、
githubでpublic repoを徘徊したので、最低限自分にあったやり方をまとめておきたい

workflow

stepなどはほぼ他のciサービスと同じだと思うので割愛
実際のyamlは以下

github/workflows/ci.yaml
// 好きな名前
name: golang_ci
// フック
on: [push]

jobs:
  build:
    name: all
    runs-on: ubuntu-latest
    steps:
      # GOPATHなどのセットアップ
      - name: set up
        uses: actions/setup-go@v1
        with:
          go-version: 1.13
        id: go
      # repositoryの中身にアクセスするためのチェックアウト
      - name: checkout
        uses: actions/checkout@v1
      # cache store
      - name: cache
        uses: actions/cache@v1
        with:
          path: ~/go/pkg/mod
          key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
          restore-keys: |
            ${{ runner.os }}-go-
      - name: download modules
        # キャッシュが保存されていればそれを使う
        if: steps.cache.outputs.cache-hit != 'true'
        run: go mod download
      - name: install lint
        run: curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(go env GOPATH)/bin v1.18.0
      - name: lint
        run: golangci-lint run
      - name: test
        run: go test ./... -v

基本的にactionを指定して環境を構築 -> 実行という流れ

参照できるactionは以下で
- public repository
- workflowと同一repository
- Dockerhubで公開されているDockerコンテナイメージ

感想

circleciと比べてみての感想になってしまうが以下のようなメリデメリを感じた

メリット

デメリット

  • 参考にできる実例が少ない これは仕方ない。実際に公開repositoryでgo.modのキャッシュ関連をAll Githubで調べても100件はなさそうだった
  • sshがちょっとめんどくさそう workflowに変更を入れないとsshできない様子

手軽にsshしたいなと感じたくらいで、あとはCI構築のためには最適なGithub上でのtry and error、確認が手軽にできるのが嬉しい
今後個人repoなら使っていくかもしれない

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

04. 元素記号

04. 元素記号

"Hi He Lied Because Boron Could Not Oxidize Fluorine. New Nations Might Also Sign Peace Security Clause. Arthur King Can."という文を単語に分解し,1, 5, 6, 7, 8, 9, 15, 16, 19番目の単語は先頭の1文字,それ以外の単語は先頭に2文字を取り出し,取り出した文字列から単語の位置(先頭から何番目の単語か)への連想配列(辞書型もしくはマップ型)を作成せよ.

Go

package main

import (
    "fmt"
    "strings"
)

func main() {
    var src = "Hi He Lied Because Boron Could Not Oxidize Fluorine. New Nations Might Also Sign Peace Security Clause. Arthur King Can.";
    var dw = map[int]bool{1: true, 5: true, 6: true, 7: true, 8: true, 9: true, 15: true, 16: true, 19: true}; //  1文字の単語番号
    var res map[string]int = map[string]int{};

    //  単語に分割
    words := strings.Split(src, " ")
    for i, word := range words {
        idx := i + 1

        //  配列の exists的関数がなさそうなので Map を使用...
        if dw[idx] {
            //  1文字を map へ保存
            res[word[0:1]] = idx
        } else {
            //  2文字を map へ保存
            res[word[0:2]] = idx
        }
    }

    //  結果を表示
    fmt.Println(res)
}

python

# -*- coding: utf-8 -*-
src = "Hi He Lied Because Boron Could Not Oxidize Fluorine. New Nations Might Also Sign Peace Security Clause. Arthur King Can."
dw = [1, 5, 6, 7, 8, 9, 15, 16, 19]  # 1文字の単語番号
res = {}

#   単語に分割
words = src.split(" ")
for i in range(len(words)):
    #   単語番号
    idx = i + 1

    #   1文字の単語番号の場合
    if idx in dw:
        #   1文字を map へ保存
        res[words[i][0:1]] = idx
    else:
        #   2文字を map へ保存
        res[words[i][0:2]] = idx

#   結果を表示
print(res)

Javascript

var src = "Hi He Lied Because Boron Could Not Oxidize Fluorine. New Nations Might Also Sign Peace Security Clause. Arthur King Can."
var dw = [1, 5, 6, 7, 8, 9, 15, 16, 19];    //  1文字の単語番号
var res = new Map();

//  単語を空白で分割し単語数処理
var words = src.split(' ');
for (var i = 0; i < words.length; i++) {
    //  単語番号
    var idx = i+1;

    //  1文字の単語指定の場合 (ES2017)
    if (dw.includes(i+1)) {
        //  1文字を map へ保存
        res.set(words[i].substring(0,1),idx);
    }
    else {
        //  2文字を map へ保存
        res.set(words[i].substring(0,2),idx);
    }
}

//  結果表示
console.log(res);

まとめ

if文で or を書くのがイマイチかと思い 1文字の単語番号をまとめてみた。
Goの配列などの存在チェックがイマイチ?。

次の「05. n-gram」 は問題のの意味がよくわからない、まずは問題の理解から初めて見る。

他の人のを確認したら Map の内容が [文字 => 単語番号] と逆だったため修正しました。

その他

もともと、C言語やPHPが長いので Goの行末の ";" 癖が治らない。w

トップ

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

DockerでGoのRestAPIを作ってみた(ホットリロード対応)

dockerでGolangのAPIを作りたいと思いましてやってみました。
ちなみに、dockerはホットリロードさせたかったので、docker-composeを使っています。

環境

それぞれのバージョンはこちら。
Dockerについての説明とかは省きます。

$ docker-compose --version
docker-compose version 1.24.1, build 4667896b

$ go version
go version go1.13.4 darwin/amd64

■ echo 4.1.11 (ルーティングが便利になるかな、と)
  公式はこちら https://echo.labstack.com/guide
  以下によると、RestAPIに最適化されているらしー。
   https://rightcode.co.jp/blog/become-engineer/go-flamework

■ oxequa/realize
  ホットリロードを行うために入れてみた。

ファイルの準備

適当なフォルダに以下の3ファイルを用意します。

Docker

Dockerfile

# 公式 golang ランタイムをイメージとして使用
FROM golang:1.13

# ソース入れる用のディレクトリ作成(名前はなんでもいいけど、この後の「app」は全部合わせる)
RUN mkdir /app 

# ソース入ってるのここだよ
WORKDIR /app

# ホスト側のカレントにあるファイルをソースフォルダにコピーするよ
COPY . /app

# echo と oxequa/realize のライブラリ取ってくるよ(ぼちぼち時間かかるよ...)
RUN go get -u github.com/labstack/echo/...
RUN go get github.com/oxequa/realize

docker-compose.yml

docker-compose.yml
version: '3'

services:
  api:
    build: .

    #buildが指定されている場合は作成イメージの名前になる
    #build指定なし:このイメージを元に作成される(ex.mysqlとか)
    image: sample_api:0.1

    # ポートフォワーディング(ホスト側:docker側)
    # ホスト側のブラウザで実行するときは http://localhost:1000 で見れるようになる
    ports:
      - "1000:1323"

    # (ホスト側:docker側)dockerの「/app」フォルダとホストのカレントを繋げるよ。
    # ホストの変更が反映されるようになる
    volumes:
      - .:/app

    # デフォルトのコマンドをこれに変える。ホットリロードを有効にする?
    command: realize start --run --no-config

    tty: true

Golang

server.go

server.go
package main

import (
    "net/http"

    "github.com/labstack/echo"
)

func main() {
    e := echo.New()
    routing(e) 
    e.Logger.Fatal(e.Start(":1323"))
}

/* ルーティングを行う */
func routing(e *echo.Echo) {
    e.GET("/", hello)
    e.GET("/:name", greeting)
}

/* http:/~/ の時 */
func hello(c echo.Context) error {
    return c.JSON(http.StatusOK, map[string]string{"message": "hello"})
}

/* http:/~/(名前) の時 */
func greeting(c echo.Context) error {
    /* c.Param("name") とすることで、URLの:name と対応させて取得 */
    return c.JSON(http.StatusOK, map[string]string{"message": "hello " + c.Param("name")})
}

Docker起動

上記ファイルを保管したフォルダに移動して、コマンド実行
(「-d」オプションつけてバックグラウンド実行しない理由は後述。つけても大丈夫)

 $ docker-compose up

これで、ローカルのブラウザからhttp://localhost:1000ってしたら
{message:hello}が。

http://localhost:1000/taroってしたら
{message:hello taro}って帰ってくるはず。

詰まった時の小ネタ

docker-compose up でやってみる
 どういうことかというと、「-d」を取ることでエラーの内容を教えてくれる。

 あずきはコマンド実行後、ターミナルをそのまま使いたかったので、おまじないのように「-d」をつけてました。
 で、エラーで動かないんだけど(docker ps にあがってこない)なんでか分かんなかった。

 原因は同じフォルダ内に「func main()~」が書かれたファイルがいたってことだった。
 試行錯誤しながら作ってたので、バックグランド実行だと気づかなくて何時間も無駄にした。

 でも、苦労したことは忘れない。きっと。

参考

https://qiita.com/y-ohgi/items/671de11f094f72a058b1
https://www.fox-hound.tech/1179/
https://qiita.com/prgseek/items/e557a371d7bd1f57b9b1
https://qiita.com/TsutomuNakamura/items/7e90e5efb36601c5bc8a
http://docs.docker.jp/engine/articles/dockerfile_best-practice.html
https://qiita.com/zembutsu/items/9e9d80e05e36e882caaa
API関連
https://ken-aio.github.io/post/2019/01/30/golang-echo/

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

ストレージ層のテスト

ではstore構造体にtodoManagerインターフェイスを実装していきます。

store.go
func (s *store) projectTodo(t todo) (string, error) {
    return "", nil
}

テーブル定義

まずは保存用のテーブルを定義します。シンプルに以下の感じでいいでしょう。

sql.go
package schema

var schemas = map[int]string{
    0: `
...
`,
    1: `
BEGIN;

CREATE TABLE todo ( 
        id          UUID   NOT NULL PRIMARY KEY,
        title       STRING NOT NULL,
        description STRING NOT NULL
);

UPDATE schema_version SET md_curr = false, md_update = EXTRACT(EPOCH FROM current_timestamp)::INT WHERE md_curr = true;
INSERT INTO schema_version VALUES (1);

COMMIT;
`,
}

スキーマのバージョンを更新するクエリにも注目してみてください。

store構造体のテスト

以下のようにテストを書いていきます。

store_test.go
package main

import "testing"

func TestStore_ProjectTodo(t *testing.T) {
    t.Run("project a new todo", func(t *testing.T) {})
}

テスト用データベースのセットアップ

テスト時にはローカルにCockroachDBのインスタンスを立ち上げ、ダミーのDBに対してテストを実行していきたいと思います。
なのでセットアップ用に以下のユーティリティ関数、変数を用意します。

store_test.go
var testDB = fmt.Sprintf("test-%s", uuid.New().String())

func openDB(t *testing.T) *sql.DB {
    connStr := "postgres://root@localhost:26257/?sslmode=disable"
    db, err := sql.Open("postgres", connStr)
    require.NoError(t, err)

    _, err = db.Exec(fmt.Sprintf(`CREATE DATABASE IF NOT EXISTS "%s"; USE "%s"`, testDB, testDB))
    require.NoError(t, err)

    return db
}

func closeDB(t *testing.T, db *sql.DB) {
    require.NoError(t, db.Close())
}

テスト実行のたびにデータベースを作成することになるためパフォーマンスはよいとは言えませんが、問題となる規模になるまではこれで十分でしょう。
ローカルでのCockroachDB起動のため以下のdocker-compose.yamlを用意します。

docker-compose.yaml
version: '3'
services:
  cockroachdb:
    image: cockroachdb/cockroach
    container_name: cockroachdb
    ports:
      - "26257:26257"
      - "8080:8080"
    command: ["start", "--insecure"]

テストの実行

準備が整ったためテストを書きます。
一気に書いてしまいます。以下のような感じです。

store_test.go
func TestStore_ProjectTodo(t *testing.T) {
    t.Run("project a new todo", func(t *testing.T) {
        db := openDB(t)
        defer closeDB(t, db)

        sut, err := newStore(db, defaultSchemaVersion)
        require.NoError(t, err)

        input := todo{
            title:       "foo title",
            description: "foo description",
        }

        id, err := sut.projectTodo(input)
        require.NoError(t, err)

        var got todo
        require.NoError(t, db.QueryRow(
            `SELECT title, description FROM todo WHERE id = $1`,
            id,
        ).Scan(&got.title, &got.description))
        assert.Equal(t, input, got)
    })
}

main.goでデフォルトのスキーマバージョンを更新しておくのもお忘れなく。

var (
        gitHash              = "overriden at compile time"
-       defaultSchemaVersion = 0
+       defaultSchemaVersion = 1
)

ではdocker-composeでローカル環境を立ち上げて、テストを実行してみます。

$ docker-compose up
Creating network "qiita-advent-calendar-2019_default" with the default driver
Creating cockroachdb ... done
Attaching to cockroachdb
...

$ make test
go test -v -cover -timeout 30s ./...
=== RUN   TestServer_CreateTodo
=== RUN   TestServer_CreateTodo/project_a_new_todo
=== RUN   TestServer_CreateTodo/error_in_projection
--- PASS: TestServer_CreateTodo (0.00s)
    --- PASS: TestServer_CreateTodo/project_a_new_todo (0.00s)
    --- PASS: TestServer_CreateTodo/error_in_projection (0.00s)
=== RUN   TestStore_ProjectTodo
=== RUN   TestStore_ProjectTodo/project_a_new_todo
--- FAIL: TestStore_ProjectTodo (0.21s)
    --- FAIL: TestStore_ProjectTodo/project_a_new_todo (0.21s)
        require.go:794: 
                Error Trace:    store_test.go:47
                Error:          Received unexpected error:
                                pq: error in argument for $1: could not parse string "" as uuid
                Test:           TestStore_ProjectTodo/project_a_new_todo
FAIL
coverage: 22.9% of statements
FAIL    github.com/KentaKudo/qiita-advent-calendar-2019 0.897s
?       github.com/KentaKudo/qiita-advent-calendar-2019/internal/pb/service     [no test files]
?       github.com/KentaKudo/qiita-advent-calendar-2019/internal/schema [no test files]
FAIL
make: *** [test] Error 1

失敗です。いいですね。

store.projectTodoの実装

ではいよいよstore.projectTodoを以下のように実装します。

store.go
func (s *store) projectTodo(t todo) (string, error) {
    id := uuid.New().String()
    if _, err := s.db.Exec(
        `INSERT INTO todo (id, title, description) VALUES ($1, $2, $3)`,
        id, t.title, t.description,
    ); err != nil {
        return "", err
    }

    return id, nil
}

再度テストを実行。

$ make test
go test -v -cover -timeout 30s ./...
=== RUN   TestServer_CreateTodo
=== RUN   TestServer_CreateTodo/project_a_new_todo
=== RUN   TestServer_CreateTodo/error_in_projection
--- PASS: TestServer_CreateTodo (0.00s)
    --- PASS: TestServer_CreateTodo/project_a_new_todo (0.00s)
    --- PASS: TestServer_CreateTodo/error_in_projection (0.00s)
=== RUN   TestStore_ProjectTodo
=== RUN   TestStore_ProjectTodo/project_a_new_todo
--- PASS: TestStore_ProjectTodo (0.55s)
    --- PASS: TestStore_ProjectTodo/project_a_new_todo (0.55s)
PASS
coverage: 24.7% of statements
ok      github.com/KentaKudo/qiita-advent-calendar-2019 1.269s  coverage: 24.7% of statements
?       github.com/KentaKudo/qiita-advent-calendar-2019/internal/pb/service     [no test files]
?       github.com/KentaKudo/qiita-advent-calendar-2019/internal/schema [no test files]

通りました:)

main.goの更新

最後にmain.go内でserver構造体に依存注入するようにしましょう。
利便性のためnewServer()関数も定義しておきます。

server.go
func newServer(todoMgr todoManager) *server {
    return &server{
        todoMgr: todoMgr,
    }
}
main.go
func main() {
    app := cli.App(appName, appDesc)

    ...

    app.Action = func() {
        ...

        store, err := newStore(db, *schemaVersion)
        if err != nil {
            log.WithError(err).Fatalln("init store")
        }

        lis, err := net.Listen("tcp", net.JoinHostPort("", strconv.Itoa(*grpcPort)))
        if err != nil {
            log.Fatalln("init gRPC server:", err)
        }
        defer lis.Close()

        gSrv := initialiseGRPCServer(newServer(store))

        ...
    }

    if err := app.Run(os.Args); err != nil {
        log.WithError(err).Fatal("app run")
    }
}

CircleCI設定の更新

忘れていました。CircleCI上でもCockroachDBを立ち上げてテストできるようにします。

$ git diff
diff --git a/.circleci/config.yml b/.circleci/config.yml
index 52436a0..fbeabaa 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -4,6 +4,8 @@ jobs:
     working_directory: ~/qiita-advent-calendar-2019
     docker:
     - image: circleci/golang:1
+    - image: cockroachdb/cockroach
+      command: ["start", "--insecure"]
     steps:
     - checkout
     - run: make all
@@ -12,6 +14,8 @@ jobs:
     working_directory: ~/qiita-advent-calendar-2019
     docker:
     - image: circleci/golang:1
+    - image: cockroachdb/cockroach
+      command: ["start", "--insecure"]
     steps:
     - checkout
     - run: make all

ついでに

やっぱりインテグレーションテストもしてみたかったのでBloomRPCでデバッグしてみました。

デプロイをしなおしたらport-forwardでポッドを繋いで、

$ kubectl -n qiita scale --replicas=0 deployment qiita-advent-calendar-2019

$ kubectl -n qiita scale --replicas=1 deployment qiita-advent-calendar-2019

$ kubectl -n qiita port-forward qiita-advent-calendar-2019-5bc6786c75-jwl5r 8090:8090
Forwarding from 127.0.0.1:8090 -> 8090
Forwarding from [::1]:8090 -> 8090

BloomRPCでリクエストの送信。

Screenshot 2019-11-28 at 18.39.57.png

DBの覗き見。

$ kubectl -n qiita exec -it cockroachdb-0 -- /cockroach/cockroach sql --url postgres://root@localhost:26257 --insecure
...
root@localhost:26257/defaultdb> use qiita_advent_calendar_2019_db;
SET

Time: 998.634µs

root@localhost:26257/qiita_advent_calendar_2019_db> select * from todo;
                   id                  |      title       |                   description                    
+--------------------------------------+------------------+-------------------------------------------------+
  8644af1a-d16a-4b92-911f-19e03905dffe | wash your hands! | wash your hands when you get back from outside!  
(1 row)

Time: 26.989411ms

いいですねいいいですね。


というわけでCreateTodoエンドポイントの実装が完了しました。
盛りだくさんでしたがいかがでしたか?

明日からはトピックがまた変わります。お楽しみに。

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

インターフェイス、テスト、モック

昨日までPOSTメソッドとか言ってましたが実際にはrpc CreateTodoですね、失礼しました。
実装していきます。

実装していく過程で、テストやモックなどもみていきたいと思います。
では参りましょう。

インターフェイスの定義

まずインターフェイスを定義し、server構造体からどう呼び出したいかを決めます。
今回はTodoを作成したいので以下のようにserver.goに定義しました。

server.go
...

var _ service.TodoAPIServer = (*server)(nil)

type (
    todo        struct{}
    todoManager interface {
        projectTodo(todo) (string, error)
    }

    server struct {
        todoMgr todoManager
    }
)

func (*server) GetTodo(context.Context, *service.GetTodoRequest) (*service.GetTodoResponse, error) {

...

todo構造体を受け取り、保存したらそのidを返します。
サーバーはこのtodoManagerインターフェイスへ依存することにします。

モックの作成

ではテストを書くために、このインターフェイスからモックを作成しましょう。
モックのコードジェネレートには github.com/golang/mock/mockgen を使います。
今回は直接Makefileにタスクを追加していきましょう。まずはツールインストール用のタスク。

mockgen-install:
    GO111MODULE=off go get github.com/golang/mock/gomock
    go install github.com/golang/mock/mockgen

次にコード生成用のタスク。

mockgen:
    mockgen -source=server.go -package=main -destination server_mock.go -mock_names todoManager=MockTodoManager

では試してみます。

$ make mockgen-install
GO111MODULE=off go get github.com/golang/mock/gomock
go install github.com/golang/mock/mockgen

$ make mockgen
mockgen -source=server.go -package=main -destination server_mock.go -mock_names todoManager=MockTodoManager

無事server_mock.goが生成されているようです。

テスト

ではserver_test.goを作成し、テストを書いていきましょう。

server_test.go
package main

import "testing"

func TestServer_CreateTodo(t *testing.T) {
}

個人的にはTestSuite構造体を作ってテストを書くのがお気に入りです。

server_test.go
type serverTestSuite struct {
    sut     *server
    ctrl    *gomock.Controller
    todoMgr *MockTodoManager
}

func newServerTestSuite(t *testing.T) serverTestSuite {
    ctrl := gomock.NewController(t)
    todoMgr := NewMockTodoManager(ctrl)
    return serverTestSuite{
        sut:     &server{todoMgr: todoMgr},
        ctrl:    ctrl,
        todoMgr: todoMgr,
    }
}

まずは保存が成功するパターンをテストしてみましょう。

server_test.go
func TestServer_CreateTodo(t *testing.T) {
    t.Run("project a new todo", func(t *testing.T) {
        s := newServerTestSuite(t)
        defer s.ctrl.Finish()

        input := &service.CreateTodoRequest{}
        want := &service.CreateTodoResponse{}

        got, err := s.sut.CreateTodo(context.Background(), input)
        require.NoError(t, err)
        assert.Equal(t, want, got)
    })
}

ここで期待すべきはserver構造体がtodoMgr.projectTodo()を呼び出すことなので、以下のようにアサーションを追加します。

server_test.go
func TestServer_CreateTodo(t *testing.T) {
    t.Run("project a new todo", func(t *testing.T) {
        s := newServerTestSuite(t)
        defer s.ctrl.Finish()

        input := &service.CreateTodoRequest{}

        todoID := uuid.New().String()
        want := &service.CreateTodoResponse{}

        s.todoMgr.EXPECT().
            projectTodo(todo{}).
            Return(todoID, nil)

        got, err := s.sut.CreateTodo(context.Background(), input)
        require.NoError(t, err)
        assert.Equal(t, want, got)
    })
}

inputとwantも値を埋めていきましょう。

server_test.go
func TestServer_CreateTodo(t *testing.T) {
    t.Run("project a new todo", func(t *testing.T) {
        s := newServerTestSuite(t)
        defer s.ctrl.Finish()

        input := &service.CreateTodoRequest{
            Todo: &service.Todo{
                Title:       "foo todo",
                Description: "foo description",
            },
        }

        todoID := uuid.New().String()
        want := &service.CreateTodoResponse{
            Success: true,
            Id:      todoID,
        }

        s.todoMgr.EXPECT().
            projectTodo(todo{}).
            Return(todoID, nil)

        got, err := s.sut.CreateTodo(context.Background(), input)
        require.NoError(t, err)
        assert.Equal(t, want, got)
    })
}

最後にtodo構造体を更新してタイトルと説明文が保存されるようにします。

server.go
type (
    todo struct {
        title       string
        description string
    }
    ...
)
server_test.go
func TestServer_CreateTodo(t *testing.T) {
    t.Run("project a new todo", func(t *testing.T) {
        s := newServerTestSuite(t)
        defer s.ctrl.Finish()

        input := &service.CreateTodoRequest{
            Todo: &service.Todo{
                Title:       "foo todo",
                Description: "foo description",
            },
        }

        todoID := uuid.New().String()
        want := &service.CreateTodoResponse{
            Success: true,
            Id:      todoID,
        }

        s.todoMgr.EXPECT().
            projectTodo(todo{
                title:       input.Todo.Title,
                description: input.Todo.Description,
            }).
            Return(todoID, nil)

        got, err := s.sut.CreateTodo(context.Background(), input)
        require.NoError(t, err)
        assert.Equal(t, want, got)
    })
}

いい感じです。テストを実行してみます。

$ make test
go test -v -cover -timeout 30s ./...
=== RUN   TestServer_CreateTodo
=== RUN   TestServer_CreateTodo/project_a_new_todo
--- FAIL: TestServer_CreateTodo (0.00s)
    --- FAIL: TestServer_CreateTodo/project_a_new_todo (0.00s)
        server_test.go:57: 
                Error Trace:    server_test.go:57
                Error:          Not equal: 
                                expected: &service.CreateTodoResponse{Success: true,
                                Id: "1fbf35f0-9b75-4d28-95fe-ea0dc3d67866",
                                }
                                actual  : &service.CreateTodoResponse{Success: false,
                                Id: "",
                                }

                                Diff:
                                --- Expected
                                +++ Actual
                                @@ -1,2 +1,2 @@
                                -(*service.CreateTodoResponse)(&CreateTodoResponse{Success:true,Id:1fbf35f0-9b75-4d28-95fe-ea0dc3d67866,})
                                +(*service.CreateTodoResponse)(&CreateTodoResponse{Success:false,Id:,})

                Test:           TestServer_CreateTodo/project_a_new_todo
        server_test.go:58: missing call(s) to *main.MockTodoManager.projectTodo(is equal to {foo todo foo description}) /Users/kenta/.ghq/github.com/KentaKudo/qiita-advent-calendar-2019/server_test.go:49
        server_test.go:58: aborting test due to missing call(s)
FAIL
coverage: 9.1% of statements
FAIL    github.com/KentaKudo/qiita-advent-calendar-2019 0.682s
?       github.com/KentaKudo/qiita-advent-calendar-2019/internal/pb/service     [no test files]
?       github.com/KentaKudo/qiita-advent-calendar-2019/internal/schema [no test files]
FAIL
make: *** [test] Error 1

いいですね、期待通り失敗しています。

server.CreateTodoの実装

では、server.goを以下のように更新して、もう一度実行してみます。

server.go
func (s *server) CreateTodo(ctx context.Context, req *service.CreateTodoRequest) (*service.CreateTodoResponse, error) {
    id, err := s.todoMgr.projectTodo(todo{
        title:       req.Todo.Title,
        description: req.Todo.Description,
    })
    if err != nil {
        return nil, err
    }

    return &service.CreateTodoResponse{
        Success: true,
        Id:      id,
    }, nil
}
$ make test
go test -v -cover -timeout 30s ./...
=== RUN   TestServer_CreateTodo
=== RUN   TestServer_CreateTodo/project_a_new_todo
--- PASS: TestServer_CreateTodo (0.00s)
    --- PASS: TestServer_CreateTodo/project_a_new_todo (0.00s)
PASS
coverage: 17.4% of statements
ok      github.com/KentaKudo/qiita-advent-calendar-2019 0.695s  coverage: 17.4% of statements
?       github.com/KentaKudo/qiita-advent-calendar-2019/internal/pb/service     [no test files]
?       github.com/KentaKudo/qiita-advent-calendar-2019/internal/schema [no test files]

無事テストが通りました:)
todoMgrがエラーを返すパターンもテストしますが、目新しいことはないので説明は割愛します。コミットを確認してみてください。


残るはstore構造体にtodoManagerインターフェイスを実装する部分ですが、少し長くなってきたのでまた明日にします。では。

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

構造体へのインターフェースの埋め込み

構造体へのインターフェースの埋め込みについての知識に漏れがあったので、調べたことをまとめました。

Goでは、インターフェースの明示的宣言はなく、インターフェースの持つメソッドを実装することでそのインターフェースを満たしていることになります(ダックタイピング)。

しかし、実はインターフェースを構造体に埋め込むことでも、例えその構造体はインターフェースの持つメソッドを持っていなくても、そのインターフェースを満たしていることになります。

下記のコードでは、user構造体はUserInterfaceを持っているのでUserInterfaceのもつFullName()を持っていなくてもUserInterfaceを満たしていることになります。よって、下記のコードはビルドが通ります。

func main() {
    taro := testUser{
        firstName: "Taro",
        lastName:  "Yamada",
    }
    outputUser(taro) //testUserがUserInterfaceを満たしているため、taroをoputputUserに渡せる
}

type UserInterface interface {
    FullName() string
}

type testUser struct {
    UserInterface       // interfaceを埋め込むことで、メソッドを実装しなくてもそのinterfaceを満たせる
    firstName, lastName string
}

func outputUser(u UserInterface) {
    fmt.Println(u.FullName())//userはFullNameメソッドを持っていないがコンパイルエラーにならない
}

しかし、ouputUserメソッド内で実際には実装されていないFullName()メソッドを呼んでいるため、実行時にはpanicが起きます。

❯ go run main.go
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x10992c7]

goroutine 1 [running]:
main.main()
        /Users/mmm/go/src/github.com/masalennon/test/main.go:13 +0x87
exit status 2

上記のコードに、FullNameメソッドを追加すれば、ちゃんと実行されます。

func (u user) FullName() string {
    return u.firstName + " " + u.lastName
}
// Taro Yamada

おわり

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