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

fmtとポインタのTips

はじめに

標準出力する際に fmt.Print を用いることがあると思います。ポインタを fmt.Println に渡すと興味深い結果が表示されることがあります。その例を紹介します。

まずは以下の実装例を見てください。単に fmt.Println で time.Now() の変数とそのポインタ型の変数を標準出力しているだけです。

package main

import (
    "fmt"
    "time"
)

func main() {
    t := time.Now()
    printTime(&t)
}

func printTime(t *time.Time) {
    fmt.Println(t)
    fmt.Println(*t)
}
  • 出力結果

上記の実装の出力結果は以下のようになります。ポインタの値を表示される想定でしたが、実際に表示されたのは time.Time の値でした。なぜでしょう。

2009-11-10 23:00:00 +0000 UTC m=+0.000000001
2009-11-10 23:00:00 +0000 UTC m=+0.000000001

https://play.golang.org/p/8_kqljv7LVL

理由

fmtが表示するもの

まず fmt.Println が何を表示するのか確認します。Package fmt によると %T%p を用いる場合を除いて、指定の順番で表示するようになっています。 fmt.Println に効いてくるのは 5 番目の内容です。

  1. If an operand implements method String() string, that method will be invoked to convert the object to a string, which will then be formatted as required by the verb (if any).

String() メソッドを実装している場合は、オブジェクトを文字列に変換するために String() が呼ばれます。time.Time は以下のように String() メソッドを実装していました。そのため fmt.Print() で標準出力するときは String() メソッドの返り値の文字列が表示されます。

func (t Time) String() string {
    s := t.Format("2006-01-02 15:04:05.999999999 -0700 MST")

    // Format monotonic clock reading as m=±ddd.nnnnnnnnn.
    if t.wall&hasMonotonic != 0 {
        m2 := uint64(t.ext)
        sign := byte('+')
        if t.ext < 0 {
            sign = '-'
            m2 = -m2
        }
        m1, m2 := m2/1e9, m2%1e9
        m0, m1 := m1/1e9, m1%1e9
        var buf []byte
        buf = append(buf, " m="...)
        buf = append(buf, sign)
        wid := 0
        if m0 != 0 {
            buf = appendInt(buf, int(m0), 0)
            wid = 9
        }
        buf = appendInt(buf, int(m1), wid)
        buf = append(buf, '.')
        buf = appendInt(buf, int(m2), 9)
        s += string(buf)
    }
    return s
}

ポインタのメソッドセット

time.Time は String() メソッドを保持しているので文字列を表示するのは分かりました。ではなぜ *time.Time が String() メソッドを保持しているのでしょうか?

Method sets から引用すると、以下が理由になります。

The method set of the corresponding pointer type *T is the set of all methods declared with receiver *T or T (that is, it also contains the method set of T).

つまり *T 型のメソッドセットは T 型と *T 型のメソッドセットを保持します。

よって今回の例では *time.Time 型の変数は time.Time 型で実装していた String() を保持しているので String() が返却する文字列を表示することができました。

参考

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

Go で DIP に従って依存関係を逆転させたいときは ifacemaker と moq の組み合わせが強い

はじめに

  • Go で DIP(依存関係逆転の法則)に従って依存関係を逆転させたくなったとき ifacemaker を使って構造体からインターフェイスを生成して moq を使ってインターフェイスからモックを作るのが便利

  • 構造体 User の値をメモリに登録・参照するリポジトリ UserMemoryRepository がある
  • UserMemoryRepository をサービスなどから利用したくなったとして、直接参照するのではなく UserMemoryRepository を抽象化したインターフェイスを参照するようにしたいとする
Userの定義
type User struct {
    ID   int
    Name string
}
UserMemoryRepositoryの定義
type UserMemoryRepository struct {
}

var userMemoryRepositoryMap = map[int]*User{}

func (r *UserMemoryRepository) Get(id int) (*User, bool) {
    u, ok := userMemoryRepositoryMap[id]
    return u, ok
}

func (r *UserMemoryRepository) Put(u *User) error {
    userMemoryRepositoryMap[u.ID] = u
    return nil
}

構造体からインターフェイスを生成する

  • UserMemoryRepository を抽象化して扱うために ifacemaker を使ってインターフェイスを生成する
実行コマンド
ifacemaker -f user.go -s UserMemoryRepository -i UserRepository -p user -o user_interface.go
生成されたインターフェイスUserRepository
package user

// UserRepository ...
type UserRepository interface {
    Get(id int) (*User, bool)
    Put(u *User) error
}

インターフェイスからモックを生成する

  • UserMemoryRepository のインターフェイスとして生成した UserRepository を使ったテストをしやすいように moq を使ってモックを生成する
実行コマンド
moq . UserRepository -out user_mock.go

生成されたモック
生成されたモック
// Code generated by moq; DO NOT EDIT.
// github.com/matryer/moq

package user

import (
    "sync"
)

var (
    lockUserRepositoryMockGet sync.RWMutex
    lockUserRepositoryMockPut sync.RWMutex
)

// Ensure, that UserRepositoryMock does implement UserRepository.
// If this is not the case, regenerate this file with moq.
var _ UserRepository = &UserRepositoryMock{}

// UserRepositoryMock is a mock implementation of UserRepository.
//
//     func TestSomethingThatUsesUserRepository(t *testing.T) {
//
//         // make and configure a mocked UserRepository
//         mockedUserRepository := &UserRepositoryMock{
//             GetFunc: func(id int) (*User, bool) {
//                 panic("mock out the Get method")
//             },
//             PutFunc: func(u *User) error {
//                 panic("mock out the Put method")
//             },
//         }
//
//         // use mockedUserRepository in code that requires UserRepository
//         // and then make assertions.
//
//     }
type UserRepositoryMock struct {
    // GetFunc mocks the Get method.
    GetFunc func(id int) (*User, bool)

    // PutFunc mocks the Put method.
    PutFunc func(u *User) error

    // calls tracks calls to the methods.
    calls struct {
        // Get holds details about calls to the Get method.
        Get []struct {
            // ID is the id argument value.
            ID int
        }
        // Put holds details about calls to the Put method.
        Put []struct {
            // U is the u argument value.
            U *User
        }
    }
}

// Get calls GetFunc.
func (mock *UserRepositoryMock) Get(id int) (*User, bool) {
    if mock.GetFunc == nil {
        panic("UserRepositoryMock.GetFunc: method is nil but UserRepository.Get was just called")
    }
    callInfo := struct {
        ID int
    }{
        ID: id,
    }
    lockUserRepositoryMockGet.Lock()
    mock.calls.Get = append(mock.calls.Get, callInfo)
    lockUserRepositoryMockGet.Unlock()
    return mock.GetFunc(id)
}

// GetCalls gets all the calls that were made to Get.
// Check the length with:
//     len(mockedUserRepository.GetCalls())
func (mock *UserRepositoryMock) GetCalls() []struct {
    ID int
} {
    var calls []struct {
        ID int
    }
    lockUserRepositoryMockGet.RLock()
    calls = mock.calls.Get
    lockUserRepositoryMockGet.RUnlock()
    return calls
}

// Put calls PutFunc.
func (mock *UserRepositoryMock) Put(u *User) error {
    if mock.PutFunc == nil {
        panic("UserRepositoryMock.PutFunc: method is nil but UserRepository.Put was just called")
    }
    callInfo := struct {
        U *User
    }{
        U: u,
    }
    lockUserRepositoryMockPut.Lock()
    mock.calls.Put = append(mock.calls.Put, callInfo)
    lockUserRepositoryMockPut.Unlock()
    return mock.PutFunc(u)
}

// PutCalls gets all the calls that were made to Put.
// Check the length with:
//     len(mockedUserRepository.PutCalls())
func (mock *UserRepositoryMock) PutCalls() []struct {
    U *User
} {
    var calls []struct {
        U *User
    }
    lockUserRepositoryMockPut.RLock()
    calls = mock.calls.Put
    lockUserRepositoryMockPut.RUnlock()
    return calls
}

これまでのコード生成を go generate で実行する

  • 構造体からインターフェイスの生成、インターフェイスからモックの生成、それぞれを go generate コマンドひとつでまとめて実行できるように go:generate コメントを追記してく
UserMemoryRepositoryの定義の上にコメントを追記
//go:generate ifacemaker -f user.go -s UserMemoryRepository -i UserRepository -p user -o user_interface.go
//go:generate moq -out user_mock.go . UserRepository
type UserMemoryRepository struct {

モックを使ってテストする

  • 生成したモックを使ってテストを書いてみる
  • moq で生成したモック構造体を使う場合は初期化時に必要な関数の実装を関数リテラルで指定するだけなので直感的
package user

import "testing"

func TestUserRepository_Get(t *testing.T) {
    r := UserRepositoryMock{
        GetFunc: func(id int) (*User, bool) {
            return &User{}, true
        },
    }
    if _, ok := r.Get(100); !ok {
        t.Fatal("Get must ok.")
    }
}

おわりに

  • testing はテスト用のミニ言語とか読むのも覚えるのもだるいからやめようぜっていう方向性なのに gomock が公式みたいな顔して独自の記法をゴリ押ししてくるのが納得いってないので moq を使ってやればいいとおもいました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

個人開発のWebサービスをリリースして2ヶ月で見つかったバグを一挙紹介する

この記事は、ひとり開発 Advent Calendar 2019の2日目の記事になります。
1日目の記事はhimataniさんのひとり開発でも諦めない、これからのプロダクトのつくり方でした。

はじめに

こんにちは、ぷらす (@p1ass)です。

皆さんは個人開発をする時にしっかりとテストやデバッグをしていると言い切れますか?

私は趣味レベルで開発しているときはスピード優先でテストをないがしろにしていまうときがあります。しかし、大抵バグを埋め込んでしまい、後々大変なことになってしまいます

この記事では、私が個人で開発しているMemoito(めもいと)を開発・運用していく上で見つかったバグ・不具合を自戒を込めて一挙紹介していきます。

この記事を読んで、今一度、自分の開発しているサービスを見直すきっかけになれば幸いです。

Memoitoとは

最初に、この記事で紹介していくバグが発見されたWebサービスであるMemoitoについて先に紹介しておきます。

MemoitoはTwitter連携を用いて、メモをフォローしている人と紐付けて保存することができるWebサービスです。

紹介資料

Twitterで気軽に連絡先を交換できるようになりましたが、徐々にフォローしている人が増えてきて、「この人誰だっけ?」となる経験はないでしょうか?

Memoitoはフォローしている人と紐付けてメモを保存できるので、スマホの標準メモアプリでメモするより簡単にメモを取ることができます。

勉強会やミートアップ、カンファレンス等で会った人のことを、その時話した内容と一緒にメモを取れば、後から見直すことができてとても便利です。

公式Twitterもあります。

アーキテクチャ

サービスのアーキテクチャが分かっていた方が記事を読みやすいと思うので、軽く説明します。詳しいことは、今度書く予定の記事を参照してもらうとして、こんな感じのアーキテクチャになっています。

フロントエンド

  • Nuxt.js
  • Netlify

バックエンド

  • Go

インフラ

  • GKE
  • CloudSQL

デプロイはCircleCIに寄せていて、Dockerイメージのpushやkubectl applyなどを実行しています。また、GCPのインフラ構成定義にはTerraformを使っています。

個人ではオーバーエンジニアリングですが、頑張って今風なアーキテクチャにしています。

見つかったバグ

さて、ここからは見つかったバグを紹介していきます。

リリース前の検証ユーザがDBに残っている

問題

リリース前に無効なIDを持つTwitterのユーザ情報をインサートしてしまっていて、それがDBに残ったままでした。

mysql> select id from twitter_users where id = 0;
+----+
| id |
+----+
|  0 |
+----+
1 row in set (0.04 sec)

ここでのidはTwitter側の内部IDと同じ値なので、0になることはありません。
おそらく、ゼロ値の構造体をそのままインサートしてしまっていたが原因と考えられます。

対応

直接本番DBのコンソールからrowを消しました。

ただ、この作業はさくっとSQLを1回発行するだけではダメでした。
SQLの制約がいくつか張られていたので、他のテーブルのrowを先に削除する必要がありました。

間違って他のユーザのデータを消すわけにはいかないので、入念に調査をして、冪等性があるクエリでrowを削除するようにしました。

無効な値を持つレコードがDBに格納されている

問題

これも上と似ていますが、本来存在し得ない値が書き込まれていました。

たとえ話ですが、ある状態を表すカラムstatusは本来、[0, 2]の値を取るはずが、3が書き込まれている、といった状況でした。

これは、JSON APIでPOSTされた値のバリデーションが漏れていたのが原因でした。

対応

全てのフィールドに対して、値が有効であるかを確認するバリデーション関数を実装し、テストもしっかりと書きました。

いくらクライアントサイドでバリデーションしているとはいえ、サーバ側でも実装しなきゃなという気持ちになりました(当たり前)。

1対1で対応しているはずのテーブルのレコード数が違う

問題

Memoitoは、将来的に他のSNSの対応を見据えているため、ユーザ情報は汎用的なusersテーブルとTwitter固有の情報を格納しているtwitter_usersテーブルに分けて保存しています。

現時点では、Twitterしか対応していないため、usersテーブルとtwitter_usersテーブルのrowの数は一致するはずですが、なぜか一致していませんでした。

mysql> select count(id) from users;
+-----------+
| count(id) |
+-----------+
|       200 |
+-----------+
1 row in set (0.05 sec)

mysql> select count(id) from twitter_users;
+-----------+
| count(id) |
+-----------+
|       184 |
+-----------+
1 row in set (0.04 sec)

これは、トランザクションが正しく貼れていなかったことが原因で、片方が失敗した場合に不整合が生じていました。

対応

まず、2つのテーブルに対するインサートを同じトランザクションで実行するようにアプリケーションを修正しました。

その後、おかしいrowを探し出し、SQLでパッチを当てていきました。

具体的には、

  1. 不必要なusersのrowを探す
  2. そのidが使われているメモ(notesテーブル)を探す
  3. notesuser_idを正しいものに変更する
  4. 参照が全てなくなった不必要なusersのrowを削除

という手順で対応しました。

これが、一番修正が面倒くさいバグでした。
おかしなrowの洗い出しが大変でしたし、SQLの制約を考慮しつつパッチを当てなくてないけないのでなかなか骨の折れる作業でした?

Twitterのプロフィールが更新されない (未解決)

問題

MemoitoではTwitterのプロフィールをこちら側のDBに保存しています。
登録ユーザはログインするたびに新しいプロフィールに更新されるようになっています。

しかし、メモを取った相手は必ずしも登録ユーザではないため、プロフィールが更新されていませんでした。

結果として、プロフィールアイコンが404で表示されないという不具合がありました。

対応

実はこの不具合はまだ修正できていません。せっせと実装中です。

DBに保存されているユーザの全てのプロフィール情報をTwitter APIから取得して、プロフィール情報を更新する方法を考えています。

おそらく、一番オーソドックスな方法なのではないでしょうか。他に良い方法があれば教えて下さい。

おわりに

ここまで、リリースしてから見つかったバグを紹介してきました。

特に整合性周りがきちんと実装していないと、不具合が生まれがちなので次からはしっかりと考えて実装しようと思いました。(対応大変なので、、、)

皆さんもこの記事を反面教師として、今一度自分のアプリケーションを見直してみましょう。何かヤバいものが見つかるかもしれません、

最後になりますが、よかったらMemoito使ってみてください!

明日はひとり開発 Advent Calendar 2019はbinnmtiさんの担当です。お楽しみに。

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

Goを書く前に知っておくべき3つのこと

Goを書く前に知っておくべき3つのこと

言語について学習する以外で知っておくとあとあと人生が楽になることがいくつかあるので個人的に必須だと思っていること3つをサクっとリストアップしてみました。

1. 環境の準備

Goの面倒な要素として $GOPATH という変数(未設定の場合は $HOME/go)が指すディレクトリの下に全てのGoのソースコードが(少なくともエクスポートしたいものに関しては)入っている必要があります。変なこと(たとえばiCloudやDropBoxでコードをクラウドで同期するみたいなこと)をしない場合は未設定あるいは GOPATH=$HOME/go と明示的に設定してしまっていいと思います。合わせて PATH=$PATH:$GOPATH/bin も設定してしまいましょう。

$GOPATH 以下の階層は大雑把に以下のように構造化されています。

  • $GOPATH/
    • bin/ go get などでインストールされたバイナリ郡
    • pkg/ 標準ライブラリ
    • src/ その他のGoコード

ここでポイントになるのは src 以下の構造です。本来であれば src/myprojects などとディレクトリを切りたいところですが、そうもいきません。というのも src 以下にはまずドメイン名が来るからです。「なんでや」と思われるかもしれませんがこの切り分けをすることでそのソースコードがどこから来ているかがわかりやすくなるというメリットがあります。

Goをインストールしたての場合は src 以下には何もないかと思います。そこで試しに以下のコマンドを実行してみてください。

$ go get github.com/ktnyt/assert

すると、$GOPATH/src/github.com/ktnyt/assert というディレクトリが生成されます。同様に go get doma.in/path... というコマンドを実行すると $GOPATH/src/doma.in/path... というディレクトリができます。なので、自分のコードは原則的に $GOPATH/src/github.com/<GitHub user name> というディレクトリ以下に作りましょう。

「現実的に github.com 以外のドメインからコード拾ってくることはあるの?」と思われるかもしれませんが実は結構あります。まず Go 公式のツールは大体 golang.org/x/tools/cmd/<toolname> にあります。あとはGitHubのレポジトリから勝手にバージョンタグを拾って適切にインストールしてくれる gopkg.in というサービスがあったり(例: gopkg.in/go-yaml/yaml.v2github.com/go-yaml/yaml というレポジトリのバージョンタグで v2, v2.x, または v2.x.x とついている中で最新のものを拾ってくる)と実は色々あります。

Go のディレクトリ構造やコード整理などについてより詳しいことは公式ドキュメントにまとまっているので一読することをおすすめします。

2. プロジェクトの構造

次に重要なのは個別のプロジェクトの構造です。Goのソースコードはプロジェクトディレクトリの直下に置いてくださいsrc などのディレクトリを切ることは非推奨です)。なぜかというとプロジェクトディレクトリ以下にディレクトリを切るとそれらのディレクトリはサブパッケージ扱いになるからです(github.com/<username>/<package>/srcsrc というパッケージになってしまう)。大量のファイルができてしまうことが憂慮される場合は適切にディレクトリを切ってサブパッケージ化したりプロジェクトを分けたりとコードの境界を意識して開発をする必要があります。

次にバイナリを配布したい場合はプロジェクトそのものを main パッケージにしてしまうことは基本的に避けましょう。cmd/<commandname> というディレクトリを切って go get github.com/<usermame>/<package>/cmd/<commandname> とさせましょう。もちろん cmd/<command1> cmd/<command2> のように複数のコマンドを作ることも可能です。

他にも慣例的にいくつかのディレクトリを切ることがあります。例としてパッケージ内に参照範囲を限定する internal やバージョンをフリーズしてコードを配布するための vendor などがあります。Go プロジェクトの標準レイアウトは https://github.com/golang-standards/project-layout にテンプレートがあるので一度ご覧になることをおすすめします。

3. goimports

Goを書くときにストレスになりがちなことのひとつにインポート管理があります。未インポートのAPIを参照すると当然コンパイルエラーになりますが未使用のパッケージがインポートされているのもまたコンパイルエラーになります。Goと同梱のフォーマッタである gofmt は便利ですが実は goimports というインポートを自動的に挿入・削除してくれる上位互換があります。

$ go get golang.org/x/tools/cmd/goimports

このコマンドを実行すれば $GOPATH/bin にバイナリがインストールされます。あとはお好みのエディタのフォーマッタコマンドに goimports を指定すればよしなにインポートを解決してくれるのでとても便利です。

まとめ

TL;DR

Happy Hacking!

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

01. 「パタトクカシーー」

01. 「パタトクカシーー」

「パタトクカシーー」という文字列の1,3,5,7文字目を取り出して連結した文字列を得よ.

Go

package main

import "fmt"

func main() {
    var src string = "パタトクカシーー";
    var des string = "";

    //  rune 型へ変換
    msrc := []rune(src);

    //  1.ループで処理
    for i := 0; i <= 7; i++ {
        //  問題では 1,3,5,7 指定だが奇数判定で対応
        if (i % 2) != 0 {
            des += string(msrc[i]);
        }
    }
    fmt.Println(des);
}

python

# -*- coding: utf-8 -*-
src = u"パタトクカシーー"
des = ""

#   1.ループで処理(range)
reverse = ""
for i in range(len(src)):
    # 問題では 1, 3, 5, 7 指定だが奇数判定で対応
    if i % 2 != 0 :
        des += src[i]
print(des)

# -*- coding: utf-8 -*-
src = u"パタトクカシーー"
des = ""

#   1.ループで処理(range)
reverse = ""
for i in range(len(src)):
    # 1, 3, 5, 7 指定だが奇数判定で対応
    if i % 2 != 0 :
        des += src[i]
print(des)

#   2.スライスで処理(ステップを指定)
print(src[1::2])

Javascript

var src = "パタトクカシーー";
var dsc = ""

//  1.ループで処理
for (var i = 0; i < src.length; i++) {
    //  問題では 1, 3, 5, 7 指定だが奇数判定で対応
    if (i % 2 != 0) {
        dsc += src[i];
    }
}
console.log(dsc);

まとめ

単純なループ処理しか思いつかない。
他のロジックって有るのかな?。100本ノックの記事探してみる。
1,3,5,7 を渡すと、切り出してくれる標準関数とか有るのかな?。

トップ

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

言語処理100本ノック 2015 をやってみた 00

言語処理100本ノック 2015 をやってみた

動機と目的

新たな言語を勉強してみるが使わないと忘れていく。
Qiita で 100本ノックの記事を見た。なるほど。やってみよう。
言語処理100本ノック 2015

目標

毎日(たぶん)、数問を解くことでそれぞれの言語に慣れるようにしてみる。
Qiita に書くことで Markdown に慣れる。

言語

  • Go
  • Python
  • Javascript

を主にやってみる。

00.文字列の逆順

Go

package main

import_ "fmt"

func main() {
    var data string = "stressed"
    var reverse string

    // 1.ループ
    for i := len(data) - 1; i >= 0; i-- {
        reverse += string(data[i])
    }

    fmt.Println(reverse)
}

python

# -*- coding: utf-8 -*-
data = "stressed"
reverse = ""

#   1.逆カウントのループ
i = len(data) - 1
while i >= 0:
    reverse += data[i]
    i-=1
print(reverse)

#   2.range->reversed
reverse = ""
for i in reversed(range(len(data))):
    reverse += data[i]
print(reverse)

#   3.reversed->join
print(''.join(reversed(data)))

#   4.スライス
print(data[::-1])

Javascript

var data = "stressed";
var reverse = ""

//  1.逆カウントのループ
for (var i = data.length - 1; i >= 0; i--) {
    reverse += data[i];
}
console.log(reverse);

//  2.split->reverse->join
console.debug(data.split("").reverse().join(""));

まとめ

Javascript以外は日本語非対応。
後でマルチバイト対応予定。

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

00. 文字列の逆順

00.文字列の逆順

文字列"stressed"の文字を逆に(末尾から先頭に向かって)並べた文字列を得よ.

Go

package main

import_ "fmt"

func main() {
    var data string = "stressed"
    var reverse string

    // 1.ループ
    for i := len(data) - 1; i >= 0; i-- {
        reverse += string(data[i])
    }

    fmt.Println(reverse)
}

python

# -*- coding: utf-8 -*-
data = "stressed"
reverse = ""

#   1.逆カウントのループ
i = len(data) - 1
while i >= 0:
    reverse += data[i]
    i-=1
print(reverse)

#   2.range->reversed
reverse = ""
for i in reversed(range(len(data))):
    reverse += data[i]
print(reverse)

#   3.reversed->join
print(''.join(reversed(data)))

#   4.スライス
print(data[::-1])

Javascript

var data = "stressed";
var reverse = ""

//  1.逆カウントのループ
for (var i = data.length - 1; i >= 0; i--) {
    reverse += data[i];
}
console.log(reverse);

//  2.split->reverse->join
console.debug(data.split("").reverse().join(""));

まとめ

goは日本語非対応。
問題ページとトップページ分割。

トップ

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

Golang - Incorrect CPU Numbers of GOMAXPROCS in Docker

The runtime.GOMAXPROCS(??) will read the host machine's CPU numbers instead of container's (--cpus=??). We can use UBER's automaxprocs to solve it.

go get -u go.uber.org/automaxprocs
import _ "go.uber.org/automaxprocs"

func main() {
  // Your application logic here.
}

https://github.com/uber-go/automaxprocs

https://mp.weixin.qq.com/s/Lk1EbiT7WprVOyX_dXYMyg

http://vearne.cc/archives/39195

https://colobu.com/2017/10/11/interesting-things-about-GOMAXPROCS/

http://www.ojit.com/article/204492

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

CockroachDBへのアクセスとDBマイグレーション

今日はアプリからCockroachDBにアクセスしてみたいと思います。ついでにDBマイグレーションにも触れます。

DBコネクションの作成

DBへのアクセスには標準のdatabase/sqlパッケージを使います。使用するドライバーはPostgresのものを使います。
ということでmain.goで以下のパッケージを読み込みます。

main.go
import (
        log "github.com/sirupsen/logrus"
        "github.com/utilitywarehouse/go-operational/op"
        "google.golang.org/grpc"
+
+       _ "github.com/lib/pq"
)

store.goファイルを作成し、DBに接続するための関数initDB()を以下のように定義します。

store.go
package main

import (
    "database/sql"

    "github.com/pkg/errors"
)

func initDB(connString string) (*sql.DB, error) {
    db, err := sql.Open("postgres", connString)
    if err != nil {
        return nil, err
    }

    if err := db.Ping(); err != nil {
        return nil, errors.Wrap(err, "ping")
    }
    return db, nil
}

main.goからの呼び出しはこんな感じです。

main.go
func main() {

        ...

+       dbURL := app.String(cli.StringOpt{
+               Name:   "db-url",
+               Desc:   "cockroachdb url",
+               EnvVar: "DB_URL",
+               Value:  "postgresql://root@localhost:26257/test?sslmode=disable",
+       })
+
        app.Action = func() {
                log.WithField("git_hash", gitHash).Println("Hello, world")

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

                ...

ではapp.yamlで環境変数DB_URLを定義してリソースを更新し、起動時にエラーが出ないことを確認してみましょう。

app.yaml
         env:
         - name: SRV_PORT
           value: "8080"
+        - name: DB_URL
+          value: "postgres://qiita_advent_calendar_2019@cockroachdb-proxy:26257/qiita_advent_calendar_2019_db?sslmode=disable"
         ports:
         - containerPort: 8080
           name: srv
$ kubectl apply -f kubernetes/app.yaml
service/qiita-advent-calendar-2019 unchanged
deployment.apps/qiita-advent-calendar-2019 configured

$ kubectl -n qiita logs qiita-advent-calendar-2019-5bc6786c75-gbr5s qiita-advent-calendar-2019
time="2019-11-26T18:24:23Z" level=info msg="Hello, world" git_hash=bb1916d14726fbd4d3e98b333d54ccfad45f824d

特にエラーがないため接続できているようです。

テーブルスキーマ定義

せっかくDBに接続できたので、テーブルを作成してみましょう。
internal/schemaパッケージを切り、テーブルスキーマ定義用のファイルsql.goを作成します。

internal/schema/sql.go
package schema

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

CREATE TABLE schema_version (
        id        SMALLSERIAL NOT NULL PRIMARY KEY,
        md_insert BIGINT      DEFAULT  EXTRACT(EPOCH FROM current_timestamp)::INT,
        md_update BIGINT      DEFAULT  0,
        md_curr   BOOL        DEFAULT  'true'
);

INSERT INTO schema_version VALUES (0);

COMMIT;
`,
}

先にschema_versionテーブルを書いてしまいました。
僕のチームでは特別マイグレーション用のライブラリを使用しておらず、このテーブルでスキーマのバージョン管理をしてます。
マイクロサービスにより、一つ一つのデータベースの規模は小さく、スキーマが更新される頻度はモノリスのアプリケーションに比べて少ないため、今のところはこれで事足りているという状況です。

ではマイグレーション用の関数Migrate()をみていきましょう。
関数はinternal/schema/schema.goに置いておきます。

internal/schema/schema.go
package schema

import (
    "database/sql"

    "github.com/pkg/errors"
)

// Migrate sets the schema at the requested version
func Migrate(db *sql.DB, expectedVersion int) error {
    currentVersion, err := Version(db)
    if err != nil {
        return err
    }
    switch {
    case expectedVersion == currentVersion:
        return nil
    case expectedVersion < currentVersion:
        return errors.Errorf("schema migrate: invalid request, can not migrate backwards, current: %v, expected: %v", currentVersion, expectedVersion)
    case expectedVersion > currentVersion:
        for ; expectedVersion > currentVersion; currentVersion++ {
            if _, err := db.Exec(schemas[currentVersion+1]); err != nil {
                return errors.Wrap(err, "exec migration")
            }
        }
    }
    return nil
}

// Version returns the current schema version
func Version(db *sql.DB) (v int, err error) {
    if err := db.QueryRow("SELECT id FROM schema_version WHERE md_curr = true").Scan(&v); err != nil {
        if err.Error() == `pq: relation "schema_version" does not exist` {
            return -1, nil
        }
    }
    return v, errors.Wrap(err, "select version")
}

この関数では指定されたバージョンまでのSQLを順に適用します。

store構造体の定義とマイグレーションの実行

ではstore.goファイルに戻り、DBアクセス用のメソッドを定義するstore構造体を定義し、その初期化処理時に上記のMigrate()関数を呼び出すようにしましょう。

store.go
package main

import (
    "database/sql"

    "github.com/KentaKudo/qiita-advent-calendar-2019/internal/schema"
    "github.com/pkg/errors"
)

...

type store struct {
    db *sql.DB
}

func newStore(db *sql.DB, version int) (*store, error) {
    if err := schema.Migrate(db, version); err != nil {
        return nil, errors.Wrap(err, "migrate db schema")
    }

    return &store{db: db}, nil
}

main.goではこの初期化関数を呼び出すだけです。

main.go
package main

...

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

...

func main() {

    ...

    schemaVersion := app.Int(cli.IntOpt{
        Name:   "schema-version",
        Desc:   "schema version",
        EnvVar: "SCHEMA_VERSION",
        Value:  defaultSchemaVersion,
    })

    app.Action = func() {

        ...

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

        ...
}

さてでは先ほどと同様にアプリをデプロイしなおしてエラーが出ないことを確認してみましょう。

$ kubectl -n qiita scale --replicas=0 deployment qiita-advent-calendar-2019
deployment.extensions/qiita-advent-calendar-2019 scaled
$ kubectl -n qiita scale --replicas=1 deployment qiita-advent-calendar-2019
deployment.extensions/qiita-advent-calendar-2019 scaled

$ kubectl -n qiita logs qiita-advent-calendar-2019-5bc6786c75-lx4zk qiita-advent-calendar-2019
time="2019-11-26T19:09:37Z" level=info msg="Hello, world" git_hash=395149f7219511f2d72a09cd935e53f56fd29255

エラーが出ていません。マイグレーションが正常に終了したようです。
昨日同様に直接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: 867.116µs

root@localhost:26257/qiita_advent_calendar_2019_db> show tables;
    table_name    
+----------------+
  schema_version  
(1 row)

Time: 6.743223ms

root@localhost:26257/qiita_advent_calendar_2019_db> show columns from schema_version;
  column_name | data_type | is_nullable |                          column_default                           | generation_expression |   indices   | is_hidden  
+-------------+-----------+-------------+-------------------------------------------------------------------+-----------------------+-------------+-----------+
  id          | INT       |    false    | unique_rowid()                                                    |                       | {"primary"} |   false    
  md_insert   | INT8      |    true     | extract('epoch':::STRING, current_timestamp():::TIMESTAMPTZ)::INT |                       | {}          |   false    
  md_update   | INT8      |    true     | 0:::INT                                                           |                       | {}          |   false    
  md_curr     | BOOL      |    true     | true                                                              |                       | {}          |   false    
(4 rows)

Time: 21.007097ms

root@localhost:26257/qiita_advent_calendar_2019_db> select * from schema_version;
  id | md_insert  | md_update | md_curr  
+----+------------+-----------+---------+
   0 | 1574795377 |         0 |  true    
(1 row)

Time: 73.755494ms

無事テーブルが作成されていていい感じです。


今日はちょっといろいろやりました。
駆け足になってしまいましたがこれでDBのセットアップは完了したので明日、POSTエンドポイントを実装したいと思います。

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