20210314のGoに関する記事は10件です。

17rep - GolangでMySQLのトランザクションラッパー作成

使うシーン

  • 一括で複数のInsert Updateで全部成功させないといけないシーン
  • リレーションを貼っているテーブルに対してデータの更新や追加などを一括して追加変更しないといけないテーブルが存在するシーン

Go言語でトランザクション制御

  • トランザクション制御のラッパー関数の作成
  • トランザクションラッパー関数を開始し、エラーだったらrollbackを実行エラーじゃなければcommit

Begin()、Commit()、Rollback()が同じ関数内に定義(これは冗長)

  • SqlHandlerポインタレシーバからDB接続を定義し同じ関数内で実行
func (h *SqlHandler) WithTransaction() (err error) {
    tx, err := h.db.Begin()
    if err != nil {
        return
    }
    defer func() {
        if err != nil {
            tx.Rollback()
            return
        }
        err = tx.Commit()
    }()
    if _, err = tx.Exec(...); err != nil {
        return
    }
    if _, err = tx.Exec(...); err != nil {
        return
    }
    // ...
    return
}

func (t *sql.Tx)Exec(...){
    // Insert or Update SQL
    // ...
}

トランザクションラッパー関数

  • 実行したい関数を引数 txFuncとして受け取り、トランザクションを開始してからtxFuncを実行
  • txFuncの中でerrorもしくはpanicが起きたらdeferでrecoverを使って広い、rollbackを実行
  • 問題がなければcommitという流れです。
  • トランザクション制御を行う際に、トランザクション境界線がリクエストの開始〜終了となりますが、ラッパー関数方式の場合トランザクション境界を柔軟に変更可能
func (t *sqlRepository) ExecWithTx(txFunc func(*sql.Tx) error) (err error) {
    tx, err := t.db.Begin()
    if err != nil {
        return
    }
    defer func() {
        if p := recover(); p != nil {
            tx.Rollback()
        } else if err != nil {
            tx.Rollback() 
        } else {
            err = tx.Commit() 
        }
    }()
    err = txFunc(tx)
    return err
}

トランザクションラッパー関数使い方(サンプル)

  • 実行したいSQL関数を引数txFunc内に記述
func (r *Repository) DoSomething() error {
    return r.DB.ExecWithTx(func(tx *sql.Tx) error {
        if _, err := tx.Exec(...); err != nil { // ここにSQL系関数定義
            return err
        }
        if _, err := tx.Exec(...); err != nil { // ここにSQL系関数定義
            return err
        }
        return nil
    })
}

参考資料

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

【Go言語/golang】ブロックチェーンの実装

※備忘録として記載しますので、ご容赦ください。

package main

import (
    "bytes"
    "crypto/sha256"
    "fmt"
    "strconv"
    "time"
)

type Block struct {
    Timestamp     int64
    Data          []byte
    PrevBlockHash []byte
    Hash          []byte
}

func (b *Block) SetHash() {
    timestamp := []byte(strconv.FormatInt(b.Timestamp, 10))
    headers := bytes.Join([][]byte{b.PrevBlockHash, b.Data, timestamp}, []byte{})
    hash := sha256.Sum256(headers)

    b.Hash = hash[:]
}

func NewBlock(data string, prevBlockHash []byte) *Block {
    block := &Block{time.Now().Unix(), []byte(data), prevBlockHash, []byte{}}
    block.SetHash()
    return block
}

type Blockchain struct {
    blocks []*Block
}

func (bc *Blockchain) AddBlock(data string) {
    prevBlock := bc.blocks[len(bc.blocks)-1]
    newBlock := NewBlock(data, prevBlock.Hash)
    bc.blocks = append(bc.blocks, newBlock)
}

func NewGenesisBlock() *Block {
    return NewBlock("Genesis Block", []byte{})
}

func NewBlockchain() *Blockchain {
    return &Blockchain{[]*Block{NewGenesisBlock()}}
}

func main() {
    bc := NewBlockchain()

    bc.AddBlock("Send 1 BTC to Ivan")
    bc.AddBlock("Send 2 more BTC to Ivan")

    for _, block := range bc.blocks {
        fmt.Printf("Prev. hash: %x\n", block.PrevBlockHash)
        fmt.Printf("Data: %s\n", block.Data)
        fmt.Printf("Hash: %x\n", block.Hash)
        fmt.Println()
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Go/golang】sync.Mutex / sync.WaitGroupで排他制御をする

※本記事は備忘録として記述しておりますので、ご容赦ください。

sync.Mutex

次のコードは、IDを作成するコードが同時に1つしか実行されないようにしています。
※goroutineにmutex構造体を渡すときは、必ずポインタでわたすこと。
値のまま渡してしまうと、ロックしている状態のまま別のsync.Mutexインスタンスを渡すことになってしまうので、注意が必要です。

package main

import (
    "fmt"
    "sync"
    "time"
)

var id int

func generateID(mutex *sync.Mutex) int {
    // Lock() / Unlock()をペアで呼び出してロックする
    mutex.Lock()
    defer mutex.Unlock()

    id++
    result := id

    return result
}

func main(){
    // sync.Mutex構造体の変数宣言
    // 次の宣言をしてもポインタ型になるだけで正常に動作する
    // mutex := new(sync.Mutex)
    var mutex sync.Mutex

    for i := 0; i < 100; i++ {
        go func() {
            fmt.Printf("id: %d\n", generateID(&mutex))
        }()
    }
    // 100まで出力させたいなら終了待ちの操作が必要
    // time.Sleep(4 * time.Second)
}
# 実行結果
id: 1
id: 5
id: 6
id: 2
id: 7
id: 4
id: 3
id: 8
id: 10
id: 11
id: 12
id: 13
id: 14
id: 15
id: 16
id: 17
id: 18
id: 9
id: 19
id: 20
...
省略

# goroutineの終了待ちをしていないので、100個に到達する前に終了してしまうことがある

ロックしたままの値を渡していないか確認するには、go vetを実行することで見ることができる。

Mutexとチャネルの使い分け

  • チャネルが有用な場合
    • 非同期で結果を受け取る場合
  • Mutexが有用な場合
    • キャッシュ、状態管理

sync.WaitGroup

多数のgoroutineで実行しているジョブの終了待ちに使います。
※goroutineにwg構造体を渡すときは、Mutex同様必ずポインタでわたすこと。

package main

import (
    "fmt"
    "sync"
)

func main() {
    var wg sync.WaitGroup

    // ジョブ数をあらかじめ登録 (必ずgoroutineを作成する前に呼ぶ)
    // Doneが呼ばれるたびに、数がデクリメントされていく
    // Waitはこの中の数が0になるのを待機している
    wg.Add(2)

    go func() {
        // 非同期で仕事をする
        fmt.Println("仕事1")
        // Doneで完了を通知
        wg.Done()
    }()

    go func() {
        // 非同期で仕事をする
        fmt.Println("仕事2")
        // Doneで完了を通知
        wg.Done()
    }()

    // 全ての処理が終わるのを待つ
    wg.Wait()
    fmt.Println("完了")
}
# 出力結果 (仕事1と仕事2は順不同で出力される)
仕事2
仕事1
完了

# Mutexと違い、終了待ちをしているため、確実にgoroutineの終了を確認できる

WaitGroupとチャネルの使い分け

  • WaitGroupの方が良い場合
    • ジョブ数が大量にある場合
    • 可変個の場合
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GoでO/Rマッパーと生SQLの違いを比較してみた

はじめに

Ruby on Railsを学習している方はActiveRecordという名称のORマッパーを目にする事が度々あると思います。
私もプログラミングの学習をRubyから始めたのですが、ORマッパーと聞くとなんとなく「DBからのデータの取り出しを簡単にやってくれているんだな」くらいの認識でした。
Railsがあまりに様々な事をよしなにしてくれるので、Rails初学者の方は生のSQLを書く機会もほとんどないのではないかと思います。
最近Goを勉強し始めた事で、そのRailsがよしなにしてくれていた部分を基本から学び直す必要が出てきたので、ORMに関しても一つ学び直してここに備忘録として残したいと思います。

対象者

  • プログラミング初学者
  • SQLは書いた事があるがORMは利用した事がない。あるいはORMは利用した事があるが生のSQLを書く事はあまりない。

概要

Goのデータベースとのやり取りを、生のSQLで行った場合とORマッパーを利用した場合とでどうコードの違いが出るのかを比較して見ていきます。
Goを扱っていますが、簡単なコードなのでGoを触った事が無い方でも理解出来ると思います。
データベースはPostgreSQL、ORMはgormを使用します。

環境

Go 1.16
PostgreSQL 13.0

コード全容

store.goが生SQLで記述した場合。
store2.goがORMを使用した場合のコードです。

setup.sql
create table users (
    id serial primary key,
    name text
);

create table posts (
    id serial primary key,
    content text,
    user_id integer references users(id)
);
store.go
package main

import (
    "database/sql"
    "fmt"

    _ "github.com/lib/pq"
)

// テーブルに対応した構造体を定義
type User struct {
    Id    int
    Name  string
    Posts []Post
}
type Post struct {
    Id      int
    Content string
    User    *User
}

// データベースへ接続
var Db *sql.DB
var err error

func init() {
    Db, err = sql.Open("postgres", "user=test dbname=test password=test sslmode=disable")
    if err != nil {
        panic(err)
    }
}

func main() {
    // ユーザデータの作成
    user := User{Name: "堀口太郎"}
    sql := "INSERT INTO \"users\" (name) values ($1) RETURNING id"
    Db.QueryRow(sql, user.Name).Scan(&user.Id)

    // ポストデータの作成
    post := Post{Content: "どうも皆さんこんにちは", User: &user}
    sql2 := "INSERT INTO posts (content, user_id) values ($1, $2)"
    Db.Exec(sql2, post.Content, post.User.Id)

    // ユーザの取得
    var readUser User
    sql3 := "SELECT id, name FROM users WHERE name = $1"
    usename := "堀口太郎"
    Db.QueryRow(sql3, usename).Scan(&readUser.Id, &readUser.Name)

    // ユーザからコメント取得
    var posts []Post
    sql4 := "SELECT Id, Content FROM posts WHERE user_id = $1"
    rows, _ := Db.Query(sql4, readUser.Id)
    for rows.Next() {
        post := Post{User: &readUser}
        rows.Scan(&post.Id, &post.Content)
        posts = append(posts, post)
    }
}
store2.go
package main

import (
    "fmt"

    "github.com/jinzhu/gorm"
    _ "github.com/lib/pq"
)

// テーブルに対応した構造体を定義
type User struct {
    Id    int
    Name  string
    Posts []Post
}
type Post struct {
    Id      int
    Content string
    UserId  int
}

// データベースへ接続
var Db *gorm.DB

func init() {
    var err error
    Db, err = gorm.Open("postgres", "user=test dbname=test password=test sslmode=disable")
    if err != nil {
        panic(err)
    }

    // 構造体の内容を基にテーブルを自動作成
    Db.AutoMigrate(&User{}, &Post{})
}

func main() {
    // ユーザデータの作成
    user := User{Name: "山田太郎"}
    Db.Create(&user)

    // ポストデータの作成
    post := Post{Content: "どうも皆さんこんにちは"}
    Db.Model(&user).Association("Posts").Append(post)

    // ユーザの取得
    var readUser User
    username := "山田太郎"
    Db.Where("name=$1", username).First(&readUser)

    // ユーザからコメント取得
    var posts []Post
    Db.Model(&readUser).Related(&posts)
}

ORMとは

オブジェクト関係マッピング(英: Object-relational mapping, O/RM, ORM)とは、データベースとオブジェクト指向プログラミング言語の間の非互換なデータを変換するプログラミング技法である。オブジェクト関連マッピングとも呼ぶ。実際には、オブジェクト指向言語から使える「仮想」オブジェクトデータベースを構築する手法である。 - Wikipedia

簡単に言うと、オブジェクト指向(Object)リレーショナルデータベース(Relational)対応付け(Mapping)を行う事でデータベースから取得するレコードデータをオブジェクト化し、直感的に扱えるようにしたものになります。

なぜそのような対応付けが必要になるかというと、現実世界のモデルに即したデータモデルであるオブジェクト指向と、データのCRUD処理に最適化されたデータモデルであるRDBではその設計思想の違いにより、インピーダンスミスマッチが発生してしまう為と言われています。

解説

※store.goが生SQLで作成したコード、store2.goがgormを使用したコードになります。

今回はとてもシンブルなテーブル構造のデータベースに対してCreate,及びReadを行った場合の違いを比較していきます。
データベース側はtestという名称のユーザ、同じくtestという名称のDBをそれぞれ作成しています。
テーブル以下のUserとPostの二つです。

User
id
name
Post
id
content
user_id

次に、この二つのテーブルを作成していきます。
ORMを利用しない場合は以下スクリプトファイルを作成し、データベース側でテーブルの作成を行います。(Go側で作成する事も可能ですが、今回はデータベース側行います。)

setup.sql
create table users (
    id serial primary key,
    name text
);

create table posts (
    id serial primary key,
    content text,
    user_id integer references users(id)
);

以下のコマンドを実行し、テーブルを作成します。

$ psql -U test -f .\setup.sql -d test

対して、ORMを利用した場合の処理を見ていきます。

store2.go
type User struct {
    Id    int
    Name  string
    Posts []Post
}
type Post struct {
    Id      int
    Content string
    UserId  int
}
store2.go
Db.AutoMigrate(&User{}, &Post{})

テーブルの作成処理を行っている部分は上記のコードになります。
レコードを格納するための構造体を定義する事で、その構造体を利用してテーブルの作成を行う事が出来ます。

次に実際のデータの操作を見ていきます。

Create

store.go
// ユーザデータの作成
user := User{Name: "堀口太郎"}
sql := "INSERT INTO \"users\" (name) values ($1) RETURNING id"
Db.QueryRow(sql, user.Name).Scan(&user.Id)

// ポストデータの作成
post := Post{Content: "どうも皆さんこんにちは", User: &user}
sql2 := "INSERT INTO posts (content, user_id) values ($1, $2)"
Db.Exec(sql2, post.Content, post.User.Id)
store2.go
// ユーザデータの作成
user := User{Name: "堀口太郎"}
Db.Create(&user)

// ポストデータの作成
post := Post{Content: "どうも皆さんこんにちは"}
Db.Model(&user).Association("Posts").Append(post)

Read

store.go
// ユーザの取得
var readUser User
sql3 := "SELECT id, name FROM users WHERE name = $1"
usename := "堀口太郎"
Db.QueryRow(sql3, usename).Scan(&readUser.Id, &readUser.Name)

// ユーザからコメント取得
var posts []Post
sql4 := "SELECT Id, Content FROM posts WHERE user_id = $1"
rows, _ := Db.Query(sql4, readUser.Id)
for rows.Next() {
    post := Post{User: &readUser}
    rows.Scan(&post.Id, &post.Content)
    posts = append(posts, post)
}
store2.go
// ユーザの取得
var readUser User
username := "堀口太郎"
Db.Where("name=$1", username).First(&readUser)

// ユーザからコメント取得
var posts []Post
Db.Model(&readUser).Related(&posts)

どのコードを比較しても、ORMを使用した場合は短く、そして直感的に理解しやすいコードになっているかと思います。

まとめ

実際にORMを利用した場合と生のSQLを利用した場合でどのような違いが出るのか簡単にではありますが比較してみました。
今回はとても簡単なコードで比較したので一見メリットしかないように見えますが、ORMにも様々なデメリットが存在しています。
今回はその点については触れませんが、調べてみると賛否両論色んな意見が出てくると思いますので興味がある方は是非調べて見て下さい。
また、私自身Goの学習を始めたばかりなので気になる点があれば是非ご指摘頂けると幸いです。

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

goroutineで値(変数)を共有する

go routineのスレッド間でデータを共有する

※本記事は備忘録として記載したものでありますため、ご容赦ください。

goroutineで動作している関数でデータを共有するには、
1. 引数として渡す方法
2. クロージャのローカル変数にキャプチャして渡す方法
があります。

package main

import (
    "fmt"
    "time"
)

func sub1(c int) {
    fmt.Println("share by arguments : ", c*c)
}

func main() {
    // 引数で渡す方法
    go sub1(10)

    // クロージャのキャプチャで渡す方法
    c := 20
    go func() {
        fmt.Println("share by capture : ", c*c)
    }()

    time.Sleep(time.Second)
}
# 結果
share by arguments :  100
share by capture :  400

クロージャのキャプチャ渡しの場合も、内部的には無名関数に引数が追加されて参照が渡されるため、厳密には同じ動きとなります。

forを使用してみる

package main

import (
    "fmt"
    "time"
)

func main() {
    tasks := []string{
        "cmake ..",
        "cmake . --build release",
        "cpack",
    }
    for _, task := range tasks {
        go func() {
            // goroutineが起動する時にはループが回り切って
            // 全部のtaskが最後のタスクになってしまう
            fmt.Println(task)
        }()
    }
    time.Sleep(time.Second)
}
# 結果
cpack
cpack
cpack

高速ではありますが、単純なループに比べてgoroutineの起動が遅いため、taskに"cpack"が入った状態でループが回ってしまう。
このような場合は、引数経由にして明示的に値をコントロールするべきです。

子goroutineから親goroutineへの値渡し

両者は共有アドレスを参照しているため、上書きに注意が必要です。

参考

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

DynamoDBでインクリメンタルなIDを管理する場合の実装

概要

状況としては以下の様な場合を想定

  • 各グループ毎にインクリメンタルなIDを管理する
  • グループの設定を管理するテーブルに最新のIDを保存

結論

  1. 更新式でADDを使ってインクリメントしてIDを生成する
    ※ただしIDの間が抜ける可能性がある

  2. IDの抜けが許容できない場合はトランザクションでUpdateとPutする
    ※Lockをを考慮する必要がある

1. インクリメントするやり方

サンプルはGoとguregu/dynamoを使用したコードとなります

var domainConfig DomainConfig
err := configTable.Update(hashkey, target).
    Add("CurrentID", 1).
    Value(&domainConfig)
if err != nil {
    // error処理
}

data.ID = domainConfig.CurrentID
err = dataTable.Put(data).Run()
if err != nil {
    // error処理
}

上記の書き方で現在のIDに+1した値が確実に取得できるので、このIDをデータにセットすればグループ毎にインクリメンタルなIDを使うことができる
ただし、取得した後のPut処理が失敗した場合、取得したIDが使われないケースはある

2. トランザクションを使って確実にIDをセットするやり方

DynamoDBのトランザクションはGet系と更新系でしかまとめられないので、Getしてから設定をUpdate、データをPutするところまで全てを同一のトランザクションに入れることはできない
なので条件付きのUpdate処理を使うことでデータが既に更新されている場合は更新しないようにする
以下の様にトランザクションを使ってUpdateとPutを実行し、かつ設定をUpdateする条件にCurrentIDが変更されていない時のみとすればIDが先に更新されてしまった場合はエラーとなるのでIDがずれることはなくなる
ただし、先に他の処理に更新されてしまった場合や、トランザクション処理中にDBアクセスした時はエラーとなるため、リトライ処理を検討する
大量のバッチ処理時にはロックをかけて複数の処理が同時に実行されないような工夫をしないと、大量にエラーとなってしまうので注意する

err := configTable.Get(hashkey, domainID).One(&domainConfig)
if err != nil {
    // error処理
}
data.ID = domainConfig.CurrentKey + 1

tx := db.WriteTx() // dbは *dynamo.DB
tx.Update(configTable.Update(hashkey, domainID).
    Set(currentID, key).
    If("$ = ?", "CurrentID", domainConfig.CurrentID)) // CurrentIDが変更されていない時のみ更新する
tx.Put(dataTable.Put(data))
err = tx.Run()
if err != nil {
    // error処理
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Goで実装したHyperledger FabricのChaincodeを単体テストしたい

はじめに

Hyperledger FabricのChaincodeをネットワークにデプロイする前に、機能チェックしておきたい。

ということで、Go初学者がFabricのChaincodeを題材にして、Goで単体テストする方法を書き残しました。

環境

  • Hyperledger Fabric v2.2.0
    • サンプルソース、バイナリツール、Dockerイメージのインストール方法はこちら
  • Go 1.15.8
  • Visual Studio Codeのおすすめ拡張機能

注意

本記事では、Chaincode開発用のGoパッケージとしてshimの利用を前提としていますが、v2.0以降ではshimより高水準なAPIとしてcontractapiが提供されています。
今後はこちらを使っていく方が望ましいかもしれません。

(参考)
What is the difference between fabric-chaincode-go and fabric-contract-api-go?
https://stackoverflow.com/questions/60900762/what-is-the-difference-between-fabric-chaincode-go-and-fabric-contract-api-go

準備知識

Goのパッケージについて

  • FabricのChaincode開発ではshimパッケージが提供するAPIを使います。

  • Goでは依存パッケージをimportで指定します。

import (
    "github.com/hyperledger/fabric-chaincode-go/shim"
)
  • コマンド$go mod tidyで、importで記載したパッケージをダウンロードできます。このときgo.modgo.sumという2つのファイルも更新されます。
    • go.modは依存パッケージを管理するファイル。
    • go.sumはパッケージに更新があったかを判断するためのファイルで、依存モジュールのチェックサムが記録される。
  • パッケージ自体は$GOPATH/pkg/mod配下に格納されます。
  • コマンド$go getでも任意のパッケージを引数に指定してダウンロードできます。ただし、$go mod tidyの方がimportで記載した依存パッケージに応じて過不足なくダウンロードできる点が良さそうです。

Goのテストについて

  • Goは標準で自動テストが組み込まれています。(コマンド$go test)
  • カバレッジやベンチマーク測定なども、オプションで組み込まれています。(-cover-bench
  • テストする上で命名規則が決まってます。
    • XXX.goがテスト対象の場合、テストプログラムのファイル名はXXX_test.goとする。
    • テスト関数名の接頭にTestをつける。
  • テストプログラムはテスト対象と同じパッケージにします。XXX.goと同ディレクトリにXXX_test.goを置いておくのが吉。

(参考)
はじめてのGoTest(Qiita)
https://qiita.com/marnie_ms4/items/e51cc6d879cc9ad07af3

GoによるChaincode開発/単体テスト

既に述べた通り、Chaincode開発ではshimパッケージを使います。
そして、Chaincodeの単体テストではshimtestパッケージを使います。

Fabric v1.4.xでの注意点

Fabric v1.4.xではパッケージ体系が異なるのでご注意ください。
Fabric v2.x以降から変更があったようです。

また、v1.4.xではshimに単体テスト用のAPIも含まれていて、shimtestは提供されていません。

v1.4.xで開発/単体テストする場合は、バージョンを指定してパッケージをダウンロードしましょう。

$ go mod init ...
$ go get github.com/hyperledger/fabric/core/chaincode/shim@v1.4

(参考)
Work with Hyperledger Fabric Chaincode Development Tools #4415
https://github.com/SAPDocuments/Tutorials/issues/4415

実践

ここではmarbles02(Tag v2.2.0)の単体テストを実行する方法を記します。(カバレッジ100%を目指すものではありません)

サンプルコードを使うのでテスト対象のmarbles_chaincode.goは実装完了しているものとします。

単体テスト

marbles_chaincode.goと同じディレクトリに、命名規則に従ってテストプログラム(空ファイル)を作成します。

$ cd fabric-samples/chaincode/marbles02/go/
$ touch marbles_chaincode_test.go

一部メソッドのみを対象とした、簡易なテストコードを実装します。
Chaincodeの実装ではバイト配列を扱うことが多いので、標準パッケージの他にも幾つかimportしてます。

marbles_chaincode.go
package main

import (
    "strconv" // 文字列変換を扱う標準パッケージ
    "strings" // 文字列を扱う標準パッケージ
    "testing" // テスト用の関数・構造体を扱う標準パッケージ

    simplejson "github.com/bitly/go-simplejson" // JSON型式を扱うパッケージ
    "github.com/stretchr/testify/assert"        // アサーション機能を扱うパッケージ
    funk "github.com/thoas/go-funk"             // Sliceに対する便利機能(Map, Filterなど)を扱うパッケージ

    "github.com/hyperledger/fabric-chaincode-go/shim"
    "github.com/hyperledger/fabric-chaincode-go/shimtest"
)

var simpleChaincode = new(SimpleChaincode)
var mockStub = shimtest.NewMockStub("Test Creation", simpleChaincode)

func TestInitMarble(t *testing.T) {
    t.Run("success InitMarble()", func(t *testing.T) {
        functionName := "initMarble"
        marbleName := "marble1"
        marbleColor := "blue"
        marbleSize := "35"
        marbleOwner := "tom"
        functionAndArgs := [][]byte{[]byte(functionName), []byte(marbleName), []byte(marbleColor), []byte(marbleSize), []byte(marbleOwner)}

        // Invoke Function
        res := mockStub.MockInvoke("InitMarble001_1", functionAndArgs)
        if res.Status != shim.OK {
            t.Error("Invoke", functionAndArgs, "failed", string(res.Message))
            t.FailNow()
        } else {
            functionAndArgsMap := funk.Map(functionAndArgs, func(b []byte) string { return string(b) })
            t.Log("Invoke", "(", strings.Join(functionAndArgsMap.([]string), ", "), ")", "successful", string(res.Message))
        }

        // Check StateDB
        bytes, err := mockStub.GetState(marbleName)
        if err != nil {
            t.Error("Failed to get state")
            t.FailNow()
        } else if bytes == nil {
            t.Error("State", marbleName, "failed to get value")
            t.FailNow()
        }

        state, err := simplejson.NewJson(bytes)
        assert.Equal(t, marbleName, state.Get("name").MustString())
        assert.Equal(t, marbleColor, state.Get("color").MustString())
        assert.Equal(t, marbleSize, strconv.Itoa((state.Get("size").MustInt())))
        assert.Equal(t, marbleOwner, state.Get("owner").MustString())
    })
}

func TestReadMarble(t *testing.T) {
    t.Run("success ReadMarble()", func(t *testing.T) {
        functionName := "readMarble"
        marbleName := "marble1"
        marbleColor := "blue"
        marbleSize := "35"
        marbleOwner := "tom"
        functionAndArgs := [][]byte{[]byte(functionName), []byte(marbleName)}

        // Invoke Function
        res := mockStub.MockInvoke("ReadMarble001_1", functionAndArgs)
        if res.Status != shim.OK {
            t.Error("Invoke", functionAndArgs, "failed", string(res.Message))
            t.FailNow()
        } else {
            functionAndArgsMap := funk.Map(functionAndArgs, func(b []byte) string { return string(b) })
            t.Log("Invoke", "(", strings.Join(functionAndArgsMap.([]string), ", "), ")", "successful", string(res.Message))
        }

        // Check Response
        payload := res.Payload
        value, _ := simplejson.NewJson(payload)
        assert.Equal(t, marbleName, value.Get("name").MustString())
        assert.Equal(t, marbleColor, value.Get("color").MustString())
        assert.Equal(t, marbleSize, strconv.Itoa((value.Get("size").MustInt())))
        assert.Equal(t, marbleOwner, value.Get("owner").MustString())
    })
}

依存パッケージダウンロード

テストコードを書いたら依存パッケージをダウンロードします。
go.modがなければ$ go mod initで作成する必要があります。
ただし、marbles02(Tag v2.2.0)では既にgo.modも含まれているため省略します。
$ go mod tidyコマンドで必要なパッケージをダウンロードしましょう。

$ go mod tidy

テスト実行

コマンド$ go testでテスト実行です。

$ go test
> invoke is running initMarble
> - start init marble
> - end init marble
> invoke is running readMarble
> PASS
> ok      github.com/hyperledger/fabric-samples/chaincode/marbles02/go    0.006s

カバレッジ取得

カバレッジ結果はファイルに出力できます。
HTMLに変換することもでき、カバーされている箇所を見やすくできます。

$ go test -coverprofile=cover.out .
> ok      github.com/hyperledger/fabric-samples/chaincode/marbles02/go    0.005s  coverage: 15.0% of statements
$ go tool cover -html=cover.out -o cover.html

(参考)
Go でコードカバレッジを取得する
https://qiita.com/kkohtaka/items/965fe08821cda8c9da8a

Visual Studio Codeなら、html-preview-vscodeという拡張機能でHTMLをプレビューできます。ブラウザ要らずで便利です。

Some content has been disabled in this documentというメッセージが表示される場合は、「change Preview Security Settings」を「Disable」に設定しましょう。

(参考)
VSCodeのhtmlPreviewでscriptタブの箇所がうまく表示されない場合の解決方法
https://entlife.net/2020/10/26/vscode%E3%81%AEhtmlpreview%E3%81%A7script%E3%82%BF%E3%83%96%E3%81%AE%E7%AE%87%E6%89%80%E3%81%8C%E3%81%86%E3%81%BE%E3%81%8F%E8%A1%A8%E7%A4%BA%E3%81%95%E3%82%8C%E3%81%AA%E3%81%84%E5%A0%B4%E5%90%88/

おわり

個人的にはGoの勉強になりました。

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

Go module の Dependency 問題を解決する

今日はGoの依存関係で躓いたので、次回躓かないようにいろいろ試してみる。

go mod init

の後次の簡単なk8s client のプログラムを書いて実行する。サンプルのまんまなのに、失敗する。

package main

import (
    "flag"
    "fmt"
    "path/filepath"
    "time"

    "k8s.io/apimachinery/pkg/api/errors"
    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    "k8s.io/client-go/kubernetes"
    "k8s.io/client-go/tools/clientcmd"
    "k8s.io/client-go/util/homedir"
)

func main() {
    var kubeconfig *string
    if home := homedir.HomeDir(); home != "" {
        kubeconfig = flag.String("kubeconfig", filepath.Join(home, ".kube", "config"), "(optional) absolute path to the kubeconfig file")
    } else {
        kubeconfig = flag.String("kubeconfig", "", "absolute path to the kubeconfig file")
    }
    flag.Parse()

    config, err := clientcmd.BuildConfigFromFlags("", *kubeconfig)
    if err != nil {
        panic(err.Error())
    }

    clientset, err := kubernetes.NewForConfig(config)

    if err != nil {
        panic(err.Error())
    }
    for {
        pods, err := clientset.CoreV1().Pods("").List(metav1.ListOptions{})
        if err != nil {
            panic(err.Error())
        }
        fmt.Printf("Tehre are %d pods in the cluster\n", len(pods.Items))

        namespace := "default"
        pod := "nginx"

        _, err = clientset.CoreV1().Pods(namespace).Get(pod, metav1.GetOptions{})
        if errors.IsNotFound(err) {
            fmt.Printf("Pod %s is namespace %s not found\n", pod, namespace)
        } else if statusError, isStatus := err.(*errors.StatusError); isStatus {
            fmt.Printf("Error getting pod %s in namespace %s: %v\n",
                pod, namespace, statusError.ErrStatus.Message)
        } else if err != nil {
            panic(err.Error())
        } else {
            fmt.Printf("Found pod %s in namespace %s\n", pod, namespace)
        }
        time.Sleep(10 * time.Second)

    }

}

result

$ go run main.go 
build command-line-arguments: cannot load github.com/googleapis/gnostic/OpenAPIv2: module github.com/googleapis/gnostic@latest found (v0.5.4), but does not contain package github.com/googleapis/gnostic/OpenAPIv2

なんだこれ?多分該当のバージョンが壊れていることが推察される。

今は解決しているわけだが、その場その場の解決策がわかっても意味がないので、実際に自分がやったことを書いてみる。

最初に思ったことは、github.com/googleapis/gnostic/OpenAPIv2 を使っている依存関係を調べてそのライブラリをバージョンアップ、もしくはダウンすればよいということ。go では明確に依存関係のグラフみたいなものが出るのもが見つからなかった。go mod why が説明的にはそうなのだが、うまく動作していないように感じる。StackOverflowも同じようなことを言っていた。

いろいろしたが、本来するべきだった手順は、きっと go.mod を見ることだろう。

module simplearchitect.com

go 1.13

require (
    github.com/imdario/mergo v0.3.12 // indirect
    golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba // indirect
    k8s.io/api v0.20.4 // indirect
    k8s.io/apimachinery v0.20.4
    k8s.io/client-go v11.0.0+incompatible
    k8s.io/klog v1.0.0 // indirect
    k8s.io/utils v0.0.0-20210305010621-2afb4311ab10 // indirect
)

// indirect というのがあるが、これは私が直接使っていないもの。+incompatible は go module に対応していないものなので怪しいところ。もし go module の依存関係がツリーで出てくるものがあれば最高なのだが、見つからなかったので、直接使っているライブラリの go.mod をGitHub のページに行って、それぞれ調査する。少なくとも、master/main のgithub.com/googleapis/gnostic のバージョンは v0.4.1 であり、v0.5.4 ではない。k8s.io/client-go k8s.io/apimachinery の双方が依存しているらしいが、どちらも、v0.4.1 を使っている。不思議である。

では、次のステップとして、github.com/googleapis/gnostic@v0.4.1 を試してみるが、同じエラーが出る。おそらく何かが v0.5.4 を参照しているからであろう。次に考えたのが v0.5.4 を削除してみようということ。go mod tidy を使うと参照がキレた依存関係を整理してくれる。

$ go mod tidy
simplearchitect.com imports
        k8s.io/client-go/kubernetes imports
        k8s.io/client-go/discovery imports
        github.com/googleapis/gnostic/OpenAPIv2: module github.com/googleapis/gnostic@latest found (v0.5.4), but does not contain package github.com/googleapis/gnostic/OpenAPIv2
simplearchitect.com imports
        k8s.io/client-go/kubernetes imports
        k8s.io/client-go/kubernetes/typed/auditregistration/v1alpha1 imports
        k8s.io/api/auditregistration/v1alpha1: module k8s.io/api@latest found (v0.20.4), but does not contain package k8s.io/api/auditregistration/v1alpha1
simplearchitect.com imports
        k8s.io/client-go/kubernetes imports
        k8s.io/client-go/kubernetes/typed/settings/v1alpha1 imports
        k8s.io/api/settings/v1alpha1: module k8s.io/api@latest found (v0.20.4), but does not contain package k8s.io/api/settings/v1alpha1

うむ。これはなかなかわかりやすい。近づいている。例のライブラリは、k8s.io/client-go が使用している。では、v11.0.0 はどんなバージョンだろうとclient-go のサイトにいくと、意味が分からないが

image.png

最新 v12.0.0 とかあるのだが、(日付に注意)クリックすると、最新のバージョン全然ちゃうがな。
しかも、最新のプレリリースは、v11.0.0+incompatible を回避してくれて最新バージョンになるみたい。
image.png

結局のところ、解決策は go get k8s.io/client-go@v0.20.4 だったのだがなぜそうなるんだろう?

Major version suffix

go mod の公式ドキュメントを読んでいると、Major Version Suffix というコンセプトが出てきた。これは、依存性の問題を避けるための解決策で、通常ライブラリが異なるバージョンを使っている場合、より上のバージョンが採用される。ところがそうだとバージョンのコンフリクトの問題が発生する。であるので、Goは メジャーバージョンが2以上の場合は、メジャーバージョンサフィックスをつけるように変更を行った。つまり、

example.com/mod は、@v1.0.0 もしくは @v0.0.0 代のバージョンで使える表記であるが、メジャーバージョンが2以上の場合は、example.com/mod/v2 として使う必要がある。これによって、メジャーバージョンが違うと、パスが異なるので、同じライブラリの異なるバージョンが存在できるようになる。つまり v11.0.0.0+incompatible の意味合いは、モジュールに対応していないからではなく、v11.0.0.0 のライブラリは本来バージョンサフィックス付きで使われるべきなので、k8s.io/client-go/v11 としてインポートするべき(だけとできないのだろう)

インポート部分を以下のように変えてみる。

import (
    "flag"
    "fmt"
    "path/filepath"
    "time"

    "k8s.io/apimachinery/pkg/api/errors"
    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    "k8s.io/client-go/v11/kubernetes"
    "k8s.io/client-go/v11/tools/clientcmd"
    "k8s.io/client-go/v11/util/homedir"
)

result

$ go run main.go 
build command-line-arguments: cannot load k8s.io/client-go/v11/kubernetes: module k8s.io/client-go@latest found (v11.0.0+incompatible), but does not contain package k8s.io/client-go/v11/kubernetes

すると、どこかで見たようなエラーメッセージが出てきた。本来、v11 のサフィックスでアクセスできるようにするべきが、そうなってないので、コンテンツが無いように見えるということである。だから、おそらく k8s.io/client-go のプロジェクトは、バージョンサフィックスに対応するためにバージョン番号をv11.0.0 から、v0.20.0 に変更して、バージョンサフィックス問題を避けているのだろう。(多分)ところが、v11.0.0.0 は既にリリースされているので、普通に使うとlatest ではそれがインポートされて incompatible 問題が発生したということであろう。

自分のメンテしているアプリが、メジャーバージョン上げたいときは、ディレクトリを掘る必要があるっぽい。

まとめ

  • メジャーバージョンが大きい場合は、現在の go mod ではバージョンサフィックスが必要なはずなので、無い場合は、そこが怪しい。
  • 最初に確認するべきはgo.mod
  • go mod tidy で、依存関係が壊れている個所を特定する
  • 変なメジャーバージョンがある場合は、リポジトリの最新を見て、最新のものをインストールする。go get k8s.io/client-go@v0.20.4
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Golang ハンドラ関数のチェインで詰まった過去の自分へ

Goプログラミング実践入門 標準ライブラリでゼロからWebアプリを作るを読み進める中で、
第3章にある 3.10 ハンドラとハンドラ関数のチェイン の項で最初理解不明だったけど向き合ったら理解できたのでその過程を書いていきます。

↓(詰まった)問題となるコード

gowebprog/ch03/10chain_handlerfunc/server.go
package main

import (
    "fmt"
    "net/http"
    "reflect"
    "runtime"
)

func hello(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello!")
}

func log(h http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        name := runtime.FuncForPC(reflect.ValueOf(h).Pointer()).Name()
        fmt.Println("ハンドラ関数が呼び出されました - " + name)
        h(w, r)
    }
}

func main() {
    server := http.Server{
        Addr: "127.0.0.1:8080",
    }
    http.HandleFunc("/hello", log(hello))
    server.ListenAndServe()
}

詰まった(1回目)

log()でreturnしている匿名関数が、log()内に有りもしない値を引数としてセットしている点。

HandleFunc()の中身を見てみる。

func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
    DefaultServeMux.HandleFunc(pattern, handler)
}

第2引数にあるhandler func(ResponseWriter, *Request)に着目すると、
log()内の匿名関数と形が一致していることがわかる -> 高階関数

↓処理のフロー的なもの

HandleFunc(~, log(hello))
          ↓ 中身を展開
        log(hello) {
          return func(w, r) {
            name := runtime.FuncForPC(reflect.ValueOf(next).Pointer()).Name()
            fmt.Println("ハンドラ関数が呼び出されました - " + name)
            hello(w, r)
          }
        }
          ↓ log実行
        func (w, r) {
          name := runtime.FuncForPC(reflect.ValueOf(next).Pointer()).Name()
          fmt.Println("ハンドラ関数が呼び出されました - " + name)
          hello(w, r)
        }
          ↓ funcがhandlerとして扱われる
        name := runtime.FuncForPC(reflect.ValueOf(next).Pointer()).Name()
        fmt.Println("ハンドラ関数が呼び出されました - " + name)
        hello(w, r) // ←最終的にhelloが実行

詰まった(2回目)

log()でreturnしている匿名関数ではh()に引数を書くのに、main()で書くhelloは引数が不要な点

HandleFuncの引数のhelloは関数オブジェクトとして扱われていて、
log()内匿名関数のhは関数呼び出しをしていることの違い。

まとめ

個人的にはこう書いたほうがわかりやすそう

package main

import (
    "fmt"
    "net/http"
    "reflect"
    "runtime"
)

func hello(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello!")
}

func log(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        name := runtime.FuncForPC(reflect.ValueOf(next).Pointer()).Name()
        fmt.Println("ハンドラ関数が呼び出されました - " + name)
        next(w, r)
    }
}

func main() {
    server := http.Server{
        Addr: "127.0.0.1:8080",
    }
    http.HandleFunc("/hello", log(hello))
    server.ListenAndServe()
}

追記

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

[andlabs / ui] Windows GUI examples

GoでWindowsなGUIを作れるものを見つけたので、
exampleを動かして確認してみます。

今回紹介するのは、andlabs/ui です。

この記事を書いている時点では、
まだ安定版ではなさそうなので使用される場合はご注意を。

GoでWindowsなGUIを作成したいときは、
すこし安定している walk をオススメします。

examples

サンプルとなるコードは、以下のURLにあります。
https://github.com/andlabs/ui/tree/master/examples

  • Control Gallery
    • テキストボックス・ボタン・ダイアログ・スライダー など
  • Draw Text
    • 様々なテキストの表示方法
  • Histogram
    • 折れ線グラフ
  • Table
    • 表(行内に画像・ボタン・プログレスバーがある)
  • UpdateUI
    • UIを更新するサンプル?

では順番に見てみましょう。

Control Gallery

controlgallery.go
https://github.com/andlabs/ui/blob/master/examples/controlgallery.go

ボタン・チェックボックス・ラベル・テキストボックス など

image.png

スライダーをグリグリすると、プログレスバーが連動して動きます。

image.png

Dateピッカーやファイルパスの指定(読み込み先・保存先)・ダイアログ など

image.png

Draw Text

drawtext.go
https://github.com/andlabs/ui/blob/master/examples/drawtext.go

フォントの修飾・サイズ・位置・フォント変更 など
結構きめ細かに指定できます!

image.png

Histogram

histogram.go
https://github.com/andlabs/ui/blob/master/examples/histogram.go

数値を変えると折れ線グラフが連動して動く!

image.png

Table

table.go
https://github.com/andlabs/ui/blob/master/examples/table.go

行内でいろいろカスタムができるみたいですね!

image.png

UpdateUI

動かなかった、、、

まとめ

動かしてみた感じ、ちょっと気になるとこがありましたがそれなりには使えそう感。
あとビルド自体は少し遅めかなという印象(とはいえ数秒ですが)
コード自体はスッキリしていて書きやすい気がします。

気になる部品があったらソースコードを覗いて触ってみてくださいね~!

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