- 投稿日:2019-12-04T23:50:25+09:00
Go言語でHello World
Go言語をやってみた
最近Fintechという言葉を多く耳にするんですが、Fintechって何?
あぁ金融系と技術を併せてFintechねぇ。。。ふ〜ん。かっこいいじゃん。
じゃあ何を使えばできるのかな〜?って
Udemyで探したら、酒井潤先生の講座でGo言語とFintechアプリの開発というのを見つけました!
この講座を受講しながら、自分の為のoutputを書いていきたいと思います。現役シリコンバレーエンジニアが教えるGo入門 + 応用でビットコインのシストレFintechアプリの開発
書いてる内容はここからの情報ばかりになると思います。初期設定は飛ばします。。。詳しくはwebへ。。。
(そのうち書くかも・・・。)
いつも最初にやるやつ・・・Hello World
早速いつものやつをやっていきたいと思います。
どんな参考書にも書いてある「Hello World」、Udemyの講座でもこの法則は不変でした。VBA出身の僕は直感的にこう書いちゃう(真似しないでください)
lesson.goprint("Hello World") >>> syntax error: non-declaration statement outside function body直感的に書いてもダメなことがわかりました。
Go言語として実行するために準備をしましょう!
1.パッケージを宣言する
lesson.gopackage ***2.パッケージの中にmain関数を定義する
lesson.gopackage main func main() { }このmain関数で実行していくのがGoの基本ですね!
Hello Worldを表示しよう!
1.fmtのパッケージをインポートする
lesson.gopackage main import "fmt" func main() { }2.fmtパッケージのPrintlnで表示する
lesson.gopackage main import "fmt" func main() { fmt.Println("Hello World") } >>> Hello Worldはい!!表示できました!!
VBAからpythonを勉強した僕からすると、すごく遠回しな書き方だな。。。って印象です。
でも天下のグーグルが作っているのと、これから注目されている言語なので
Go言語を理解しつつ有益なoutputができるようになりたいと思います!以上です!ありがとうございました!
- 投稿日:2019-12-04T21:07:39+09:00
Go言語に触れてみよう Part.2
はじめに
前回からだいぶ時間が経ってしまいました。。。
今回は、Tour of Goに触れていきながらGoの文法・構文についてみていきます。
Tour of Go
以下からサイトに飛んでください。
Tour of GoTour 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.gopackage 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.gopackage 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.gopackage 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.gopackage 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.gopackage 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.gopackage 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.gopackage 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.gopackage 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.gopackage 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) }終わり
これでとりあえずの基本的なことができるようになると思います。
もっと深くやりたい人は、次の章に進んでください。
- 投稿日:2019-12-04T18:19:36+09:00
[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]
正解しましたか?
正解した方はおめでとうございます
外れてしまったかたは惜しかったですね...(´・ω・`)ちなみにこれらの挙動を見たときの私の反応はこのような感じでした。
₍₍(ง˘ω˘)ว⁾⁾
??????????????????????????
え、ん...? あ、そう...ふーん( ´_ゝ`)なんでやねん
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も同様です。さいごに
「ここまずいですよ」や「そいつぁちげーぜ!」などがありましたらコメントいただけますと幸いです₍₍(ง˘ω˘)ว⁾⁾
ゼロは俺に何も言ってはくれない...
とりあえず仕様であることだけは間違いありません。 https://docs.oracle.com/javase/jp/8/docs/api/java/lang/String.html#split-java.lang.String- ↩
- 投稿日:2019-12-04T18:08:06+09:00
vegetaのビッグバンアタック
この記事は LIFULLその2 Advent Calendar 2019 の4日目の記事です。
ベジータ「ビッグバンアタック!!!!!!!!!!!!???????????????????????」
はい。以下は真面目な内容です目的
作成したサービスに対して、自分でシナリオを組んで負荷試験を行いたい。
背景
負荷試験をする際に、システムによってはキャッシュの機構が存在するため、同じリクエストを何度も投げても期待する負荷試験とならないことがあります。
そのため、リクエストのパラメータを変えながら負荷試験を行いたいということは比較的存在するシナリオだと思います。
しかし過去に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"とかにしてるの、中二病っぽくてすごい好きです。
- 投稿日:2019-12-04T18:08:06+09:00
vegetaのビッグバンアタック?
この記事は LIFULLその2 Advent Calendar 2019 の4日目の記事です。
ベジータ「ビッグバンアタック!!!!!!!!!!!!???????????????????????」
はい。以下は真面目な内容です目的
作成したサービスに対して、自分でシナリオを組んで負荷試験を行いたい。
背景
負荷試験をする際に、システムによってはキャッシュの機構が存在するため、同じリクエストを何度も投げても期待する負荷試験とならないことがあります。
そのため、リクエストのパラメータを変えながら負荷試験を行いたいということは比較的存在するシナリオだと思います。
しかし過去に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"とかにしてるの、中二病っぽくてすごい好きです。
- 投稿日:2019-12-04T16:18:37+09:00
Goを使ってCLIにフラッシュ画を描画する
この記事はtomowarkar ひとりAdvent Calendar 2019の5日目の記事です。
今日は少し前に作ったCLIへのフラッシュ画像の描画について書いていきます。
はじめに
こんな感じで一定時間スパンでn*m行のフィールドにランダムに⚪️??を描画しています。
- 時間ループ、キーアクションでGo Channelを使用
- 描画範囲を構造体で定義
と少し発展的なGoの仕組みを学ぶのに役立ちました。
注意
今回こちらのライブラリを使用させていただきましたがREADMEにもあるように、すでに運用が終了しているライブラリです。(2019年12月5日現在)
使用の際はご注意ください。なお、このライブラリでは代替案として以下のライブラリが挙げられています。
コード全文
コード全文は少し長くなるので折りたたんでおいておきます。
また、今回参考としてライブラリのデモを参考にしました。
参考: https://github.com/nsf/termbox-go/tree/master/_demos
コード全文
(クリックしてください)flash.gopackage 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.gopackage 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() }
- フィールドを初期化
- フィールド情報の0,1,2をそれぞれ⚪️??に変換
- 描画
この流れです。
しかしこのままではコンマ秒単位で描画されるので、描画されていることを認識することができません。
なので以下のようにしてループを定義して描画の様子を確認してみます。tmp.gofunc 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() } }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) }
- メインループで描画を更新、描画のループを行う。
- メインループが回っている間並行してキーイベントのループが回っていて、キーイベントがあった場合
kch
に情報を送る- メインループは
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) }
- メインループは無限ループを行う。
- メインループが回っている間並行して時間イベントのループが回っていて任意の時間[ms]ごとに
tch
に情報を送る- メインループは
tch
から時間イベントの情報を受け取り、時間イベントが発生して場合において描画の更新、再描画を行う。という形になりました。
まとめ
これで、
- 500ms毎に15×15行列がランダムで更新され描画される
- Escキーを押すことで描画画面から脱出ということができました。
行列の数や描画の更新時間を変えて遊んでみてください。コードはGithub Gistにもおいておきます!
https://gist.github.com/tomowarkar/3bb50298393b66148e43e00238e52083
以上明日も頑張ります!!
tomowarkar ひとりAdvent Calendar Advent Calendar 2019追記
先日(4日目)のアドベントカレンダーで同じくtermbox-goを使った記事を発見したので載せておきます。
TUI版インベーダーゲームをGo言語で作る
- 投稿日:2019-12-04T14:50:04+09:00
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の実装を行う場合ある程度割り切りが必要になるのかなと感じています。
なんだか結局解決出来てないじゃないかという終わり方ですが、それでもこういった試行錯誤をしながら実装していく事には意味があるのではないかと思います(思いたい)し、今後も続けていきたいと思います。
- 投稿日:2019-12-04T08:51:23+09:00
golangで楽々API作成
はじめに
2019年に最も注目された言語といえば…
そうだね、golangだね!(私の中では)ってことで今更感はあるが、golang触ってみました!
やったこと
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作成したので、次はクライアント側作ってみます。
- 投稿日:2019-12-04T01:36:42+09:00
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)
しましょうというあたりでしょうか。勘弁カツオなLambdapackage 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.tsimport 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のアクセスキー情報などを登録しておきます。プロジェクトなんかでやるならマシンユーザー的なものでやるのが良いのでしょうかね。
下記のように
AWS_ACCESS_KEY_ID
,AWS_SECRET_ACCESS_KEY
みたく登録できればおけーです。actions本題
ではここからactionsを作成していきます。
まずはリポジトリの真ん中あたりのActions
タブを選択します。actionsタブをクリックするとあらかじめ用意されたactionが沢山出てきますが残念ながらCDKは用意されていません。
こんなようなものもありましたがタスクの途中でこけてしまい解決もできず...
なのでactionのフロー上で自分でCDKをUbunts環境に用意しデプロイをしてみました。
aws-cli
が必要になるのはpython
,cdk
はnode
, そして今回のgo
のビルド環境をあらかじめ用意されているactionからコピペしながら作成したものが以下の通りです。
自分で追加したのはpipのupgradeとaws-cli
,cdk
,typescript
のインストール、goのビルドコマンドくらいです。actions.ymlname: 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さんの記事です!