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

Go言語でHello World

Go言語をやってみた

最近Fintechという言葉を多く耳にするんですが、Fintechって何?
あぁ金融系と技術を併せてFintechねぇ。。。

ふ〜ん。かっこいいじゃん。

じゃあ何を使えばできるのかな〜?って
Udemyで探したら、酒井潤先生の講座でGo言語とFintechアプリの開発というのを見つけました!
この講座を受講しながら、自分の為のoutputを書いていきたいと思います。

現役シリコンバレーエンジニアが教えるGo入門 + 応用でビットコインのシストレFintechアプリの開発
書いてる内容はここからの情報ばかりになると思います。

初期設定は飛ばします。。。詳しくはwebへ。。。

(そのうち書くかも・・・。)

いつも最初にやるやつ・・・Hello World

早速いつものやつをやっていきたいと思います。
どんな参考書にも書いてある「Hello World」、Udemyの講座でもこの法則は不変でした。

VBA出身の僕は直感的にこう書いちゃう(真似しないでください)

lesson.go
print("Hello World")
>>> syntax error: non-declaration statement outside function body

直感的に書いてもダメなことがわかりました。

Go言語として実行するために準備をしましょう!

1.パッケージを宣言する

lesson.go
package ***

2.パッケージの中にmain関数を定義する

lesson.go
package main

func main() {
}

このmain関数で実行していくのがGoの基本ですね!

Hello Worldを表示しよう!

1.fmtのパッケージをインポートする

lesson.go
package main

import "fmt"

func main() {
}

2.fmtパッケージのPrintlnで表示する

lesson.go
package main

import "fmt"

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

はい!!表示できました!!
VBAからpythonを勉強した僕からすると、すごく遠回しな書き方だな。。。って印象です。
でも天下のグーグルが作っているのと、これから注目されている言語なので
Go言語を理解しつつ有益なoutputができるようになりたいと思います!

以上です!ありがとうございました!

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

Go言語に触れてみよう Part.2

はじめに

前回からだいぶ時間が経ってしまいました。。。

今回は、Tour of Goに触れていきながらGoの文法・構文についてみていきます。

Tour of Go

以下からサイトに飛んでください。
Tour of Go

Tour of Goの日本語訳なのでかなり読みやすくわかりやすいです。
また、Go言語を学ぶためにはかなりいい教材だと思っています。

では、演習をみていきましょう!!
(全ての演習をやっていきません。ごめんなさい。。。)

Package・Import

演習前に基本的な文法をおさらいします。

基本的にpackageについてはmain

ただ、ディレクトリ管理によってはpackege名を変える必要があるため、
Go言語のWebアプリ開発で注意。

importはGo言語ではグループ化があります。

import (
    "string"
    . "fmt"
    m "math"   
)

"fmt"と"math"の書き方は、それぞれドット操作エイリアス操作といいます。
こうすることで、fmtパッケージやmathメソッドを呼び出す際に省略・簡略可能になります。

fmt.Println("hoge")

Println("hoge") // 省略

math.Abs(-12.34)

m.Abs(-12.34) // 簡略

Exercise: Loops and Functions

色々進めていくと、関数や変数宣言、for・while・if文が紹介されます。

初めの演習問題は、平方根の計算をmathメソッドを使わずに実装するものです。
ヒントとして、z -= (z*z - x) / (2*z)z := 1.0 or z := float64(1)
を使用したらできるそうです。

また、指定として10回ループしてから、結果を出します。

exercise-loops-and-functions.go
package main

import (
    "fmt"
)

func Sqrt(x float64) float64 {
    // z := 1.0
    z := float64(1)

    for i := 0; i < 10; i++ {
        z -= (z*z - x) / (2*z)
    }
    return z
}

func main() {
    fmt.Println(Sqrt(2))
}

Exercise: Slices

演習を終えると、switch文やdefer、pointer、Array・Sliceなどの説明があります。ここから初心者の方は難しくなっていきます。

pointerに関しては、C言語での勉強をお勧めします。

ここでの演習は、Sliceでの2次元配列を使用して画像を表示するものです。

exercise-slices.go
package main

import "golang.org/x/tour/pic"

func Pic(dx, dy int) [][]uint8 {
    pic := make([][]uint8, dy)
    for y := range pic {
        pic[y] = make([]uint8, dx)
        for x := range pic[y] {
            pic[y][x] = uint8((x + y) / 2)
            // pic[y][x] = uint8(x * y)
            // pic[y][x] = uint8(x ^ y)
        }
    }
    return pic
}

func main() {
    pic.Show(Pic)
}

Exercise: Maps

続いては、mapです。
扱いについては、ArrayやSliceと似ている部分があるため、
そんなに難しくはありません。

演習では、mapを使用してテストケースをしっかりと走らせるものです。
ここで、stringsのメソッドを使用して引数に渡った文字列を分解していきます。

exercise-maps.go
package main

import (
    "strings"

    "golang.org/x/tour/wc"
)

func WordCount(s string) map[string]int {
    words := make(map[string]int)
    for _, i := range strings.Fields(s) {
        words[i]++
    }
    return words
}

func main() {
    wc.Test(WordCount)
}

Exercise: Fibonacci closure

関数のクロージャーについて学びます。

演習は、Fibonacci数列を実装します。
Fibonacci数列は、「2つ前の項と1つ前の項を足し合わせていくことでできる数列」のことです。
1, 1, 2, 3, 5, 8, 13, 21 ...

これをクロージャーでやっていきます。

exercise-fibonacci-closure.go
package main

import "fmt"

// fibonacci is a function that returns
// a function that returns an int.
func fibonacci() func() int {
    a, b := 0, 1
    return func() int {
        a, b = b, a+b
        return a
    }
}

func main() {
    f := fibonacci()
    for i := 0; i < 10; i++ {
        fmt.Println(f())
    }
}

Exercise: Stringers

ここからは関数とメソッドの違いやpointerとの関係、interfaceなどを学んでいきます。少し難しくなっていきます。

演習では、interfaceの一つであるfmt内のStringerを使用して、
IPアドレスを出力します。

exercise-stringers.go
package main

import "fmt"

type IPAddr [4]byte

// TODO: Add a "String() string" method to IPAddr.
func (addr IPAddr) String() (s string) {
    for _, v := range addr {
        s += fmt.Sprintf("%d.", v)
    }
    return s[:len(s)-1]
}

func main() {
    hosts := map[string]IPAddr{
        "loopback":  {127, 0, 0, 1},
        "googleDNS": {8, 8, 8, 8},
    }
    for name, ip := range hosts {
        fmt.Printf("%v: %v\n", name, ip)
    }
}

Exercise: Errors

ここでは、Go言語におけるエラーハンドリングを学びます。

exercise-errors.go
package main

import (
    "fmt"

)

type ErrNegativeSqrt float64

func (e ErrNegativeSqrt) Error() string {
    return fmt.Sprintf("cannot Sqrt negative number: %f", float64(e))
}

func Sqrt(x float64) (float64, error) {
    if x < 0 {
        return 0, ErrNegativeSqrt(x)
    }
    // z := 1.0
    z := float64(1)

    for i := 0; i < 10; i++ {
        z -= (z*z - x) / (2*z)
    }
    return z, nil
}

func main() {
    fmt.Println(Sqrt(2))
    fmt.Println(Sqrt(-2))
}

Exercise: Readers

ioパッケージのReaderinterfaceを使ってioデータを読み取ります。

exercise-readers.go
package main

import "golang.org/x/tour/reader"

type MyReader struct{}

// TODO: Add a Read([]byte) (int, error) method to MyReader.
func (a MyReader) Read(rb []byte) (n int, e error) {
    for i, _ := range rb {
        rb[i] = 'A'
        n++
    }
    return
}

func main() {
    reader.Validate(MyReader{})
}

Exercise: rot13Reader

前の演習と同じような感じでReaderinterfaceを使用します。

exercise-rot13reader.go
package main

import (
    "io"
    "os"
    "strings"
)

type rot13Reader struct {
    r io.Reader
}

func (a *rot13Reader) Read(rb []byte) (n int, e error) {
    n, e = a.r.Read(rb)
    if e == nil {
        for i, v := range rb {
            switch {
            case v >= 'A' && v <= 'Z':
                rb[i] = (v-'A'+13)%26 + 'A'
            case v >= 'a' && v <= 'z':
                rb[i] = (v-'a'+13)%26 + 'a'
            }
        }
    }

    return
}

func main() {
    s := strings.NewReader("Lbh penpxrq gur pbqr!")
    r := rot13Reader{s}
    io.Copy(os.Stdout, &r)

    s2 := strings.NewReader("You cracked the code!")
    r2 := rot13Reader{s2}
    io.Copy(os.Stdout, &r2)
}

Exercise: Images

今までの3つでinterfaceの使い方を学ぶものなので、readerと同じように書きます。

exercise-images.go
package main

import (
    "golang.org/x/tour/pic"
    "image"
    "image/color"
)

type Image struct{}

func (im Image) ColorModel() color.Model {
    return color.RGBAModel
}

func (im Image) Bounds() image.Rectangle {
    return image.Rect(0, 0, 255, 255)
}

func (im Image) At(x, y int) color.Color {
    v := uint8(x * y)
    return color.RGBA{v, v, 255, 255}
}

func main() {
    m := Image{}
    pic.ShowImage(m)
}

終わり

これでとりあえずの基本的なことができるようになると思います。
もっと深くやりたい人は、次の章に進んでください。

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

[Java] すこしふしぎなsplitの挙動

[Java] すこしふしぎなsplitの挙動

環境

Java8 (のはず)

splitクイズ

突然ですが問題です。

以下はJavaのコードです。
実行結果はどうなるでしょうか。

第1問!デデン♪

            String test = "a-i-u-e-o";
            String[] tests = test.split("-");
            System.out.println(tests.length);
            System.out.println(java.util.Arrays.toString(tests));


答え
5
[a, i, u, e, o]


正解しましたか?

次です。 こちらの結果はどうなるでしょうか。

            String test = "--o";
            String[] tests = test.split("-");
            System.out.println(tests.length);
            System.out.println(java.util.Arrays.toString(tests));


答え
3
[, , o]


正解しましたか?
先頭にデリミタがある場合はこのような挙動になるのですね。

次はこちらです。 こちらの結果はどうなるでしょうか。
なんとなく予想がつくのではないでしょうか。

            String test = "a----o";
            String[] tests = test.split("-");
            System.out.println(tests.length);
            System.out.println(java.util.Arrays.toString(tests));


答え
5
[a, , , , o]


正解しましたか?

さて最後です。 こちらの結果はどうなるでしょうか。
ここまで正解したならきっと簡単ですね。 さくさくいきましょう。

            String test = "a--";
            String[] tests = test.split("-");
            System.out.println(tests.length);
            System.out.println(java.util.Arrays.toString(tests));


答え
1
[a]


正解しましたか?
正解した方はおめでとうございます :tada:
外れてしまったかたは惜しかったですね...(´・ω・`)

ちなみにこれらの挙動を見たときの私の反応はこのような感じでした。


₍₍(ง˘ω˘)ว⁾⁾

??????????????????????????
え、ん...? あ、そう...ふーん( ´_ゝ`)なんでやねん


Javaのsplit

今まで出たものをまとめて俯瞰すると以下のようになります。

/* 
 * Java Playground
 * https://code.sololearn.com
 */
class Main {
    public static void main(String[ ] args) {
        {
            String test = "a-i-u-e-o";
            String[] tests = test.split("-");
            System.out.println(tests.length); // 5
            System.out.println(java.util.Arrays.toString(tests)); // [a,i,u,e,o]
        }
        {
            String test = "a-";
            String[] tests = test.split("-");
            System.out.println(tests.length); // 1
            System.out.println(java.util.Arrays.toString(tests)); // [a]
        }
        {
            String test = "-o";
            String[] tests = test.split("-");
            System.out.println(tests.length); // 2
            System.out.println(java.util.Arrays.toString(tests)); // [,o]
        }
        {
            String test = "a--";
            String[] tests = test.split("-");
            System.out.println(tests.length); // 1
            System.out.println(java.util.Arrays.toString(tests)); // [a]
        }
        {
            String test = "--o";
            String[] tests = test.split("-");
            System.out.println(tests.length); // 3
            System.out.println(java.util.Arrays.toString(tests)); // [,,o]
        }
        {
            String test = "a----o";
            String[] tests = test.split("-");
            System.out.println(tests.length); // 5
            System.out.println(java.util.Arrays.toString(tests)); // [a,,,,o]
        }
    }
}

いかがでしょうか。
私は正直気持ち悪いと思ゲフンゲフン
空を無視するのかしないのかはっきりしてほしいと思いました。
しかしながら、このような挙動になっていることにはなにかしらの理由があるのかもしれませんね。1

蛇足

Golangのsplit

ちなみに言語ごとにsplitの挙動は異なるようですので、複数言語を扱う場合はご注意ください。
一例として、比較的わかりやすいGolangのサンプルを以下に貼ります。


Golangのsplitサンプル
/*
 * Golang Playground
 * https://play.golang.org/
 */
package main

import (
    "fmt"
    "strings"
)

func main() {
    {
        test := "a-i-u-e-o"
        tests := strings.Split(test, "-")
        fmt.Println(len(tests)) // 5
        fmt.Println(tests) // [a i u e o]
    }
    {
        test := "a-"
        tests := strings.Split(test, "-")
        fmt.Println(len(tests)) // 2
        fmt.Println(tests) // [a ]
    }
    {
        test := "-o"
        tests := strings.Split(test, "-")
        fmt.Println(len(tests)) // 2
        fmt.Println(tests) // [ o]
    }
    {
        test := "a--"
        tests := strings.Split(test, "-")
        fmt.Println(len(tests)) // 3
        fmt.Println(tests) // [a  ]
    }
    {
        test := "--o"
        tests := strings.Split(test, "-")
        fmt.Println(len(tests)) // 3
        fmt.Println(tests) // [  o]
    }
    {
        test := "a----o"
        tests := strings.Split(test, "-")
        fmt.Println(len(tests)) // 5
        fmt.Println(tests) // [a    o]
    }
}


Javaの挙動を見たあとだと素直な挙動に感じますね...

付録

Javaのsplitは要するに右端のデリミタを無視すればよさそうなので、
Golangで同様の挙動を模倣する場合は、右端のデリミタを除去してからsplitするといいかもしれません。
需要があるかはわかりませんがサンプルを貼ります。 需要があるかはわかりませんが。


GolangでJavaのsplitのような挙動をさせ隊 (隊員1名)
package main

import (
    "fmt"
    "strings"
)

func main() {
    {
        test := "a-i-u-e-o"
        tests := javaSplit(test, "-")
        fmt.Println(len(tests)) // 5
        fmt.Println(tests) // [a i u e o]
    }
    {
        test := "a-"
        tests := javaSplit(test, "-")
        fmt.Println(len(tests)) // 1
        fmt.Println(tests) // [a]
    }
    {
        test := "-o"
        tests := javaSplit(test, "-")
        fmt.Println(len(tests)) // 2
        fmt.Println(tests) // [ o]
    }
    {
        test := "a--"
        tests := javaSplit(test, "-")
        fmt.Println(len(tests)) // 1
        fmt.Println(tests) // [a]
    }
    {
        test := "--o"
        tests := javaSplit(test, "-")
        fmt.Println(len(tests)) // 3
        fmt.Println(tests) // [  o]
    }
    {
        test := "a----o"
        tests := javaSplit(test, "-")
        fmt.Println(len(tests)) // 5
        fmt.Println(tests) // [a    o]
    }
}

// Javaの String#split(delimiter) を模倣
func javaSplit(str string, delimiter string) []string {
    return strings.Split(strings.TrimRight(str, delimiter), delimiter)
}


え、JavaでGolangのような挙動にしたい場合...?
あー... .. . がんばってください!

【追記】

え、JavaでGolangのような挙動にしたい場合...?

@saka1029 さんに教えていただきました!

第2引数に負の数を指定すればよいのでは?(String.split(String, int)

String[] test = "a--".split("-", -1);
System.out.println(test.length);               // -> 3
System.out.println(Arrays.toString(test));     // -> [a, , ]


javadocより抜粋

public String[] split​(String regex, int limit)

limitパラメータは、このパターンの適用回数を制御するため、結果となる配列の長さに影響を及ぼします。

  • 「制限」が正の場合、パターンはほとんどの「制限」に適用されます。-1回は配列の長さが「制限」を超えることはなく、最後に一致したデリミタを超えるすべての入力が配列の最後のエントリに含まれます。
  • 「制限」がゼロの場合、パターンは可能なかぎり何度も適用され、配列には任意の長さを指定でき、後続の空の文字列は破棄されます。
  • 「制限」が負の場合、パターンは可能なかぎり適用され、配列の長さは任意になります。


まさにこれです!いえーい!
というかjavadoc読んでから執筆しなさいよ
リンクはjava13のdocですが、java8も同様です。

さいごに

「ここまずいですよ」や「そいつぁちげーぜ!」などがありましたらコメントいただけますと幸いです₍₍(ง˘ω˘)ว⁾⁾
ゼロは俺に何も言ってはくれない...


  1. とりあえず仕様であることだけは間違いありません。 https://docs.oracle.com/javase/jp/8/docs/api/java/lang/String.html#split-java.lang.String- 

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

vegetaのビッグバンアタック

この記事は LIFULLその2 Advent Calendar 2019 の4日目の記事です。

ベジータ「ビッグバンアタック!!!!!!!!!!!!???????????????????????」

vegeta.gif

  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
はい。以下は真面目な内容です

目的

作成したサービスに対して、自分でシナリオを組んで負荷試験を行いたい。

背景

負荷試験をする際に、システムによってはキャッシュの機構が存在するため、同じリクエストを何度も投げても期待する負荷試験とならないことがあります。
そのため、リクエストのパラメータを変えながら負荷試験を行いたいということは比較的存在するシナリオだと思います。
しかし過去にApache JMeterを用いて負荷試験を行った際は、BeanShellの書き方がよくわからず時間もなかったので、csvにパラメータを大量に用意し、csvファイルを食わせるという方法を取りました。
しかし、csvとしてパラメータを用意するのは地味に面倒だったので、もっといいやりかたを探していました。
そこで vegeta に出会いました。

まず

名前や、githubの画像的にイロモノ感がすごいんですが、ツールとしては使いやすく、またライブラリとして使用できるようになっており、かなりいい感じです。
もともとは自前でhttpに並列アクセスするようなものを作っていたんですが、レポート機能なども作るのが面倒だと思っていたところでこのツールを見つけ、シナリオの部分だけ自分で書けばいいのでは?ということを閃きました。

vegetaができること

READMEにほぼ書いてあるので、僕が活用した部分だけ紹介します。
vegetaは、cliツールとして提供されていて、 vegeta attackで負荷をかけ、vegeta encodeで結果を出力したり、vegeta plotで結果をグラフでプロットしたりすることができます。
実際の使い方はこんな感じです。

echo "GET http://localhost:8080/ping" | vegeta attack | vegeta encode --every 1s

vegeta attackで、独自の形式で出力が行われるのですが、それを他のサブコマンドに渡すことで結果を出力したり、いい感じに活用することができます。
そこで、vegeta attackと同じ出力をするようにして、レポートについてはvegetaを使うことにしました

実際のコード

READMEにもある通り、リクエストの内容が一定であれば、ターゲットファイルと呼ばれる、リクエストの内容を書いたテキストファイルを読み込ませたり、NewStaticTargeterを使えば可能なのですが、今回は動的に内容を変更したかったので、自前で作りました。

動的に変更させる箇所

シナリオを実装しやすくするために、シナリオが満たすべきインターフェースを定義しました。

// TargetGenerator を実装すると、Attackで実行できる
type TargetGenerator interface {
    GenerateTarget(*vegeta.Target) error
}

// NewStaticTargeterを参考に、排他制御しておく
func generateTargeter(gen TargetGenerator) vegeta.Targeter {
    var mu sync.Mutex

    return func(tgt *vegeta.Target) error {
        mu.Lock()
        defer mu.Unlock()

        return gen.GenerateTarget(tgt)
    }
}

func attack(targeter vegeta.Targeter, freq, duration int, name string) <-chan *vegeta.Result {
    rate := vegeta.Rate{Freq: freq, Per: time.Second}
    dur := time.Duration(duration) * time.Second

    attacker := vegeta.NewAttacker()
    return attacker.Attack(targeter, rate, dur, name)
}

func main() {
    url := flag.String("url", "http://localhost:8080", "target")
    duration := flag.Int("duration", 10, "duration time(second)")
    freq := flag.Int("freq", 1, "frequence per 1 second")
    flag.Parse()

    result := attack(generateTargeter(/* ここで TargetGenerator を満たした構造体を渡す */))

    enc := vegeta.NewEncoder(os.Stdout)
    for res := range result {
        enc.Encode(res)
    }
}

複数シナリオをマージする箇所

とりえあずこのコードでリクエストを送れる状態になりましたが、複数のシナリオがあった際に内容をまとめる必要があります。
まとめずにシナリオの数だけgoroutineを用意して出力すると、出力の内容が壊れてしまうので、一つのチャンネルにまとめることにします。

func merge(cs ...<-chan *vegeta.Result) <-chan *vegeta.Result {
    out := make(chan *vegeta.Result)
    var wg sync.WaitGroup
    wg.Add(len(cs))
    // channelの数だけgoroutineを起動して、一つにまとめる
    for _, c := range cs {
        c := c
        go func() {
            for v := range c {
                out <- v
            }
            wg.Done()
        }()
    }
    // すべてのchannelがクローズされるのを待つ
    go func() {
        wg.Wait()
        close(out)
    }()
    return out
}

func main() {
    result := merge(
        attack(generateTargeter(/* ここで TargetGenerator を満たした構造体を渡す */, *freq, *duration, "Attacker1"),
        attack(generateTargeter(/* ここで TargetGenerator を満たした構造体を渡す */, *freq, *duration, "Attacker2"),
    )

    enc := vegeta.NewEncoder(os.Stdout)
    for res := range result {
        enc.Encode(res)
    }
}

今はAttack時のオプション等が全部のシナリオで同じですが、そこをシナリオごとに変更したければ、適当にattackerを渡せばいいと思います。

シナリオの書き方

シナリオの書き方は普通のGoのプログラムです

var rs1Letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")

func RandString(n int) string {
    b := make([]rune, n)
    for i := range b {
        b[i] = rs1Letters[rand.Intn(len(rs1Letters))]
    }
    return string(b)
}

type randomScenario struct {
    url    string
}

func (s *randomScenario) GenerateTarget(tgt *vegeta.Target) error {
    tgt.Method = http.MethodGet
    tgt.URL = s.url
    tgt.Body = []byte(RandomString(32) + "\n")
    return nil
}

プログラム全体

package main

import (
    "flag"
    "math/rand"
    "net/http"
    "os"
    "sync"
    "time"

    vegeta "github.com/tsenart/vegeta/lib"
)

var rs1Letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")

func RandString(n int) string {
    b := make([]rune, n)
    for i := range b {
        b[i] = rs1Letters[rand.Intn(len(rs1Letters))]
    }
    return string(b)
}

type randomScenario struct {
    url    string
    method string
}

func (s *randomScenario) GenerateTarget(tgt *vegeta.Target) error {
    tgt.Method = s.method
    tgt.URL = s.url
    tgt.Body = []byte(RandString(32) + "\n")
    return nil
}

func merge(cs ...<-chan *vegeta.Result) <-chan *vegeta.Result {
    out := make(chan *vegeta.Result)
    var wg sync.WaitGroup
    wg.Add(len(cs))

    for _, c := range cs {
        c := c
        go func() {
            for v := range c {
                out <- v
            }
            wg.Done()
        }()
    }
    go func() {
        wg.Wait()
        close(out)
    }()
    return out
}

// TargetGenerator を実装すると、Attackで実行できる
type TargetGenerator interface {
    GenerateTarget(*vegeta.Target) error
}

func generateTargeter(gen TargetGenerator) vegeta.Targeter {
    var mu sync.Mutex

    return func(tgt *vegeta.Target) error {
        mu.Lock()
        defer mu.Unlock()

        return gen.GenerateTarget(tgt)
    }
}

func attack(targeter vegeta.Targeter, freq, duration int, name string) <-chan *vegeta.Result {
    rate := vegeta.Rate{Freq: freq, Per: time.Second}
    dur := time.Duration(duration) * time.Second

    attacker := vegeta.NewAttacker()
    return attacker.Attack(targeter, rate, dur, name)
}

func main() {
    url := flag.String("url", "http://localhost:8080", "target")
    duration := flag.Int("duration", 10, "duration time(second)")
    freq := flag.Int("freq", 1, "frequence per 1 second")
    flag.Parse()

    result := merge(
        attack(generateTargeter(&randomScenario{url: *url}), *freq, *duration, "Attacker1"),
        attack(generateTargeter(&randomScenario{url: *url}), *freq, *duration, "Attacker2"),
    )

    enc := vegeta.NewEncoder(os.Stdout)
    for res := range result {
        enc.Encode(res)
    }
}

まとめ

vegetaを使って並列にシナリオを実行したり、レポートを簡単に取得することができました。
非同期処理は普通ゲロ難しいイメージがありますが、Golangだと、比較的簡単に扱えていいですね。
(goroutineをどう起動するかや、値をどう受け渡しするかなど考えることはありますが)

※ 負荷試験を行う際はリクエスト先やリクエスト数などには十分注意してください。このプログラムを使用して問題が起きても責任は負いかねます。

個人的に、Attackerの名前を、"Attacker1"とか"Attacker2"とかにしてるの、中二病っぽくてすごい好きです。

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

vegetaのビッグバンアタック?

この記事は LIFULLその2 Advent Calendar 2019 の4日目の記事です。

ベジータ「ビッグバンアタック!!!!!!!!!!!!???????????????????????」

vegeta.gif

  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
はい。以下は真面目な内容です

目的

作成したサービスに対して、自分でシナリオを組んで負荷試験を行いたい。

背景

負荷試験をする際に、システムによってはキャッシュの機構が存在するため、同じリクエストを何度も投げても期待する負荷試験とならないことがあります。
そのため、リクエストのパラメータを変えながら負荷試験を行いたいということは比較的存在するシナリオだと思います。
しかし過去にApache JMeterを用いて負荷試験を行った際は、BeanShellの書き方がよくわからず時間もなかったので、csvにパラメータを大量に用意し、csvファイルを食わせるという方法を取りました。
しかし、csvとしてパラメータを用意するのは地味に面倒だったので、もっといいやりかたを探していました。
そこで vegeta に出会いました。

まず

名前や、githubの画像的にイロモノ感がすごいんですが、ツールとしては使いやすく、またライブラリとして使用できるようになっており、かなりいい感じです。
もともとは自前でhttpに並列アクセスするようなものを作っていたんですが、レポート機能なども作るのが面倒だと思っていたところでこのツールを見つけ、シナリオの部分だけ自分で書けばいいのでは?ということを閃きました。

vegetaができること

READMEにほぼ書いてあるので、僕が活用した部分だけ紹介します。
vegetaは、cliツールとして提供されていて、 vegeta attackで負荷をかけ、vegeta encodeで結果を出力したり、vegeta plotで結果をグラフでプロットしたりすることができます。
実際の使い方はこんな感じです。

echo "GET http://localhost:8080/ping" | vegeta attack | vegeta encode --every 1s

vegeta attackで、独自の形式で出力が行われるのですが、それを他のサブコマンドに渡すことで結果を出力したり、いい感じに活用することができます。
そこで、vegeta attackと同じ出力をするようにして、レポートについてはvegetaを使うことにしました

実際のコード

READMEにもある通り、リクエストの内容が一定であれば、ターゲットファイルと呼ばれる、リクエストの内容を書いたテキストファイルを読み込ませたり、NewStaticTargeterを使えば可能なのですが、今回は動的に内容を変更したかったので、自前で作りました。

動的に変更させる箇所

シナリオを実装しやすくするために、シナリオが満たすべきインターフェースを定義しました。

// TargetGenerator を実装すると、Attackで実行できる
type TargetGenerator interface {
    GenerateTarget(*vegeta.Target) error
}

// NewStaticTargeterを参考に、排他制御しておく
func generateTargeter(gen TargetGenerator) vegeta.Targeter {
    var mu sync.Mutex

    return func(tgt *vegeta.Target) error {
        mu.Lock()
        defer mu.Unlock()

        return gen.GenerateTarget(tgt)
    }
}

func attack(targeter vegeta.Targeter, freq, duration int, name string) <-chan *vegeta.Result {
    rate := vegeta.Rate{Freq: freq, Per: time.Second}
    dur := time.Duration(duration) * time.Second

    attacker := vegeta.NewAttacker()
    return attacker.Attack(targeter, rate, dur, name)
}

func main() {
    url := flag.String("url", "http://localhost:8080", "target")
    duration := flag.Int("duration", 10, "duration time(second)")
    freq := flag.Int("freq", 1, "frequence per 1 second")
    flag.Parse()

    result := attack(generateTargeter(/* ここで TargetGenerator を満たした構造体を渡す */))

    enc := vegeta.NewEncoder(os.Stdout)
    for res := range result {
        enc.Encode(res)
    }
}

複数シナリオをマージする箇所

とりえあずこのコードでリクエストを送れる状態になりましたが、複数のシナリオがあった際に内容をまとめる必要があります。
まとめずにシナリオの数だけgoroutineを用意して出力すると、出力の内容が壊れてしまうので、一つのチャンネルにまとめることにします。

func merge(cs ...<-chan *vegeta.Result) <-chan *vegeta.Result {
    out := make(chan *vegeta.Result)
    var wg sync.WaitGroup
    wg.Add(len(cs))
    // channelの数だけgoroutineを起動して、一つにまとめる
    for _, c := range cs {
        c := c
        go func() {
            for v := range c {
                out <- v
            }
            wg.Done()
        }()
    }
    // すべてのchannelがクローズされるのを待つ
    go func() {
        wg.Wait()
        close(out)
    }()
    return out
}

func main() {
    result := merge(
        attack(generateTargeter(/* ここで TargetGenerator を満たした構造体を渡す */, *freq, *duration, "Attacker1"),
        attack(generateTargeter(/* ここで TargetGenerator を満たした構造体を渡す */, *freq, *duration, "Attacker2"),
    )

    enc := vegeta.NewEncoder(os.Stdout)
    for res := range result {
        enc.Encode(res)
    }
}

今はAttack時のオプション等が全部のシナリオで同じですが、そこをシナリオごとに変更したければ、適当にattackerを渡せばいいと思います。

シナリオの書き方

シナリオの書き方は普通のGoのプログラムです

var rs1Letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")

func RandString(n int) string {
    b := make([]rune, n)
    for i := range b {
        b[i] = rs1Letters[rand.Intn(len(rs1Letters))]
    }
    return string(b)
}

type randomScenario struct {
    url    string
}

func (s *randomScenario) GenerateTarget(tgt *vegeta.Target) error {
    tgt.Method = http.MethodGet
    tgt.URL = s.url
    tgt.Body = []byte(RandomString(32) + "\n")
    return nil
}

プログラム全体

package main

import (
    "flag"
    "math/rand"
    "net/http"
    "os"
    "sync"
    "time"

    vegeta "github.com/tsenart/vegeta/lib"
)

var rs1Letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")

func RandString(n int) string {
    b := make([]rune, n)
    for i := range b {
        b[i] = rs1Letters[rand.Intn(len(rs1Letters))]
    }
    return string(b)
}

type randomScenario struct {
    url    string
    method string
}

func (s *randomScenario) GenerateTarget(tgt *vegeta.Target) error {
    tgt.Method = s.method
    tgt.URL = s.url
    tgt.Body = []byte(RandString(32) + "\n")
    return nil
}

func merge(cs ...<-chan *vegeta.Result) <-chan *vegeta.Result {
    out := make(chan *vegeta.Result)
    var wg sync.WaitGroup
    wg.Add(len(cs))

    for _, c := range cs {
        c := c
        go func() {
            for v := range c {
                out <- v
            }
            wg.Done()
        }()
    }
    go func() {
        wg.Wait()
        close(out)
    }()
    return out
}

// TargetGenerator を実装すると、Attackで実行できる
type TargetGenerator interface {
    GenerateTarget(*vegeta.Target) error
}

func generateTargeter(gen TargetGenerator) vegeta.Targeter {
    var mu sync.Mutex

    return func(tgt *vegeta.Target) error {
        mu.Lock()
        defer mu.Unlock()

        return gen.GenerateTarget(tgt)
    }
}

func attack(targeter vegeta.Targeter, freq, duration int, name string) <-chan *vegeta.Result {
    rate := vegeta.Rate{Freq: freq, Per: time.Second}
    dur := time.Duration(duration) * time.Second

    attacker := vegeta.NewAttacker()
    return attacker.Attack(targeter, rate, dur, name)
}

func main() {
    url := flag.String("url", "http://localhost:8080", "target")
    duration := flag.Int("duration", 10, "duration time(second)")
    freq := flag.Int("freq", 1, "frequence per 1 second")
    flag.Parse()

    result := merge(
        attack(generateTargeter(&randomScenario{url: *url}), *freq, *duration, "Attacker1"),
        attack(generateTargeter(&randomScenario{url: *url}), *freq, *duration, "Attacker2"),
    )

    enc := vegeta.NewEncoder(os.Stdout)
    for res := range result {
        enc.Encode(res)
    }
}

まとめ

vegetaを使って並列にシナリオを実行したり、レポートを簡単に取得することができました。
非同期処理は普通ゲロ難しいイメージがありますが、Golangだと、比較的簡単に扱えていいですね。
(goroutineをどう起動するかや、値をどう受け渡しするかなど考えることはありますが)

※ 負荷試験を行う際はリクエスト先やリクエスト数などには十分注意してください。このプログラムを使用して問題が起きても責任は負いかねます。

個人的に、Attackerの名前を、"Attacker1"とか"Attacker2"とかにしてるの、中二病っぽくてすごい好きです。

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

Goを使ってCLIにフラッシュ画を描画する

この記事はtomowarkar ひとりAdvent Calendar 2019の5日目の記事です。

今日は少し前に作ったCLIへのフラッシュ画像の描画について書いていきます。

はじめに

gogo.gif
こんな感じで一定時間スパンでn*m行のフィールドにランダムに⚪️??を描画しています。

  • 時間ループ、キーアクションでGo Channelを使用
  • 描画範囲を構造体で定義

と少し発展的なGoの仕組みを学ぶのに役立ちました。

注意


今回こちらのライブラリを使用させていただきましたがREADMEにもあるように、すでに運用が終了しているライブラリです。(2019年12月5日現在)
使用の際はご注意ください。

なお、このライブラリでは代替案として以下のライブラリが挙げられています。

コード全文

コード全文は少し長くなるので折りたたんでおいておきます。
また、今回参考としてライブラリのデモを参考にしました。
参考: https://github.com/nsf/termbox-go/tree/master/_demos

コード全文



(クリックしてください)
flash.go
package main

import (
    "math/rand"
    "time"

    "github.com/nsf/termbox-go"
)

const coldef = termbox.ColorDefault

// Maze ...
type Maze struct {
    width  int
    height int
    field  [][]int
}

// InitMaze ...
func (m *Maze) InitMaze(h, w int) {
    m.width = w
    m.height = h
    m.field = make([][]int, h)
    for i := 0; i < h; i++ {
        m.field[i] = make([]int, w)
    }
}

// RandMaze ...
func (m *Maze) RandMaze() {
    for j := 0; j < m.height; j++ {
        for i := 0; i < m.width; i++ {
            m.field[j][i] = rand.Intn(3)
        }
    }
}

// DrawField ...
func (m Maze) DrawField() {
    termbox.Clear(coldef, coldef)

    for j := 0; j < m.height; j++ {
        for i := 0; i < m.width; i++ {
            if m.field[j][i] == 0 {
                termbox.SetCell(i*2, j, '⚪', coldef, coldef)
            } else if m.field[j][i] == 1 {
                termbox.SetCell(i*2, j, '?', coldef, coldef)
            } else {
                termbox.SetCell(i*2, j, '?', coldef, coldef)
            }
        }
    }
    termbox.Flush()
}

//key event
func keyEventLoop(kch chan termbox.Key) {
    for {
        switch ev := termbox.PollEvent(); ev.Type {
        case termbox.EventKey:
            kch <- ev.Key
        default:
        }
    }
}

//time event
func timeEventLoop(tch chan bool, span int) {
    for {
        tch <- true
        time.Sleep(time.Duration(span) * time.Millisecond)
    }
}

func mainLoop(mz Maze, tch chan bool, kch chan termbox.Key) {
    for {
        select {
        case key := <-kch: //key event
            switch key {
            case termbox.KeyEsc, termbox.KeyCtrlC: //end event
                return
            }
        case <-tch: //time event
            mz.RandMaze()
            mz.DrawField()
            break
        default:
        }
    }
}

func main() {
    err := termbox.Init()
    if err != nil {
        panic(err)
    }
    defer termbox.Close()

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

    var maze Maze
    maze.InitMaze(15, 15)

    kch := make(chan termbox.Key)
    tch := make(chan bool)
    go keyEventLoop(kch)
    go timeEventLoop(tch, 500)

    mainLoop(maze, tch, kch)
}

コード詳細

Step0. ライブラリを使うためのおまじない

ライブラリを使用するためのおまじないです。詳細はこちらをどうぞ。

Go で "rand" を使うときは Seedを設定しろってどこかで見た気がするので(忘れた)設定。
確か初期値が決まっていて厳密に乱数ではないからだった気がする。

step0.go
package main

import (
    "math/rand"
    "time"

    "github.com/nsf/termbox-go"
)

const coldef = termbox.ColorDefault

func main() {
    err := termbox.Init()
    if err != nil {
        panic(err)
    }
    defer termbox.Close()

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

Step1. CLIに描画する構造体を定義

  • N×M行列を描画しようと思うので、構造体Mazeを定義。(※なんでMazeやねんとか言わないで?)
  • Mazeは描画するフィールド情報と、幅、高さの情報を持たせる。
  • また同時にMazeを初期化してN×M行列を作るInitMazeとランダムにMazeのフィールド情報を更新するRandMazeも作成。

今回のフィールド情報は⚪️??の3つの情報をもち、それぞれ0,1,2でフィールド情報として持たせるのでrand.Intn(3)としています。

この辺りは直接数字を打ち込むのではなく、外で定義してから変数を入れるほうが良さそうですね(書きながら反省するスタイル)

step1.go
// Maze ...
type Maze struct {
    width  int
    height int
    field  [][]int
}

// InitMaze ...
func (m *Maze) InitMaze(h, w int) {
    m.width = w
    m.height = h
    m.field = make([][]int, h)
    for i := 0; i < h; i++ {
        m.field[i] = make([]int, w)
    }
}

// RandMaze ...
func (m *Maze) RandMaze() {
    for j := 0; j < m.height; j++ {
        for i := 0; i < m.width; i++ {
            m.field[j][i] = rand.Intn(3)
        }
    }
}

Step2. CLIに構造体を描画

次に描画する関数DrawFieldを書いていきます。

step2.go
// DrawField ...
func (m Maze) DrawField() {
    termbox.Clear(coldef, coldef)

    for j := 0; j < m.height; j++ {
        for i := 0; i < m.width; i++ {
            if m.field[j][i] == 0 {
                termbox.SetCell(i*2, j, '⚪', coldef, coldef)
            } else if m.field[j][i] == 1 {
                termbox.SetCell(i*2, j, '?', coldef, coldef)
            } else {
                termbox.SetCell(i*2, j, '?', coldef, coldef)
            }
        }
    }
    termbox.Flush()
}
  1. フィールドを初期化
  2. フィールド情報の0,1,2をそれぞれ⚪️??に変換
  3. 描画

この流れです。
しかしこのままではコンマ秒単位で描画されるので、描画されていることを認識することができません。
なので以下のようにしてループを定義して描画の様子を確認してみます。

tmp.go
func main() {
    err := termbox.Init()
    if err != nil {
        panic(err)
    }
    defer termbox.Close()

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

    var maze Maze
    maze.InitMaze(15, 15)
    for i := 0; i < 10000; i++ {
        maze.DrawField()
    }
}

image.png
このような⚪️の15×15行列が確認できていれば成功です。

Step3. 描画イベントから抜け出すキーイベントを定義

いちいち描画時間を設定するのはナンセンスなので、描画を無限ループさせ、Escキーによって描画画面から脱出できるようにします。

描画のループとキーイベントは別軸で評価したいため、Go Channelを使用します。

なのでキーイベントを判定するループkeyEventLoopとメインの描画のループmainLoopをそれぞれ定義します。

step3.go
//key event
func keyEventLoop(kch chan termbox.Key) {
    for {
        switch ev := termbox.PollEvent(); ev.Type {
        case termbox.EventKey:
            kch <- ev.Key
        default:
        }
    }
}

func mainLoop(mz Maze, kch chan termbox.Key) {
    for {
        select {
        case key := <-kch: //key event
            switch key {
            case termbox.KeyEsc: //end event
                return
            }
        default:
            mz.RandMaze()
            mz.DrawField()
        }
    }
}

func main() {
    err := termbox.Init()
    if err != nil {
        panic(err)
    }
    defer termbox.Close()

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

    var maze Maze
    maze.InitMaze(15, 15)
    kch := make(chan termbox.Key)
    go keyEventLoop(kch)

    mainLoop(maze, kch)
}
  1. メインループで描画を更新、描画のループを行う。
  2. メインループが回っている間並行してキーイベントのループが回っていて、キーイベントがあった場合kchに情報を送る
  3. メインループはkchからキーイベントの情報を受け取り、キーイベントの情報によってイベントを実行(今回はEscキーでループの脱出)

これでコードを走らせると、すごい勢いで描画が⚪️??に更新されていて、かつEscキーを押すことで描画画面から抜け出すことができることがわかります。

Step4. 時間イベントを定義して一定時間ごとに描画を更新

このままでは描画の更新が早すぎます。
一定時間ごとに描画が更新されるように時間イベントtimeEventLoopを設定し、メインループmainLoopを更新します

step4.go
//time event
func timeEventLoop(tch chan bool, span int) {
    for {
        tch <- true
        time.Sleep(time.Duration(span) * time.Millisecond)
    }
}

func mainLoop(mz Maze, tch chan bool, kch chan termbox.Key) {
    for {
        select {
        case key := <-kch: //key event
            switch key {
            case termbox.KeyEsc, termbox.KeyCtrlC: //end event
                return
            }
        case <-tch: //time event
            mz.RandMaze()
            mz.DrawField()
            break
        default:
        }
    }
}

func main() {
    err := termbox.Init()
    if err != nil {
        panic(err)
    }
    defer termbox.Close()

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

    var maze Maze
    maze.InitMaze(15, 15)

    kch := make(chan termbox.Key)
    tch := make(chan bool)
    go keyEventLoop(kch)
    go timeEventLoop(tch, 500)

    mainLoop(maze, tch, kch)
}
  1. メインループは無限ループを行う。
  2. メインループが回っている間並行して時間イベントのループが回っていて任意の時間[ms]ごとにtchに情報を送る
  3. メインループはtchから時間イベントの情報を受け取り、時間イベントが発生して場合において描画の更新、再描画を行う。

という形になりました。

まとめ

これで、
- 500ms毎に15×15行列がランダムで更新され描画される
- Escキーを押すことで描画画面から脱出

ということができました。
行列の数や描画の更新時間を変えて遊んでみてください。

コードはGithub Gistにもおいておきます!

https://gist.github.com/tomowarkar/3bb50298393b66148e43e00238e52083

以上明日も頑張ります!!
tomowarkar ひとりAdvent Calendar Advent Calendar 2019

追記

先日(4日目)のアドベントカレンダーで同じくtermbox-goを使った記事を発見したので載せておきます。
TUI版インベーダーゲームをGo言語で作る

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

GoによるDDD / Clean Architecture 実装

今年の初め頃から少しずつアーキテクチャやDDDの勉強を初め、プロジェクトの中で手探りで実践してきました。
そのまとめと振り返りを兼ねて、Go言語でDDDとクリーンアーキテクチャを意識した実装を行う場合の簡単な実装例を記載していきます。

GoはJava等と違い完全なOOPを実現するような言語ではないので、あまりガチガチに追求しすぎるよりも、
ある程度妥協しながらGoの良さを活かしつつ落とし所を見つけて行くのが良いのではないかと思います。

未だ試行錯誤しながら実装しているものですので、ご意見/ツッコミ大歓迎です。

Domain Object

エンティティや値オブジェクトといったドメインオブジェクトはデータと振る舞いから成るもので、
Goでは構造体とそれが持つプロパティ値、関数で表現します。

type Users struct {
    id         int
    firstName  string
    familyName string
    birthDay   time.Time
    address    string
}

func NewUser(id int, firstName string, familyName string, birthDay time.Time, address string) (*Users, error) {
    if birthDay.After(time.Now()) {
        return nil, errors.New("Error: Incorrect birthday ")
    }

    return &Users{
        id:         id,
        firstName:  firstName,
        familyName: familyName,
        birthDay:   birthDay,
        address:    address,
    }, nil
}

func (u *Users) GetFullName() string {
    return fmt.Sprintf("%s %s", u.firstName, u.familyName)
}

func (u *Users) GetAge() int {
    return time.Now().Year() - u.birthDay.Year()
}

構造体/コンストラクタ/振る舞いを表現する関数、これらが基本的な構成になります。

構造体の各プロパティのスコープはプライベートにしておきます。
コンストラクタを通さずオブジェクトを生成したり、直接値を出し入れするようなことを防ぐためです。
生成されたオブジェクトは破棄されるまで状態を保持し続け(immutable)、変更がある場合には再度生成したオブジェクトで置き換えます。

ドメイン層は最も独立した層であるためユニットテストも容易です。
じゃんじゃんテストコード書きましょう。

UseCases

ドメインオブジェクトやアダプタを利用してデータをやり取りする一連のアプリケーションルールを表現します。
各アダプタ用のInterfaceもこのユースケース層に定義します。

アダプタ側にInterfaceを定義するのが一般的かもしれませんが、Go Code Review Comments の記載に従い、
ここは利用側でInterfaceを定義するGo Wayなやり方で行きましょう。

実際、実装の詳細と言える各アダプタ層を書く前にアプリケーションロジックを実装して検証する事が出来るためこのやり方は気に入っています。

type UsersInputPort interface {
    GetUserAge(UsersDto) error
}

type UsersRepository interface {
    GetUser(UsersDto) (*domain.Users, error)
}
type OuterSystemGateway interface {
    Send(userID int) error
}

type UsersPresenter interface {
    RespUserAge(int) error
}

type Users struct {
    repo      UsersRepository
    gateway   OuterSystemGateway
    presenter UsersPresenter
}

func NewUser(repo UsersRepository, gateway OuterSystemGateway, presenter UsersPresenter) UsersInputPort {
    return &Users{
        repo:      repo,
        gateway:   gateway,
        presenter: presenter,
    }
}

type UsersDto struct {
    ID int
}

func (u *Users) GetUserAge(dto UsersDto) error {
    user, err := u.repo.GetUser(dto)
    if err != nil {
        return err
    }

    if err := u.gateway.Send(dto.ID); err != nil {
        return err
    }

    if err := u.presenter.RespUserAge(user.GetAge()); err != nil {
        return err
    }

    return nil
}

様々なアダプタが入り込んでくるため、テストを描くのが面倒なユースケース層ですが、Interfaceに寄せることでテスタブルな実装にもなります。

テスト用のモックはやや量が多くなるかと思うので、手書きが面倒であれば gomock などのツールで生成してしまうと楽ができます。

Interface Adapters

システムの外側と内側の間を繋ぐレイヤーです。
APIのリクエストハンドラやレスポンス、データベースや外部システムとのやり取りなどを行います。

データベースや外部システムとのやり取りはRepositoryにまとめて書くケースもあると思いますが、
個人的には分けた方がどういった性質のものがどこにあるのかわかりやすくなると思うので

  • Repository: 永続化
  • Gateway: 外部システム連携
  • Controller: リクエストハンドラ
  • Presenter: レスポンス用アウトプットポート

の様にパッケージ分けしています。

基本的な構成は各パッケージ共に同じでユースケース層に定義したインターフェイスを実装する構造体とそのコンストラクタ、インターフェイスを満たす関数から成ります。

Repository

データベースなどの永続化関連の機能を取り扱います。

構造体にはデータベースとのコネクションを持つ事になりますが、ここをさらに抽象化してインターフェイスにします。
コネクション用のインターフェイスは例によって利用側であるアダプタ側に定義しておきます。

抽象化する事でテストを書く際にモックを当てたり、SQLiteで簡単にテストしたり、 dockertest でDockerでテスト用DBを用意したりと柔軟性が増すメリットもあります。

type usersRepository struct {
    dbHandler DBHandler
}

type DBHandler interface {
    Store(*domain.Users) error
    Query() (*domain.Users, error)
    ByID(int) DBHandler
    ByName(string, string) DBHandler
}

func NewUsersRepository(dbHandler DBHandler) usecase.UsersRepository {
    return &usersRepository{
        // 抽象化したコネクション
        dbHandler: dbHandler,
    }
}

func (e *usersRepository) GetUser(dto usecase.UsersDto) (*domain.Users, error) {

    user, err := e.dbHandler.ByID(dto.ID).Query()
    if err != nil {
        return nil, err
    }

    return user, nil
}

データベースが変わる事はないという様なプロジェクトであれば直接コネクションオブジェクトを持たせても良いと思います。
ただし、後者の場合依存の向きが外側の層に向くため、クリーンアーキテクチャの基本からは外れてしまう事は注意が必要です。

type usersRepository struct {
    db *sql.DB
}

Gateway

外部システムとの接続などの機能を取り扱います。
構造体には外部システム接続とのクライアントオブジェクトなどを持たせる事になります。

ここも抽象化しても良いでしょうが、あんまり変わる事はないし変わる場合はインフラ層から
ガッツリ変わるのでそこまではやっていません。

type outerSystemGateway struct {
    // 外部システム接続用のクライアントオブジェクトとか
    outerClient *outer.Client
}

func NewOuterSystemGateway() usecase.OuterSystemGateway {
    return &outerSystemGateway{}
}

func (u *outerSystemGateway) Send(userID int) error {

    // Do Something

    return nil
}

Presenter

ユーザーへのレスポンスを提供する口として処理結果の出力を行います。
利用しているフレームワークによってはここは実装しない場合もあるかもしれません。
(Controllerから直接返すなど)

type usersPresenter struct {
    respWriter http.ResponseWriter
}

func NewUsersPresenter(w http.ResponseWriter) usecase.UsersPresenter {
    return &usersPresenter{
        respWriter: w,
    }
}

type UserAge struct {
    Age int
}

func (u *usersPresenter) RespUserAge(age int) error {
    if err := Response(u.respWriter, http.StatusOK, UserAge{age}); err != nil {
        return err
    }
    return nil
}

Controller

ここまで一通り実装し終えたら、処理の入り口となるコントローラ層を実装します。
APIであればリクエストハンドラの処理が置かれ、各アダプタのDIを行うのがこの層です。

type UserHandler struct {
    dbHandler repository.DBHandler
}

func NewUser(dbHandler repository.DBHandler) *UserHandler {
    return &UserHandler{
        dbHandler: dbHandler,
    }
}

func (u *UserHandler) GetUserAge(w http.ResponseWriter, r *http.Request) {

    id, err := strconv.Atoi(r.URL.Query().Get("id"))
    if err != nil {
    }

    // 各アダプタへのDIを行いInputportを返す関数
    inputPort, err := initInputPort(u.dbHandler, w)
    if err != nil {
    }
    err = inputPort.GetUserAge(usecase.UsersDto{
        ID: id,
    })
    if err != nil {
        log.Printf("%v\n", err)
    }
}
func initInputPort(dbHandler repository.DBHandler, w http.ResponseWriter) (usecase.UsersInputPort, error) {
    usersRepository := repository.NewUsersRepository(dbHandler)
    outerSystemGateway := gateway.NewOuterSystemGateway()
    usersPresenter := presenter.NewUsersPresenter(w)
    usersInputPort := usecase.NewUsers(usersRepository, outerSystemGateway, usersPresenter)
    return usersInputPort, nil
}

DI部の実装を書くため、利用するアダプタが増えるほどコードが膨らんだり、インポート文が煩雑になり名前が被るなどどうしても辛くなりがちです。

DI部だけ切り出して辛いところを一つに集約したり、あまり面倒になるようであれば wire などのDIツールを使って省力化するのも良いと思います。

wireの場合、ここまで作っておけば下記のようなコードから一発生成してくれます。

func initInputPort(dbHandler repository.DBHandler, w http.ResponseWriter) (usecase.UsersInputPort, error) {
    wire.Build(
        usecase.NewUsers,
        repository.NewUsersRepository,
        gateway.NewOuterSystemGateway,
        presenter.NewUsersPresenter,
    )

    return &usecase.Users{}, nil
}

実装時に発生する悩み

ドメインオブジェクトのカプセル化を保つため、不要なGetter/Setterは持たせないようにしますが、
Goの場合どうしても各レイヤ間のモデルへの詰め替えの実装が発生します。
例えばドメインモデルのデータを永続化モデルに詰め替える際には下記のように。

func (h *UsersHandler) Store(user *domain.Users) error {

    m := &models.Users{
        FirstName:  user.GetFirstName(),
        FamilyName: user.GetFamilyName(),
        BirthDay:   user.GetBirthDay(),
        Address:    user.GetAddress(),
    }

    if err := h.Save(m).Error; err != nil {
        return err
    }

    return nil
}

同じような事がレスポンス用のモデルへの詰め替えなど各層で発生し、どうしても詰め替え用途のGetterを作る事になります。

ドメイン層で他層のモデルに詰め替えて渡すファクトリ関数を用意したとして、その場合にはドメイン層が外側の層の構造体に依存する事になります。

クリーンアーキテクチャに寄せてあくまでドメインモデルを外側に依存しない方法を取るか、
カプセル化の保護に寄せるか、悩ましいところです。

こういったこともあり、GoでDDDの実装を行う場合ある程度割り切りが必要になるのかなと感じています。

なんだか結局解決出来てないじゃないかという終わり方ですが、それでもこういった試行錯誤をしながら実装していく事には意味があるのではないかと思います(思いたい)し、今後も続けていきたいと思います。

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

golangで楽々API作成

はじめに

2019年に最も注目された言語といえば…
そうだね、golangだね!(私の中では)

ってことで今更感はあるが、golang触ってみました!

やったこと

  • golangの勉強がてら、TODOアプリを作ってみた
  • gin, gormを使ってRESTfulなWeb APIを作成

RESTful APIを作成するにあたり

  • CRUDを意識して!
  • バージョン管理に対応した仕様で!

以上を踏まえてデザインした。

CRUD HTTPメソッド エンドポイント
Create(新規タスクの登録) POST ~/api/v1/tasks
Read(全タスクの参照)   GET ~/api/v1/tasks
Read(一部タスクの参照) GET ~/api/v1/tasks/:id
Update(一部タスクの更新) PUT ~/api/v1/tasks/:id
Delete(一部タスクの削除) DELETE ~/api/v1/tasks/:id

構成考えてみた

Gin と GORM で作るオレオレ構成 APIを参考にさせていただきました。

.
├── controller
│   └── task_controller.go
├── db
│   └── db.go
├── entity
│   └── task.go
├── router
│   └── router.go
└── main.go
パッケージ名 処理内容
controller リクエストに対する処理、レスポンスの決定
db DB接続等の処理を実装
entity エンティティを定義
router ルーティングの設定

エンティティ考えてみた

ToDoリストでは、タスクについて以下のことを管理するようにした。
- タスク名(やらないといけない事)
- 期限
- 重要度(重要か重要でないか)
- 完了状態(完了の有無)

作ってみた

ここに関しては他に多数記事が存在するので割愛
github

所感

ginが使いやすかった件

ただただ使いやすかったです。本当にありがとうございます。

gormの自動マイグレーション機能が便利だった件

gormの自動マイグレーションは、未作成のテーブルやカラムを自動で追加してくれる機能
ちょっとした開発ならSQL書かなくていいし楽!

SQLBoilerという選択肢も?

gormの代わりに、SQLBoilerというライブラリを使う選択肢もあるみたい。
機会があったら触ってみたいですね…

今後

API作成したので、次はクライアント側作ってみます。

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

GitHub ActionsとAWS CDKを使ってAWS Lambdaを自動デプロイしていく

この記事はぷりぷりあぷりけーしょんず Advent Calendar 2019の4日目の記事です。

はじめに

個人開発をする上でAWS Lambdaというのは色んな面で低コストで様々なことができ、アプリエンジニアな私からするとなんとなく「やった」感が得られるものが有りついつい使ってしまうし、そんな人はきっと沢山いるでしょう。知らんけど。

デプロイも楽だし、Node.jsやPythonならなんかミスったらコンソールで直接いじいじ直しちゃえば何とかなるもんね、うん最高。満足満足。

と、満足せずに今日はより開発現場でも使いやすく、Gitで管理しながらプルリクが通ってmasterにマージされたらあら不思議、本番に反映されている!な事をGitHub ActionsとAWS CDKを使って実現して行こうと思います。

スキルセット

GitHub Actions
AWS CDK 1.18.0(2019.12.03時点最新)
AWS Lambda
AWS APIGateway
Go

ディレクトリ構造

- .github
  |
   - workflows
     | 
      - actions.yml 
- bin
  |
   - main(goのバイナリができる予定のdir)
- cdk
  |
   - cdk initした資材群(省略)
- src
  |
   - main.go
- go.mod
- .gitignore

なお、今回は各スキルセットにはあまり深入りせずサクサク実装していこうと思います(なんせ自分もまだよくわかってない

今回の成果物はこちらになります。

Go

GoのLambdaを作っていきます。
今回はGitHub ActionsとAWS CDKを使って自動デプロイするがメインなお題なのでここはシンプルにHello WorldなLambdaで勘弁カツオです。
ギリギリ補足するとしたらGoでLambda書くときはlambda.Start(func)しましょうというあたりでしょうか。

勘弁カツオなLambda
package main

import (   
  "fmt"
  "github.com/aws/aws-lambda-go/lambda"
)

func hello() (events.APIGatewayProxyResponse, error) {
    return events.APIGatewayProxyResponse{
        StatusCode: 200,
        Body:       "Hello Katsuo",
    }, nil
}

func main() {
  lambda.Start(hello)
}

CDK

今回はAWS APIGatewayからAWS Lambdaを発火するあるあるCDKを作成していきます。
CDKの環境構築な記事は沢山あるので今回は用意されている前提で進めます。

ディレクトリ構造にならってcdkディレクトリを作成しその中でinitします。
今回はtypescriptで実装していきます(というかこれしかやった事ない

$ mkdir cdk
$ cd cdk
$ cdk init app --language=typescript

続いて必要なライブラリをインストールしておきます。
今回使うのは@aws-cdk/aws-lambda @aws-cdk/aws-apigatewayこの二つを使っていきます。

$ npm install --save @aws-cdk/aws-lambda @aws-cdk/aws-apigateway

さて実装していく中身は主にlibパッケージの中なのですがその他のbin, testパッケージを見ていただいてわかるように.tsファイルがcdk.tsというようになっているかと思います。
initしたディレクトリ名で名前解決されているような感じです。
このままだとカッコわりいという方はいい感じの名前に変更してください(今回はこのままいきます)

cdk-stack.tsの実装

では実装です。libディレクトリの直下にある***-stack.ts(今回はcdk-stack.ts)にリソースを定義していきます。

cdk-stack.ts
import cdk = require('@aws-cdk/core');
import lambda = require("@aws-cdk/aws-lambda")
import apigateway = require("@aws-cdk/aws-apigateway")
import {Duration} from "@aws-cdk/core";

export class CdkStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // Lambda
    const lambdaHandler = new lambda.Function(this, "api", {
      code: lambda.Code.fromAsset("../bin"),
      handler: "main",
      runtime: lambda.Runtime.GO_1_X,
      timeout: Duration.seconds(10),
      environment: {
        AUTO_DEPLOY_TEST: "success"
      }
    });

    // API Gateway
    const api = new apigateway.RestApi(this, "auto-deploy-sample-api", {
      restApiName: "auto-deploy-sample-api"
    });

    const lambdaHandlerIntegration = new apigateway.LambdaIntegration(lambdaHandler, {proxy: true});
    api.root.addResource("auto-deploy-sample-api")
    api.root.addMethod("GET", lambdaHandlerIntegration)

  }
}

なんと不思議なことにインフラちんぷんかんぷんなアプリエンジニアな私でもソースコードになると途端に何がされているかが一目瞭然。
一応リソースごとに見ていきます。

Lambda

key value
code デプロイするソースコード
handler エントリーポイントとなる関数名
runtime 使用言語
timeout 実行時のタイムアウト設定
environment 環境変数

codeは今回プロジェクト直下のbin/mainにビルドされるものを参照する想定です。
ここでのソースはlibからではなくcdkから参照します。
大体これくらいが必須なものになってくるかと思います。用途によってはVPCの設定などもあるかと思いますがもちろんできますし、ドキュメントを見れば基本大丈夫な上、ソースコードの参照元を見れば使い方は大体わかるので積極的にIDEで参照ジャンプしましょう。

VPCのところ
    readonly vpc?: ec2.IVpc;
    /**
     * Where to place the network interfaces within the VPC.
     *
     * Only used if 'vpc' is supplied. Note: internet access for Lambdas
     * requires a NAT gateway, so picking Public subnets is not allowed.
     *
     * @default - Private subnets.
     */
    readonly vpcSubnets?: ec2.SubnetSelection;
    /**
     * What security group to associate with the Lambda's network interfaces.
     *
     * Only used if 'vpc' is supplied.
     *
     * @default - If the function is placed within a VPC and a security group is
     * not specified, a dedicated security group will be created for this
     * function.
     */

API Gateway

もはや解説するまでもないと思います。
lambdaのインテグレーションを定義し作成したapigateのResourceとMethodにaddするだけで紐づけられます。簡単便利。

cdk.tsの実装

ここにはAWSのアカウント情報とリージョンを定義していきたいのですがソースコードに直接書き込んでしまうのはいささかナンセンスです。
そこでcdk.jsonを使います。cdk initした際に作成されているファイルで内部にcontextというキーと更にキーバリューを定義しておくことで.tsファイルからapp.node.tryGetContext("キー名")でバリューを取り出すことがきます。
実際には以下のようになります。

cdk.json
{
  "context": {
    "account": "YOUR_AWS_ACCOUNT",
    "region": "YOUR_AWS_REGION"
  },
  "app": "npx ts-node bin/cdk.ts"
}
cdk.ts
#!/usr/bin/env node
import 'source-map-support/register';
import {CdkStack} from '../lib/cdk-stack';
import cdk = require('@aws-cdk/core');

const app = new cdk.App();
new CdkStack(app, 'CdkStack',
    {
        env: {
            account: app.node.tryGetContext("account"),
            region: app.node.tryGetContext("region")
        }
    });

ここまで実装したら一旦cdk bootstrapしておきます。
これをやり忘れると自動デプロイの際にどこにデプロイすれば良いのかわからなくなり怒られます。

$ cdk bootstrap    // --profile *** <- 必要ならつけましょう

GitHub Actions

あと一歩で自動デプロイです。
最後にGitHub Actionsにタスクを登録していきます。

AWS access情報周りを登録

タスクの登録の前に予めSettingsタブのSecretsにAWSのアクセスキー情報などを登録しておきます。プロジェクトなんかでやるならマシンユーザー的なものでやるのが良いのでしょうかね。

accesskey.png

下記のようにAWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEYみたく登録できればおけーです。

addkeys.png

actions本題

ではここからactionsを作成していきます。
まずはリポジトリの真ん中あたりのActionsタブを選択します。

repository.png

actionsタブをクリックするとあらかじめ用意されたactionが沢山出てきますが残念ながらCDKは用意されていません。

actions-list.png

こんなようなものもありましたがタスクの途中でこけてしまい解決もできず...
なのでactionのフロー上で自分でCDKをUbunts環境に用意しデプロイをしてみました。
aws-cliが必要になるのはpython, cdknode, そして今回のgoのビルド環境をあらかじめ用意されているactionからコピペしながら作成したものが以下の通りです。
自分で追加したのはpipのupgradeとaws-cli, cdk, typescriptのインストール、goのビルドコマンドくらいです。

actions.yml
name: CI

on:
  push:
    branches:
      - master

jobs:
  build:

    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v1

    # python
    - name: Set up Python 3.7
      uses: actions/setup-python@v1
      with:
        python-version: 3.7
    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip

    # node.js
    - name: Use Node.js 10.x
      uses: actions/setup-node@v1
      with:
        node-version: 10.x

    # go
    - name: Set up Go 1.13
      uses: actions/setup-go@v1
      with:
        go-version: 1.13
      id: go

    # aws cli & cdk
    - name: aws cli install
      run: |
        pip install awscli --upgrade --user    
        npm install -g aws-cdk

    - name: build and deploy
      env:
        AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
        AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
      run: |
        GOOS=linux GOARCH=amd64 go build -o ./bin/main ./src
        cd cdk
        npm install --save typescript
        npx cdk deploy --require-approval "never"

cdkのデプロイ時に--require-approval "never"をつけているのはCloudFormationの変更内容に問題ないかのチェックが入らず、問答無用でデプロイするためです。

終わりに

これでめでたくmasterリポジトリにマージされると自動でAWSにCDKで定義されたスタックがデプロイされるようになりました。
いちいち実装が終わったらローカルでCDKコマンドを打つ必要もなければ開発ユーザーの権限ではAWSのリソースをいじれないようにしマシンユーザーからだけLambdaの更新ができるようになればプロジェクトはよりセキュアでGitでソースは管理され健康的になります。

果たしてGitHub Actionsの使い方としてこれはあってるのか疑問はありますが何となく「こんなことができる」感はわかったのでこれからまたキャッチアップしていきます。

本当はCodePipeLineとか使うとより良いのでしょうが今日はここまで。

明日は@Black-Spiderさんの記事です!

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