- 投稿日:2019-05-21T23:45:21+09:00
自分の要件定義
自分のやりたい事(要件定義)
- だんだんとズレてきたので、自分のやりたい事(要件)を再定義
- 元々はAPIを叩いてその出力結果の特定箇所を確認。特定の値ならOK、違うならNG。みたいな事を自動で実行出来ないか思った。そしてそれをGOでやりたいと思ったのがきっかえ
- 調べた限りではgoogle apps scriptや他でも出来るようだけど、GOの勉強も兼ねてやってみたいと思った事が事の始まり
実際にやってみた
- 本での勉強もだんだんと飽きてきたので、少し早いかもしれないが実際にやってみた
- そしてダメ(涙
- サンプルコードの流用、サンプルコード自体を動かしてみるもダメ
- いつものごとく参った。でも諦めないし諦められない。また明日
package main import ( "log" "net/http" ) func main() { /* GETメソッドでURLにアクセス */ res, err := http.Get("https://www.google.com/") if err != nil { log.Fatal(err) } }
- 実行結果
$ go run google.go # command-line-arguments ./google.go:10:5: res declared and not used
- 投稿日:2019-05-21T23:24:08+09:00
Lチカセブン
7つの言語でLチカ。
- C
- Go
- Node.js
- Python
- Ruby
- Rust
- Swift
タイトル先行なの、なんの目的もございません。
言語の選びは適当です。環境
Model : Raspberry Pi 3 Model B Rev 1.2p
OS : Raspbian GNU/Linux 9 (stretch)Dockerで実行環境構築
以前、ラズパイでの開発にDockerを導入する記事を投稿しました。
Raspberry Pi で TensorFlow する Docker 環境構築
- Raspbianにプリインストール & GPIO制御可能なもの
C
Python
- Raspbianに言語 or GPIO制御するライブラリのインストールが必要なもの
Go
Node.js
Ruby
Rust
Swift
せっかくなので後者についてはDockerで実行環境を手に入れます。
もちろんラズパイのローカルにインストールしても問題ありません。docker run -t -i \ --name [container_id] \ --rm \ --privileged \ -v $PWD:/usr/src:rw \ -d [language_image] \ /bin/bashSwift以外は上記だけで動作可能です。
Lチカ
C
# wiringPiライブラリをリンク gcc -o led-blinker led-blinker.c -lwiringPi#include <wiringPi.h> #define GPIO2 2 int main(void) { wiringPiSetupGpio(); pinMode(GPIO2, OUTPUT); for (int i = 0; i < 7; i++) { digitalWrite(GPIO2, HIGH); delay(1000); digitalWrite(GPIO2, LOW); delay(1000); } return 0; }
Go
https://github.com/stianeikeland/go-rpio
go version # 本記事では 1.12.5 go get github.com/stianeikeland/go-rpio
package main import ( "os" "fmt" "time" "github.com/stianeikeland/go-rpio" ) var ( pin = rpio.Pin(2) ) func main() { err := rpio.Open() if err != nil { fmt.Println(err) os.Exit(1) } pin.Output() for i := 0; i < 7; i++ { pin.High() time.Sleep(1 * time.Second) pin.Low() time.Sleep(1 * time.Second) } rpio.Close() }
Node.js
node --version # 本記事では 12.2.0 # https://github.com/fivdi/onoff npm install onoff'use strict'; const Gpio = require('onoff').Gpio; const pin = new Gpio(2, 'out'); const sleep = (ms) => { return new Promise(resolve => setTimeout(resolve, ms)) } const main = async () => { try { for (let i = 0; i < 7; i++) { pin.writeSync(1); await sleep(1000); pin.writeSync(0); await sleep(1000); } } catch (err) { console.log(err); } finally { pin.unexport(); } } main();
Python
python3 --version # 本記事では 3.5.3import RPi.GPIO as GPIO import time GPIO.setmode(GPIO.BCM) pin_out = 2 GPIO.setup(pin_out, GPIO.OUT) for i in range(7): GPIO.output(pin_out, GPIO.HIGH) time.sleep(1) GPIO.output(pin_out, GPIO.LOW) time.sleep(1) GPIO.cleanup()
Ruby
ruby --version # 本記事では2.6.3 # https://github.com/jwhitehorn/pi_piper gem install pi_piperrequire 'pi_piper' pin = PiPiper::Pin.new(:pin => 2, :direction => :out) 7.times do pin.on sleep 1 pin.off sleep 1 end
Rust
rustup -V # 本記事では 1.18.2 cargo init led_blinker # edit Cargo.toml # edit src/main.rs cargo run# Cargo.toml [dependencies] # https://github.com/jwhitehorn/pi_piper rppal = "0.11.2"// src/main.rs use std::error::Error; use std::thread; use std::time::Duration; use rppal::gpio::Gpio; const GPIO_LED: u8 = 2; fn main() -> Result<(), Box<dyn Error>> { let mut pin = Gpio::new()?.get(GPIO_LED)?.into_output(); for _ in 0..7 { pin.set_high(); thread::sleep(Duration::from_secs(1)); pin.set_low(); thread::sleep(Duration::from_secs(1)); }; Ok(()) }
Swift
Swiftは自前でimageをbuildします。
docker build -t [name] .
# Dockerfile FROM resin/rpi-raspbian:stretch ENV APP_ROOT /usr/src WORKDIR $APP_ROOT ADD . $APP_ROOT RUN apt-get update RUN apt-get install libxml2-dev git # install swift4 # https://packagecloud.io/swift-arm/release/packages/raspbian/stretch/swift4_4.2.3_armhf.deb RUN curl -s https://packagecloud.io/install/repositories/swift-arm/release/script.deb.sh | sudo bash RUN apt-get install swift4=4.2.3buildが完了したら他の言語と同様
docker run
します。swift --version # 本記事では 4.2.3 mkdir led-blinker && cd led-blinker swift package init --type executable # edit Package.swift # edit Sources/led-blinker/main.swift swift build// Package.swift import PackageDescription let package = Package( name: "led-blinker", dependencies: [ // https://github.com/uraimo/SwiftyGPIO .package(url: "https://github.com/uraimo/SwiftyGPIO.git", from: "1.0.0") ] )// Sources/led-blinker/main.swift import Glibc import SwiftyGPIO let gpios = SwiftyGPIO.GPIOs(for:.RaspberryPi3) var gp = gpios[.P2]! gp.direction = .OUT for _ in 1...7 { gp.value = 1 sleep(1) gp.value = 0 sleep(1) }参考
ラズパイとサーバーサイドSwiftでLチカ! ( Raspberry Pi 2 + SwiftyGPIO ) by n0bisukeさん
単純に
/sys/class/gpio
を操作する方法もありますが、今回は各言語のライブラリを使用するようにしてみました。
RaspberryPiではPythonが選ばれることが多いですが、たまには他の言語でやってみるのも楽しそうですね。
- 投稿日:2019-05-21T21:29:53+09:00
goでソケット通信
口上
そういえばソケット通信とかの、ネットワークプログラミングの基本を全然理解していなかったので、入門中のgoでとりあえず書いてみた。
ググってもいまいち書きたい仕様にマッチするものがなかったので、試行錯誤でできた内容を、備忘録兼ねて記載。サーバー
まずはサーバー側。
server.gopackage main import ( "fmt" "net" utilsErr "sample/utils/error" utilsNet "sample/utils/net" "time" ) func main() { // 意味はないが雰囲気を出すため、敢て自分のIPを表示 myIP, _ := utilsNet.GetMyLocalIP() fmt.Println("my ip address is now ...", myIP) // 50030ポート(適当)でメッセージを受け取る port := ":50030" protocol := "tcp" tcpAddr, err := net.ResolveTCPAddr(protocol, port) utilsErr.CheckWithExit(err) listner, err := net.ListenTCP(protocol, tcpAddr) utilsErr.CheckWithExit(err) fmt.Println("waiting for the connection ...") for { conn, err := listner.Accept() if err != nil { continue } else { fmt.Println("connected by .. ", conn.RemoteAddr().String()) } // ゴルーチンほんと便利 go handleClient(conn) } } // 送信されたバイトからメッセージを取り出して表示する func handleClient(conn net.Conn) { defer conn.Close() conn.SetReadDeadline(time.Now().Add(10 * time.Second)) messageBuf := make([]byte, 1024) messageLen, err := conn.Read(messageBuf) utilsErr.CheckWithExit(err) message := string(messageBuf[:messageLen]) fmt.Println(message) // この書き込み処理がないと、連続してclientからメッセージを送信すると以下のエラーが発生する // fatal: error: dial tcp 192.168.3.22:50031->192.168.3.22:50030: bind: address already in useexit status 1 // 理由は理解していない・・ conn.SetWriteDeadline(time.Now().Add(10 * time.Second)) conn.Write([]byte(message)) }最後のところ、いったんnet.Connから読み出したメッセージを、再度Connに書き込まないと、クライアントから2回め以降に接続した際にコメントのエラーが発生する。
ちゃんと調べれてないが、書き込むことで接続が一旦クリアされている?クライアント
次、クライアント側。
client.gopackage main import ( "fmt" "net" "os" utilsErr "sample/utils/error" utilsNet "sample/utils/net" "time" ) func main() { // メッセージ用の引数を一つ受け取る仕様 if len(os.Args) != 2 { fmt.Fprintf(os.Stderr, "Usage: %s message", os.Args[0]) os.Exit(1) } message := os.Args[1] // 今回はサーバー/クライアント共にローカルマシン // ポートは適当に serverIP, _ := utilsNet.GetMyLocalIP() serverPort := "50030" clientIP, _ := utilsNet.GetMyLocalIP() clientPort := 50031 // tcpで接続 serverAdd, err := net.ResolveTCPAddr("tcp", serverIP+":"+serverPort) utilsErr.CheckWithExit(err) clientAddr := new(net.TCPAddr) clientAddr.IP = net.ParseIP(clientIP) clientAddr.Port = clientPort conn, err := net.DialTCP("tcp", clientAddr, serverAdd) utilsErr.CheckWithExit(err) defer conn.Close() // メッセージをバイトに変換して送信 conn.SetWriteDeadline(time.Now().Add(10 * time.Second)) conn.Write([]byte(message)) }ポイントは、
・コマンドライン引数で送信したいメッセージを渡す
・クライアント自身のIPを動的に取得する(後述のGetMyLocalIPメソッド)
・今回はローカルマシン内の動作なので、サーバーIPもローカルアドレスになる
くらいかな。ユーティリティー達
一応載せとく。
ipUtil.gopackage net import ( "errors" "net" ) // ローカルIPアドレスを、文字列で取得する // // ・すべてのネットワークインターフェースを走査しする // ・ループバックアドレスは無視する // 出展:https://codeday.me/jp/qa/20190120/147217.html func GetMyLocalIP() (string, error) { ifaces, err := net.Interfaces() if err != nil { return "", err } for _, iface := range ifaces { if iface.Flags&net.FlagUp == 0 { continue // interface down } if iface.Flags&net.FlagLoopback != 0 { continue // loopback interface } addrs, err := iface.Addrs() if err != nil { return "", err } for _, addr := range addrs { var ip net.IP switch v := addr.(type) { case *net.IPNet: ip = v.IP case *net.IPAddr: ip = v.IP } if ip == nil || ip.IsLoopback() { continue } ip = ip.To4() if ip == nil { continue // not an ipv4 address } return ip.String(), nil } } return "", errors.New("are you connected to the network?") }errUtil.gopackage error import ( "fmt" "os" ) func CheckWithExit(err error) { if err != nil { fmt.Fprintf(os.Stderr, "fatal: error: %s", err.Error()) os.Exit(1) } }使い方
# サーバー起動 $ go run server.go # クライアントからメッセージを送る $ go run client/client.go "こんにちは!" $ go run client/client.go "今日はいい天気ですね" $ go run client/client.go "さようなら〜"結果
出展
https://golang.org/pkg/net/
http://kudohamu.hatenablog.com/entry/2014/11/03/071802
https://codeday.me/jp/qa/20190120/147217.html
※サンプルの仕様は、こちらの本に出てくるソケット通信のサンプル(Python)を元にした。次は、この知識を元に、goでもう少し複雑なP2Pプログラムを書いてみる予定。
- 投稿日:2019-05-21T10:00:27+09:00
Goで標準パッケージのみでtest coverage 100%を維持し続けるための開発ガイド
前提
この記事は、Goでテスト駆動で開発をしていく上で、
標準パッケージのみで
、ユニットテストのカバレッジを常に100%を維持しながら開発していくための基本ガイドとなります。
「関数をMockしてテストしていく方法」を主に紹介し、最後に「どうすればtestableなコードを書けるか」という話をしております。なぜ標準パッケージだけでやるのか
Goの思想 に則ると、標準の
Testing
パッケージだけでテストすることが理想だからです。
GoのTesting
パッケージは、他言語のメジャーなテストライブラリに比べるとかなり機能が少ないものの、それゆえにGoの原則さえ理解していれば誰でもすぐに読み書きでき
、Testに合わせてコードを書いていかないといけないからこそ逆説的に適切なコードを書ける
というメリットがあります。テストカバレッジの基準とは
Goのテストカバレッジの分母は、基本的に
C0
または命令網羅率(statement coverage)
と呼ばれる命令をどれくらい網羅しているか
となります。
ここでいう、命令
とは、条件分岐を除いた、なんらかの処理を実行している部分
となります。詳細は、こちら。
Goのテストカバレッジの考え方と計測方法はこちら。
Try Golang! Go標準のカバレッジとはなにものかまた、テストのカバレッジを測定するコマンドはこちら。
テストカバレッジの測定 はじめてのGo言語関数をMockする方法
Goの標準のTestingパッケージには、Mockingなどの他のライブラリによくある機能がないので、コードを書く時点からどうやってテストするかを考えながら書いていく必要があります。
つまり、どうやってMockしていくかを常に考えていく必要があります。例えば、下記のような処理を見てみましょう。
下記のGetData
という関数をテストする場合、中で使用されているGetItem
という関数をMockする必要があります。
なぜなら、コメントアウトにある通り、GetItem
は、DBに実際にSQLを走らせる関数なので、ユニットテストでそのまま走らせるわけにはいかないからです。
また、このGetItem
の直後にエラーハンドリングがあるので、エラーハンドリングの処理もテストで網羅するために、エラーが返却されるケースのMockも必要になります。func (s *Server) GetData(query string) ([]string, error){ d, err := s.db.GetItem(query) // 内部で実際にDBに接続してSQLを走らせる関数 if err != nil { return nil, err } return d, nil }それでは、どうすれば良いか実際に見ていきましょう。
1. 関数をinterfaceに定義する
まず。GoにおけるテストのMockingの一番基本的なやり方である
interface
を使った方法を紹介していきます。Goにおける
interface
は、「実装したオブジェクトが使用できる関数(振る舞い)と入出力の型のみをまとめて定めたもの」といえます。また、関数の型だけを取り決めており、その関数の中身の挙動には一切関与しない
ということもいえます
そして、あるinterfaceを実装したオブジェクトは、interfaceに定義された関数を具体的な挙動
とともに実装しています。
テストでは、このinterfaceの特性を活かし、本番用のtypeとは別に、テスト用のオブジェクトにinterfaceを実装させて、関数のダミーの挙動を定義することでMockしていきます。
そして、そのMockの関数を呼び出す側は、具体的なtypeではなく、interface(を実装したオブジェクト)をレシーバに関数を実行するようにすることで、そのinterfaceを実装したオブジェクトならなんでも受け付けるようにしておきます。文章だけで説明してもぴんと来ないと思うので、以下で具体的な例を見ていきましょう。
a. interfaceをStructのフィールドとして持つ
先程のコードに戻ってみます。
関連するstructやinterfaceも追加しました。type Server struct{ db datagetter } func (s *Server) GetData(query string) ([]string, error){ d, err := s.db.GetItem(query) // 内部で実際にDBに接続してSQLを走らせる関数 if err != nil { return nil, err } return d, nil } type DB struct{} type datagetter interface{ GetItem(string) ([]string, error) } func GetItem(q string) ([]string, error){ // DBからDataを取得して返却 下記のようなイメージ // 1.queryを整形 // 2.整形したqueryとでレシーバのstructが持っているconnectionを利用して、DBからデータを取得 // 3.取得したデータを整形して返却 }ここで注目すべきは、interfaceである
datagetter
を、structであるServer
が中のフィールドの型として使用している点です。
つまり、ここのServer
の中のdb
というフィールドには、datagetter
を実装したオブジェクトならなんでも入る
ということになります。
こうすることで、テストの際には、db
というフィールドにdatagetter
を実装したダミーのオブジェクトを入れることで、datagetter
が宣言しているGetItem
という関数の挙動をテスト用のものに変更することができます。そして、次にこの
GetData
のMock用のstructと関数を作ってみましょう。// DBに接続して実行するメソッドをmockするためのstruct type MockDB struct{} // テスト用にMockした関数 func (*MockDB) GetItem(string) ([]string, error) { // returns dummy response return []string{"test"}, nil }func TestGetData(t *testing.T) { cases := []struct { input string expected []string }{ {"test", []string{"test"}}, } // mock // MockDBをServerの項目として持たせる s := &Server{ db: &MockDB{}, } for _, tc := range cases { // execute d, err := s.GetData(tc.input) // assert if !reflect.DeepEqual(d, tc.expected) { t.Errorf("failed handling valid cases, expected: '%#v', actual: '%#v'", tc.expected, d) } if err != nil { t.Errorf("failed handling valid cases, '%d'", err) } } }上記と同様にこのinterfaceを活用することで、エラーケースのテストも実施することができます。
// DBに接続して実行するメソッドをmockするためのstruct type MockDBError struct{} // テスト用にMockした関数 func (*MockDBError) GetItem(string) ([]string, error) { // returns dummy response return nil, fmt.Errorf("error in GetItem") }func TestGetDataError(t *testing.T) { // set up cases := []struct { input string expected []string }{ {"test", nil}, } // mock // MockDBをServerの項目として持たせる s := &Server{ db: &MockDB{}, } for _, tc := range cases { // execute d, err := s.GetData(tc.input) // assert if !reflect.DeepEqual(d, tc.expected) { t.Errorf("failed handling valid cases, expected: '%#v', actual: '%#v'", tc.expected, d) } if err == nil { t.Errorf("failed handling invalid cases, error was not found when expected") } } }このように、Mockしたい関数をinterfaceに定義して、そのinterfaceをstructのフィールドとして埋め込むことで、関数のテストができるようになります。
b. Mockしたinterfaceを関数の引数に取る
上で紹介したのは、
interfaceをStructのfieldの一つとして持たせることで、そのStructを引数かレシーバに取る関数が内部で使用している関数をMockする
というやり方でした。
ただし、このやり方では、レシーバを取らない関数をMockできません。
例えば、structの初期化の関数などはレシーバは取ろうにも取れないことが多いと思います。そこで、そのような場合は、
interfaceを関数の引数に持つ
ことで関数を必要に応じてMockできます。type Server struct { db DB } type ConnectionMaker interface { MakeDBConnection() (DB, error) } // 引数にInterfaceを取る func InitializeServer(cm ConnectionMaker) (*Server, error) { db, err := cm.MakeDBConnection() // Mockしたい関数 if err != nil { return nil, err } s := &Server{ db: db, } return s, nil }上記の
InitializeServer
は、引数にConnectionMaker
というinterfaceを取っているため、このinterfaceを実装したオブジェクトならなんでも引数として受け付ける
ということになります。
つまり、前項と同様に、今度は引数にMock用のオブジェクトを入れることでテストができるということになります。2. type funcを利用する
ここまででinterfaceを使ってMockする方法を紹介しました。基本的に、interfaceでMockできるところはそうするべきですが、interfaceを使うと冗長になって、可読性が下がる場合があります。
その際の方法として、type func
を利用する方法もあります。
つまり、Goの、関数をオブジェクトとして扱える性質を利用して、テスト対象の関数の引数またはレシーバに関数を持たせることで、外からmockした関数を投げてテストすることができるようにするということです。実際の例を見てみましょう。
// 関数をオブジェクトとして扱うためにtypeとして定義する type ConnectionFunc func() (DB, error) func InitializeServer(connect ConnectionFunc) (*Server, error) { // 引数として受け取った関数を実行する db, err := connect() if err != nil { return nil, err } s := &Server{ db: db, } return s, nil } // 本番用の関数 func connect() (DB, error) { // // DBとの接続を確立して返却 下記のようなイメージ // // 1.環境変数などからポートなどのデータベース接続用の情報を読み込み // // 2.読み込みんだ情報を利用して接続を試行(エラーなら返却) // // 3.成功したらコネクションをオブジェクトとして返却 }上記の
InitializeServer
という関数は、関数(をtypeとして定義したもの)を引数として取って中で実行しているので、引数として投げる関数次第でその挙動を変えることができるということになります。
そして、その引数の、ConnectionFunc
というtypeは、func() DB, error
という形の関数ならなんでも良いので、Mockする関数もこの形を取れば良いということになります。例えば、本番ではこのように使うとして、
func main() { s := InitializeServer(connect) }テストでは、このようにmock用の関数を用意して、テストすることができます。
// 正常系のMock func MockConnect() (DB, error) { return "mocked db", nil } // 異常系のMock func MockFailConnect() (DB, error) { return "", fmt.Errorf("mocked failure") }func TestInitializeServerSuccess(t *testing.T) { // execute(引数にMock関数を渡している) s, err := InitializeServer(MockConnect) // assert if s == nil { // checking if Server is initialized(= not nil) t.Errorf("failed handling with a valid case, return value was nil when expected") } if err != nil { t.Errorf("failed handling valid cases, '%d'", err) } } func TestInitializeServerFail(t *testing.T) { // execute(引数にMock関数を渡している) s, err := InitializeServer(MockFailConnect) // assert if s != nil { // checking if Server is not initialized t.Errorf("failed handling with a invalid case, unexpected return value:'%#v'", s) } if err == nil { t.Errorf("failed handling a invalid case, error was not found when expected") } }Mockを使ったテストのやり方
前項のまとめとして、あらためて実際にどのようにMockを作ってテストをするかということを考えていきましょう。
Step1. Mock作成用の関数をテストパッケージ用に公開する
前項の例では、便宜的にすべて同じパッケージ内の想定で説明していました。
それでも問題はないのですが、Goでは、テスト対象のパッケージとは異なるテスト用のパッケージからテストするのがベストプラクティスという考え方もあります。それは、パッケージのユーザの視点からテストするためです。
しかし、外部のパッケージからテストするとなると、パッケージ内のprivateな関数やstructのfieldはテストケースから直接参照することができない
ということになります。
例えば、最初に例にあげたコードだと、下記のdb
は、Server
のprivate fieldなので外からは参照できません。package datagetter func (s *Server) GetData(query string) ([]string, error){ d, err := s.db.GetItem(query) if err != nil { return nil, err } return d, nil }なので、この場合は、テスト用にMockした関数を実装した
db
を、あらかじめ作って外部向けに公開することになります。
具体的には、以下のようになります。package datagetter // MakeServerWithConnectionError initializes and return server with MockDBConnection func MakeServerWithMockDBConnection() *Server { return &Server{ db: &MockDB{}, } } // MakeServerWithConnectionError initializes and return server with MockDBConnection func MakeServerWithConnectionError() *Server { return &Server{ db: &MockDBError{}, } }Step2. Mockを呼び出してテストをする
そして、テストする側は、publicに提供されたツールを利用することで外部パッケージからでもMockされた関数が利用できることになります。
package datagetter_test import ( "datagetter" ) func TestGetData(t *testing.T) { cases := []struct { input string expected []string }{ {"test", []string{"test"}}, } // mock // 提供されたメソッドを利用してテスト用のオブジェクトを取得する s := datagetter.MakeServerWithMockDBConnection() for _, tc := range cases { // execute d, err := s.GetData(tc.input) // assert if !reflect.DeepEqual(d, tc.expected) { t.Errorf("failed handling valid cases, expected: '%#v', actual: '%#v'", tc.expected, d) } if err != nil { t.Errorf("failed handling valid cases, '%d'", err) } } }テストできる(=Mockできる)コードを書く方法
最後に、これまでのテストのMockの方法を踏まえて、どうやってテストできるコードを書いていくべきかという話をします。
Goでtest coverage 100%を維持し続けるためには、そもそもテストを書きながら(少なくともテストすることを考えながら)コードを書いていくテスト駆動の考え方が必要になります。そして、そのテストできるコード(関数)とは、下記のいずれかを満たしているということになります。
1. そもそもMockしないでそのまま走らせて良い関数
2. interfaceを実装していてMockできる関数
3. 引数にinterfaceを実装したオブジェクトを渡すことでMockできる関数
4. レシーバか引数でtype funcを渡すことでMockできる関数例えば、最初に例として出した
GetData
の関数も、下記のような作りだったら、上の1-4のいずれも満たしておらず、テストが難しくなります。import ( "db" // DB操作パッケージ ) func (s *Server) GetData(query string) ([]string, error){ d, err := db.GetItem(query) // dbは外部からimportしたパッケージのレシーバを取らない関数 if err != nil { return nil, err } return d, nil }まず、
GetData
の中のGetItem
は、DBを操作する処理なのでテストでそのまま走らせていい関数ではありません。また、後続にエラーハンドリングがあるので、テストによってはエラーが返ってくるようにする必要もあります。
そしてGetItem
という関数は、このdb
という外部パッケージのレシーバを持たない関数として持っています。
その上で、引数にはふつうのstringを取っているため、外部から関数の挙動を制御する方法がありません。(この引数のstringに特定の値("test"とか)の場合だけ特殊な処理をするみたいなこともできますが、それは可読性や安全性などの観点から好ましくないのは明らかでしょう。)
このような場合、このコードの書き方がTestableじゃないコードということになるので、GetItem
を定義したinterfaceを用意するか、引数を変更するなどの対応が必要になります。補足
ここまで説明したのが、「標準パッケージのみでテストのカバレッジを満たしながら開発していく」ための方法となります。この記事で書いた方法で、基本的に開発は進められると思います。
ただし、この記事では、テストのカバレッジを満たしていくことに重きを置いているため、可読性などの視点でより良いコードを書いていく方法は最低限しか触れていません。
実際には、「原則に則ってカバレッジを100%満たしたコードを書いているけど、コードの行数が膨大かつ冗長で読みづらい」みたいな状況にはしばしば陥りがちだと思います。
それらに対処する方法は大小いくつかあると思いますが、例えばsub testがその一つだと思います。
なので、実際にそれなりの規模のものを作る上では、そのあたりの方法も別途頭に入れる必要があるとは思います(そのうちそういう記事も書こうかなと思います)。まとめ
Goの標準パッケージのみでテストする際に、関数をMockする方法
1. 関数をinterfaceに定義する
2. type funcを関数の引数かレシーバに持つtest coverage100%を達成する上で、関数が満たしていないといけないこと(いずれか一つ)
1. そもそもMockしないでそのまま走らせて良い関数
2. interfaceを実装していてMockできる関数
3. 引数にinterfaceを実装したオブジェクトを渡すことでMockできる関数
4. レシーバか引数でtype funcを渡すことでMockできる関数参考
- 投稿日:2019-05-21T08:52:04+09:00
Go言語の環境を作ってみた
最近、Go言語の話題をちょこちょこ見かけるので、環境を作ってみました。
環境
- Windows+VirtualBOX+Vagrant
- ゲストOS:CentOS7
環境構築
Windows側作業
- 以下のVagrantfileを作成し、
vagrant up
を実行Vagrant.configure("2") do |config| config.vm.box = "centos/7" config.vm.network "private_network", ip: "192.168.33.20" end
- 立ち上がったら
vagrant ssh
でVMにSSH接続ゲストOS(CentOS)側作業
※Vagrantfileでまとめればよかった。。。
yum update
sudo yum install git
(後述のGinのインストールに必要)- Goのインストール(参考)
$ curl -OL https://dl.google.com/go/go1.12.5.linux-amd64.tar.gz $ sudo tar -C /usr/local -xzf go1.12.5.linux-amd64.tar.gz $ export PATH=$PATH:/usr/local/go/bin
- Hello Worldしてみる(参考)
$ mkdir hello-world $ cd hello-world/ $ sudo vi main.go $ go build $ ./hello-worldmain.gopackage main import "fmt" func main() { fmt.Printf("hello, world\n") }HTTPサーバーとして起動してみる
- Ginのインストール
$ go get -u github.com/gin-gonic/gin
- GinのQuickStartに従ってmain.goを改修
main.gopackage main import "github.com/gin-gonic/gin" func main() { r := gin.Default() r.GET("/ping", func(c *gin.Context) { c.JSON(200, gin.H{ "message": "pong", }) }) r.Run() // listen and serve on 0.0.0.0:8080 }
- 再ビルド
$ go build $ ./hello-world
- Windows側でブラウザから「http://192.168.33.20:8080/ping」にアクセス
- 投稿日:2019-05-21T08:52:04+09:00
Go言語の環境を作ってみた(GinでHTTPサーバー構築)
最近、Go言語の話題をちょこちょこ見かけるので、環境を作ってみました。
環境
- Windows+VirtualBOX+Vagrant
- ゲストOS:CentOS7
環境構築
Windows側作業
- 以下のVagrantfileを作成し、
vagrant up
を実行Vagrant.configure("2") do |config| config.vm.box = "centos/7" config.vm.network "private_network", ip: "192.168.33.20" end
- 立ち上がったら
vagrant ssh
でVMにSSH接続ゲストOS(CentOS)側作業
※Vagrantfileでまとめればよかった。。。
yum update
sudo yum install git
(後述のGinのインストールに必要)- Goのインストール(参考)
$ curl -OL https://dl.google.com/go/go1.12.5.linux-amd64.tar.gz $ sudo tar -C /usr/local -xzf go1.12.5.linux-amd64.tar.gz $ export PATH=$PATH:/usr/local/go/bin
- Hello Worldしてみる(参考)
$ mkdir hello-world $ cd hello-world/ $ sudo vi main.go $ go build $ ./hello-worldmain.gopackage main import "fmt" func main() { fmt.Printf("hello, world\n") }HTTPサーバーとして起動してみる
- Ginのインストール
$ go get -u github.com/gin-gonic/gin
- GinのQuickStartに従ってmain.goを改修
main.gopackage main import "github.com/gin-gonic/gin" func main() { r := gin.Default() r.GET("/ping", func(c *gin.Context) { c.JSON(200, gin.H{ "message": "pong", }) }) r.Run() // listen and serve on 0.0.0.0:8080 }
- 再ビルド
$ go build $ ./hello-world
- Windows側でブラウザから「http://192.168.33.20:8080/ping」にアクセス
- 投稿日:2019-05-21T07:55:56+09:00
Goで使うべきでない文法 - from:GothamGo 2018 - Things in Go I Never Use by Mat Ryer -
概要
この記事は、2018年のGothamGoのMat Ryerさんのtalkの動画のサマリーになります
GothamGo 2018 - Things in Go I Never Use by Mat Ryerこの動画では、主に、「Goで可読性の高いコードを書く上で使わない文法」について具体的なポイントを挙げて話しているので、それらのまとめとなります。
まとめ
elseはできるだけ使わない
Goでは、Happy Path(正常系)は左端に寄せて(関数内でインデントをしない)で書いて、エラーハンドリングなどのその他のケースのみインデントするという書き方が好ましいとされている。
なので、異常系のみ都度if
節で処理して、正常系はネストの中には入れないようにする。
そうすると、必然的にelseを使わないといけないケースは限られてくる。
動画内でも紹介されているリンク: Code: Align the happy path to the left edge// bad if ok { err := something() if err != nil { return errors.Wrap(err, "something") } // do more great things } else { return errors.New("not ok") } // good if !ok { return errors.New("not ok") } err := something() if err != nil { return errors.Wrap(err, "something") } // do more great thingslabelは使わない
処理の流れが把握しづらくなるため。
gotoは使わない
処理の流れが把握しづらくなるため。
arrayは使わない
slice
を使えばメモリの細かい挙動を考えずに複数のarrayを内部で作ってくれるので、厳密なメモリ最適化をしたい場合でもなければ、slice
で事足りる。newは使わない
&type{}
みたいに直接オブジェクトを初期化すれば事足りる。panicはそのままでは使わない
error
を使う。ただし、下記のように、既存のpanicをカスタマイズして使う場合は有意義なものとなる。
func (t *Greeter) Greet1(name string) { // Speakerがnilの場合に、下記の既存のpanicが起きる // "invalid memory address or nil pointer dereference" return t.Speaker.Speak("hello" + name) } func (t *Greeter) Greet2(name string) { if t.Speaker == nil { panic("greeter: cannot call Greet with nil Speaker") } return t.Speaker.Speak("hello" + name) }Structを初期化するときはfield名も明示する
それぞれの値がなにを表すのかわかりづらいため。
// bad // どっちが緯度でどっちが経度かわからない pos := &Position{40.712772, -74.006058} // good pos := &Position{ latitude: 40.712772, longitude: -74.006058, }冗長なレシーバ付き関数の呼び出しはしない
type Greeter struct { Format string } func (g Greeter) Greet(name string) { fmt.Println("hello", name) } func main() { g := Greeter{} Greeter.Greet(g, "Mat") // bad g.Greet("Mat") // good }最後に
さっと振り返れる用にまとめとして書いたもののこの動画自体そこまで長くなく、また前半では「そもそもGoにおける良いコードとは」という話もしており、冗談も交えつつで話も面白いので、もし英語に抵抗がなければ動画を全部実際に見てみることをおすすめします。
- 投稿日:2019-05-21T07:55:56+09:00
Goで可読性を上げるために使うべきでない文法 - from:GothamGo 2018 - Things in Go I Never Use by Mat Ryer -
概要
この記事は、2018年のGothamGoのMat Ryerさんのtalkの動画のサマリーになります
GothamGo 2018 - Things in Go I Never Use by Mat Ryerこの動画では、主に、「Goで可読性の高いコードを書く上で使わない文法」について具体的なポイントを挙げて話しているので、それらのまとめとなります。
まとめ
elseはできるだけ使わない
Goでは、Happy Path(正常系)は左端に寄せて(関数内でインデントをしない)で書いて、エラーハンドリングなどのその他のケースのみインデントするという書き方が好ましいとされている。
なので、異常系のみ都度if
節で処理して、正常系はネストの中には入れないようにする。
そうすると、必然的にelseを使わないといけないケースは限られてくる。
動画内でも紹介されているリンク: Code: Align the happy path to the left edge// bad if ok { err := something() if err != nil { return errors.Wrap(err, "something") } // do more great things } else { return errors.New("not ok") } // good if !ok { return errors.New("not ok") } err := something() if err != nil { return errors.Wrap(err, "something") } // do more great thingslabelは使わない
処理の流れが把握しづらくなるため。
gotoは使わない
処理の流れが把握しづらくなるため。
arrayは使わない
slice
を使えばメモリの細かい挙動を考えずに複数のarrayを内部で作ってくれるので、厳密なメモリ最適化をしたい場合でもなければ、slice
で事足りる。newは使わない
&type{}
みたいに直接オブジェクトを初期化すれば事足りる。panicはそのままでは使わない
error
を使う。ただし、下記のように、既存のpanicをカスタマイズして使う場合は有意義なものとなる。
func (t *Greeter) Greet1(name string) { // Speakerがnilの場合に、下記の既存のpanicが起きる // "invalid memory address or nil pointer dereference" return t.Speaker.Speak("hello" + name) } func (t *Greeter) Greet2(name string) { if t.Speaker == nil { panic("greeter: cannot call Greet with nil Speaker") } return t.Speaker.Speak("hello" + name) }Structを初期化するときはfield名も明示する
それぞれの値がなにを表すのかわかりづらいため。
// bad // どっちが緯度でどっちが経度かわからない pos := &Position{40.712772, -74.006058} // good pos := &Position{ latitude: 40.712772, longitude: -74.006058, }冗長なレシーバ付き関数の呼び出しはしない
例)
type Greeter struct { Format string } func (g Greeter) Greet(name string) { fmt.Println("hello", name) } func main() { g := Greeter{} Greeter.Greet(g, "Mat") // bad g.Greet("Mat") // good }最後に
さっと振り返れる用にまとめとして書いたもののこの動画自体そこまで長くなく、また前半では「そもそもGoにおける良いコードとは」という話もしており、冗談も交えつつで話も面白いので、もし英語に抵抗がなければ動画を全部実際に見てみることをおすすめします。
- 投稿日:2019-05-21T07:35:43+09:00
Protocol BuffersでGoのSlice・Mapを生成させる方法
概要
Protocol Buffersで、Goにコンパイル後にSlice・Mapを生成させるために、protocol definitionにどのように定義すれば良いかの解説となります。
slice
repeated
を使うmessage Result { repeated string message = 1; }これをコンパイルすると、
type Result struct { Messages []string }こうなります。
map
map<string, Bar>
を使うmessage Company { map<int32, string> members = 1; }これをコンパイルすると、
type Company struct { Members map[int32]string }こうなります。
参考
- 投稿日:2019-05-21T07:33:52+09:00
Goで自動生成ファイルをtest coverageの計測対象から除外する方法
概要
gRPCなどを使用していると、Goの自動生成ファイルを使用することになると思います。
その際、
go test
コマンドは、.go
という拡張子のすべてのファイルをtest coverageの計測対象とするため、CIなどでtest coverageを計測している場合、この自動生成ファイルまでtest coverageの分母に入ってしまい、意味のある数字が得られなくなると思います。そこで、それに対処する方法を解説します。
やり方
前提として、
go test
コマンド自体に、特定のファイルを除外するようなオプションはありません。
なので、go test
の生成ファイルから、除外したいファイルに関する記述をgrep
して除外していくことになります。$ go test . -coverprofile cover.out.tmp # "_generated.go"の部分は、自動生成ファイルのファイル名の形式に合わせて変更する $ cat cover.out.tmp | grep -v "_generated.go" > cover.out $ rm -f cover.out.tmp $ go tool cover -func cover.outコピペ用
理解しやすい用に上記で一行ずつのコマンドを記載しましたが、実際はまとめて実行する場合が多いと思うので、コピペして実行する際は下記がおすすめです。
$ TEMP_COVER_OUT=$(mktemp); go test ./... -coverprofile $TEMP_COVER_OUT; cat $TEMP_COVER_OUT | grep -v "pb.go" > cover.out; rm $TEMP_COVER_OUT $ go tool cover -html=cover.out参考
- 投稿日:2019-05-21T00:59:33+09:00
golangのテスト機能を使って競プロを快適にテストしてみる
はじめに
最近少しずつgolangで競技プログラミングを始めた駆け出しの競技プログラマです。
競プロを組んでると思うのが、プログラムを作って例題でテストして…と進めていくと毎回コンソールで値を入れてチェックというのが面倒くさく感じます。
色々調べた結果、golangのテスト機能を使えば簡単にテストできることがわかったのでテスト用ソースセットをつくってみました!なお、ソースは以下にあります。
https://github.com/Atoyr/CompetitiveProgramming実装
※こちらの記事を参考にして作成しました
競プロのテストを「go test」でやりたかったので作った - Qiita
Go言語で標準入出力をスタブする方法 - Qiitagolangのテストの実行については、様々な記事がありますので、本記事では割愛します。
仕組み
テストソースは
main_test.go
になります。
このファイルに標準入力と標準出力を記載してgo testを実行するとmain.go
のmainをテストしてくれます。
具体的には以下のようになっています。
- 問題の入力を文字列配列で各行ごとに設定
- 問題の出力を文字列で設定
- 問題の入力を標準入力の形でmain関数に渡して、標準出力の結果を生成する
- 標準出力の結果と問題の出力を比較ね、簡単でしょ
どないなっとんのこれ
このテストで肝となるのは、標準入力、標準出力の扱いになります。
golangには標準入出力をスタブする機能があるので、それを使って結果を取得します。入出力は以下の関数で実行します。
第一引数に標準入力をスペース区切りで、第二引数のfuncでmain関数を受け取ります。
戻り値は標準出力とエラーメッセージです。
エラーメッセージはエラーが発生していなければ空文字です。func stubio(inbuf string, f func()) (string, string) { inr, inw, _ := os.Pipe() outr, outw, _ := os.Pipe() errr, errw, _ := os.Pipe() orgStdin := os.Stdin orgStdout := os.Stdout orgStderr := os.Stderr inw.Write([]byte(inbuf)) inw.Close() os.Stdin = inr os.Stdout = outw os.Stderr = errw outC := make(chan string) errC := make(chan string) defer close(outC) defer close(errC) go func() { var buf strings.Builder io.Copy(&buf, outr) outr.Close() outC <- buf.String() }() go func() { var buf strings.Builder io.Copy(&buf, errr) errr.Close() errC <- buf.String() }() f() os.Stdin = orgStdin os.Stdout = orgStdout os.Stderr = orgStderr outw.Close() errw.Close() return <-outC, <-errC }テストする標準入力および標準出力は別途typeを作成して対応しています。
今回はatcoderのチュートリアル1問目の標準入力、標準出力を入れています。
A - Welcome to AtCodertype testValue struct { arg []string ans string } func getTests() []testValue { testValues := []testValue{ testValue{ []string{ "1", "2 3", "test", }, "6 test"}, testValue{ []string{ "72", "128 256", "myonmyon", }, "456 myonmyon"}, } return testValues }テスト本体は以下になります。
テストデータを取得してmain関数を実行、結果を比較してNGの場合のみエラーメッセージを出します。
これで間違っていた場合でも、mainの実行結果がわかるので対応できます。
また、そもそもmain関数でエラーが発生してもメッセージを表示するようにしています。
ただ、main関数が無限ループになっている場合は結果が帰ってきません。まずいですよ!func Test_main(t *testing.T) { tests := getTests() for i, tt := range tests { si := strconv.Itoa(i) t.Run("Case "+si, func(t *testing.T) { ret, err := stubio(strings.Join(tt.arg, " "), main) if err != "" { t.Errorf("error func: %s ", err) } ans := fmt.Sprintf("%s\n", tt.ans) if ret != ans { t.Errorf("Unexpected output: '%s' Need: '%s'", ret, ans) } }) } }テストを実行できるmain関数を用意しておく
競プロというんだから手早くソースを書きたい気持ちがあります。
なので、毎回使う機能はあらかじめ宣言しておきます。
自分は入力関連の関数をあらかじめ宣言しています。自分は
nextInt()
やnextString()
という関数を用意して、空白区切の文字をint型やstring型で簡単に取り込みできるようにしています。実際に使ってみる
使う際には、
main.go
のmain関数内に実際の処理を記載します。
あとは標準入力、標準出力をmain_test.go
に記載してテストを実行すればOKです。
テストはコマンドラインやシェルからgo testを実行してください。
おまけ
vimを使うとすごく早くテストできます。
必要なプラグインはvim-goになります。
様々な記事で使い方が書いてあるはずなので、ここでは割愛します。使い方は
main.go
とmain_test.go
のあるディレクトリでvimを開いて、ソースコードを記載。
最後に:GoTest
とやってあげればOKです。
NGのパターンはぜひ自分で試してみてください。
(gif作ったら容量オーバーでアップできなかったのは内緒。)これが一番早いと思います。
- 投稿日:2019-05-21T00:34:01+09:00
「写経」を自動化をリスペクトしてGolangで実装してみた
参考資料
「写経」を自動化し、オートで功徳を積める仕組みを作ってみたのでございます。
実行風景
ちょっと説明
Go
にはpyautogui
のような便利なパッケージがなさそうだったが、今回のような写経の自動化の場合は、「キーボード入力の仮想化」と「クリップボードへの書き込み」ができればよかったので、それらができるパッケージで代替した。一番大変だったところは、
rune
と キーマップのマッピングを書くところ。
DoSyakyou
の部分はオリジナルと同じなのでだいぶ楽できた。sleepが短すぎると正確に動作しないので、オリジナルより気持ち多めにとってある。
コード
package main import ( "time" "github.com/atotto/clipboard" "github.com/micmonay/keybd_event" ) var ( kb keybd_event.KeyBonding kbmap map[rune]int ) func press(ji rune) { kb.SetKeys(kbmap[ji]) kb.Launching() } func pressESC(count int, sleeptime time.Duration) { for i := 0; i < count; i++ { kb.SetKeys(keybd_event.VK_ESC) kb.Launching() } } func pressCTRL_V() { kb.SetKeys(keybd_event.VK_V) kb.HasCTRL(true) kb.Launching() kb.HasCTRL(false) } func pressEnter() { kb.SetKeys(keybd_event.VK_ENTER) kb.Launching() } func GijiHenkan(kanji, roumaji string, sleeptime time.Duration) { for _, ji := range roumaji { press(ji) time.Sleep(sleeptime) } time.Sleep(sleeptime) clipboard.WriteAll(kanji) pressESC(2, sleeptime) pressCTRL_V() time.Sleep(sleeptime) } func Kaigyou(kaigyousleeptime time.Duration) { pressEnter() time.Sleep(kaigyousleeptime) } func DoSyakyou(sleeptime, kaigyousleeptime time.Duration) { GijiHenkan("摩", "ma", sleeptime) GijiHenkan("訶", "ka", sleeptime) GijiHenkan("般", "hann", sleeptime) GijiHenkan("若", "nya", sleeptime) GijiHenkan("波", "ha", sleeptime) GijiHenkan("羅", "ra", sleeptime) GijiHenkan("蜜", "mi", sleeptime) GijiHenkan("多", "ta", sleeptime) GijiHenkan("心", "sinn", sleeptime) GijiHenkan("経", "gyou", sleeptime) Kaigyou(kaigyousleeptime) GijiHenkan("観", "kann", sleeptime) GijiHenkan("自", "ji", sleeptime) GijiHenkan("在", "zai", sleeptime) GijiHenkan("菩", "bo", sleeptime) GijiHenkan("薩", "satu", sleeptime) Kaigyou(kaigyousleeptime) GijiHenkan("行", "gyou", sleeptime) GijiHenkan("深", "jinn", sleeptime) GijiHenkan("般", "hann", sleeptime) GijiHenkan("若", "nya", sleeptime) GijiHenkan("波", "ha", sleeptime) GijiHenkan("羅", "ra", sleeptime) GijiHenkan("蜜", "mixtu", sleeptime) GijiHenkan("多", "ta", sleeptime) GijiHenkan("時", "ji", sleeptime) Kaigyou(kaigyousleeptime) GijiHenkan("照", "syou", sleeptime) GijiHenkan("見", "ken", sleeptime) GijiHenkan("五", "go", sleeptime) GijiHenkan("蘊", "unn", sleeptime) GijiHenkan("皆", "kai", sleeptime) GijiHenkan("空", "kuu", sleeptime) Kaigyou(kaigyousleeptime) GijiHenkan("度", "do", sleeptime) GijiHenkan("一", "ixtu", sleeptime) GijiHenkan("切", "sai", sleeptime) GijiHenkan("苦", "ku", sleeptime) GijiHenkan("厄", "yaku", sleeptime) Kaigyou(kaigyousleeptime) GijiHenkan("舍", "sya", sleeptime) GijiHenkan("利", "ri", sleeptime) GijiHenkan("子", "si", sleeptime) Kaigyou(kaigyousleeptime) GijiHenkan("色", "siki", sleeptime) GijiHenkan("不", "hu", sleeptime) GijiHenkan("異", "i", sleeptime) GijiHenkan("空", "kuu", sleeptime) Kaigyou(kaigyousleeptime) GijiHenkan("空", "kuu", sleeptime) GijiHenkan("不", "hu", sleeptime) GijiHenkan("異", "i", sleeptime) GijiHenkan("色", "siki", sleeptime) Kaigyou(kaigyousleeptime) GijiHenkan("色", "siki", sleeptime) GijiHenkan("即", "soku", sleeptime) GijiHenkan("是", "ze", sleeptime) GijiHenkan("空", "kuu", sleeptime) Kaigyou(kaigyousleeptime) GijiHenkan("空", "kuu", sleeptime) GijiHenkan("即", "soku", sleeptime) GijiHenkan("是", "ze", sleeptime) GijiHenkan("色", "siki", sleeptime) Kaigyou(kaigyousleeptime) GijiHenkan("受", "jyu", sleeptime) GijiHenkan("想", "sou", sleeptime) GijiHenkan("行", "gyou", sleeptime) GijiHenkan("識", "siki", sleeptime) GijiHenkan("亦", "yaku", sleeptime) GijiHenkan("復", "bu", sleeptime) GijiHenkan("如", "nyo", sleeptime) GijiHenkan("是", "ze", sleeptime) Kaigyou(kaigyousleeptime) GijiHenkan("舍", "sya", sleeptime) GijiHenkan("利", "ri", sleeptime) GijiHenkan("子", "si", sleeptime) Kaigyou(kaigyousleeptime) GijiHenkan("是", "ze", sleeptime) GijiHenkan("諸", "syo", sleeptime) GijiHenkan("法", "hou", sleeptime) GijiHenkan("空", "kuu", sleeptime) GijiHenkan("相", "sou", sleeptime) Kaigyou(kaigyousleeptime) GijiHenkan("不", "hu", sleeptime) GijiHenkan("生", "syou", sleeptime) GijiHenkan("不", "hu", sleeptime) GijiHenkan("滅", "metu", sleeptime) Kaigyou(kaigyousleeptime) GijiHenkan("不", "hu", sleeptime) GijiHenkan("垢", "ku", sleeptime) GijiHenkan("不", "hu", sleeptime) GijiHenkan("浄", "jyou", sleeptime) Kaigyou(kaigyousleeptime) GijiHenkan("不", "hu", sleeptime) GijiHenkan("増", "zou", sleeptime) GijiHenkan("不", "hu", sleeptime) GijiHenkan("減", "genn", sleeptime) Kaigyou(kaigyousleeptime) GijiHenkan("是", "ze", sleeptime) GijiHenkan("故", "ko", sleeptime) GijiHenkan("空", "kuu", sleeptime) GijiHenkan("中", "tyuu", sleeptime) Kaigyou(kaigyousleeptime) GijiHenkan("無", "mu", sleeptime) GijiHenkan("色", "siki", sleeptime) Kaigyou(kaigyousleeptime) GijiHenkan("無", "mu", sleeptime) GijiHenkan("受", "jyu", sleeptime) GijiHenkan("想", "sou", sleeptime) GijiHenkan("行", "gyou", sleeptime) GijiHenkan("識", "siki", sleeptime) Kaigyou(kaigyousleeptime) GijiHenkan("無", "mu", sleeptime) GijiHenkan("眼", "genn", sleeptime) GijiHenkan("耳", "ni", sleeptime) GijiHenkan("鼻", "bi", sleeptime) GijiHenkan("舌", "zextu", sleeptime) GijiHenkan("身", "sinn", sleeptime) GijiHenkan("意", "i", sleeptime) Kaigyou(kaigyousleeptime) GijiHenkan("無", "mu", sleeptime) GijiHenkan("色", "siki", sleeptime) GijiHenkan("声", "syou", sleeptime) GijiHenkan("香", "kou", sleeptime) GijiHenkan("味", "mi", sleeptime) GijiHenkan("触", "soku", sleeptime) GijiHenkan("法", "hou", sleeptime) Kaigyou(kaigyousleeptime) GijiHenkan("無", "mu", sleeptime) GijiHenkan("眼", "genn", sleeptime) GijiHenkan("界", "kai", sleeptime) Kaigyou(kaigyousleeptime) GijiHenkan("乃", "nai", sleeptime) GijiHenkan("至", "si", sleeptime) GijiHenkan("無", "mu", sleeptime) GijiHenkan("意", "i", sleeptime) GijiHenkan("識", "siki", sleeptime) GijiHenkan("界", "kai", sleeptime) Kaigyou(kaigyousleeptime) GijiHenkan("無", "mu", sleeptime) GijiHenkan("無", "mu", sleeptime) GijiHenkan("明", "myou", sleeptime) Kaigyou(kaigyousleeptime) GijiHenkan("亦", "yaku", sleeptime) GijiHenkan("無", "mu", sleeptime) GijiHenkan("無", "mu", sleeptime) GijiHenkan("明", "myou", sleeptime) GijiHenkan("尽", "jinn", sleeptime) Kaigyou(kaigyousleeptime) GijiHenkan("乃", "nai", sleeptime) GijiHenkan("至", "si", sleeptime) GijiHenkan("無", "mu", sleeptime) GijiHenkan("老", "rou", sleeptime) GijiHenkan("死", "si", sleeptime) Kaigyou(kaigyousleeptime) GijiHenkan("亦", "yaku", sleeptime) GijiHenkan("無", "mu", sleeptime) GijiHenkan("老", "rou", sleeptime) GijiHenkan("死", "si", sleeptime) GijiHenkan("尽", "jinn", sleeptime) Kaigyou(kaigyousleeptime) GijiHenkan("無", "mu", sleeptime) GijiHenkan("苦", "ku", sleeptime) GijiHenkan("集", "syuu", sleeptime) GijiHenkan("滅", "metu", sleeptime) GijiHenkan("道", "dou", sleeptime) Kaigyou(kaigyousleeptime) GijiHenkan("無", "mu", sleeptime) GijiHenkan("智", "ti", sleeptime) GijiHenkan("亦", "yaku", sleeptime) GijiHenkan("無", "mu", sleeptime) GijiHenkan("得", "toku", sleeptime) Kaigyou(kaigyousleeptime) GijiHenkan("以", "i", sleeptime) GijiHenkan("無", "mu", sleeptime) GijiHenkan("所", "syo", sleeptime) GijiHenkan("得", "toku", sleeptime) GijiHenkan("故", "kou", sleeptime) Kaigyou(kaigyousleeptime) GijiHenkan("菩", "bo", sleeptime) GijiHenkan("提", "dai", sleeptime) GijiHenkan("薩", "saxtu", sleeptime) GijiHenkan("埵", "ta", sleeptime) Kaigyou(kaigyousleeptime) GijiHenkan("依", "e", sleeptime) GijiHenkan("般", "hann", sleeptime) GijiHenkan("若", "nya", sleeptime) GijiHenkan("波", "ha", sleeptime) GijiHenkan("羅", "ra", sleeptime) GijiHenkan("蜜", "mixtu", sleeptime) GijiHenkan("多", "ta", sleeptime) GijiHenkan("故", "ko", sleeptime) Kaigyou(kaigyousleeptime) GijiHenkan("心", "sinn", sleeptime) GijiHenkan("無", "mu", sleeptime) GijiHenkan("罣", "kei", sleeptime) GijiHenkan("礙", "ge", sleeptime) Kaigyou(kaigyousleeptime) GijiHenkan("無", "mu", sleeptime) GijiHenkan("罣", "kei", sleeptime) GijiHenkan("礙", "ge", sleeptime) GijiHenkan("故", "ko", sleeptime) Kaigyou(kaigyousleeptime) GijiHenkan("無", "mu", sleeptime) GijiHenkan("有", "u", sleeptime) GijiHenkan("恐", "ku", sleeptime) GijiHenkan("怖", "hu", sleeptime) Kaigyou(kaigyousleeptime) GijiHenkan("遠", "onn", sleeptime) GijiHenkan("離", "ri", sleeptime) GijiHenkan("一", "ixtu", sleeptime) GijiHenkan("切", "sai", sleeptime) GijiHenkan("顛", "tenn", sleeptime) GijiHenkan("倒", "dou", sleeptime) GijiHenkan("夢", "mu", sleeptime) GijiHenkan("想", "sou", sleeptime) Kaigyou(kaigyousleeptime) GijiHenkan("究", "ku", sleeptime) GijiHenkan("竟", "kyou", sleeptime) GijiHenkan("涅", "ne", sleeptime) GijiHenkan("槃", "hann", sleeptime) Kaigyou(kaigyousleeptime) GijiHenkan("三", "sann", sleeptime) GijiHenkan("世", "ze", sleeptime) GijiHenkan("諸", "syo", sleeptime) GijiHenkan("仏", "butu", sleeptime) Kaigyou(kaigyousleeptime) GijiHenkan("依", "e", sleeptime) GijiHenkan("般", "hann", sleeptime) GijiHenkan("若", "nya", sleeptime) GijiHenkan("波", "ha", sleeptime) GijiHenkan("羅", "ra", sleeptime) GijiHenkan("蜜", "mixtu", sleeptime) GijiHenkan("多", "ta", sleeptime) GijiHenkan("故", "ko", sleeptime) Kaigyou(kaigyousleeptime) GijiHenkan("得", "toku", sleeptime) GijiHenkan("阿", "a", sleeptime) GijiHenkan("耨", "noku", sleeptime) GijiHenkan("多", "ta", sleeptime) GijiHenkan("羅", "ra", sleeptime) GijiHenkan("三", "sann", sleeptime) GijiHenkan("藐", "myaku", sleeptime) GijiHenkan("三", "sann", sleeptime) GijiHenkan("菩", "bo", sleeptime) GijiHenkan("提", "dai", sleeptime) Kaigyou(kaigyousleeptime) GijiHenkan("故", "ko", sleeptime) GijiHenkan("知", "ti", sleeptime) GijiHenkan("般", "hann", sleeptime) GijiHenkan("若", "nya", sleeptime) GijiHenkan("波", "ha", sleeptime) GijiHenkan("羅", "ra", sleeptime) GijiHenkan("蜜", "mixtu", sleeptime) GijiHenkan("多", "ta", sleeptime) Kaigyou(kaigyousleeptime) GijiHenkan("是", "ze", sleeptime) GijiHenkan("大", "dai", sleeptime) GijiHenkan("神", "jinn", sleeptime) GijiHenkan("呪", "syu", sleeptime) Kaigyou(kaigyousleeptime) GijiHenkan("是", "ze", sleeptime) GijiHenkan("大", "dai", sleeptime) GijiHenkan("明", "myou", sleeptime) GijiHenkan("呪", "syu", sleeptime) Kaigyou(kaigyousleeptime) GijiHenkan("是", "ze", sleeptime) GijiHenkan("無", "mu", sleeptime) GijiHenkan("上", "jyou", sleeptime) GijiHenkan("呪", "syu", sleeptime) Kaigyou(kaigyousleeptime) GijiHenkan("是", "ze", sleeptime) GijiHenkan("無", "mu", sleeptime) GijiHenkan("等", "tou", sleeptime) GijiHenkan("等", "dou", sleeptime) GijiHenkan("呪", "syu", sleeptime) Kaigyou(kaigyousleeptime) GijiHenkan("能", "nou", sleeptime) GijiHenkan("除", "jyo", sleeptime) GijiHenkan("一", "ixtu", sleeptime) GijiHenkan("切", "sai", sleeptime) GijiHenkan("苦", "ku", sleeptime) Kaigyou(kaigyousleeptime) GijiHenkan("真", "sinn", sleeptime) GijiHenkan("実", "jitu", sleeptime) GijiHenkan("不", "hu", sleeptime) GijiHenkan("虚", "ko", sleeptime) Kaigyou(kaigyousleeptime) GijiHenkan("故", "ko", sleeptime) GijiHenkan("説", "setu", sleeptime) GijiHenkan("般", "hann", sleeptime) GijiHenkan("若", "nya", sleeptime) GijiHenkan("波", "ha", sleeptime) GijiHenkan("羅", "ra", sleeptime) GijiHenkan("蜜", "mixtu", sleeptime) GijiHenkan("多", "ta", sleeptime) GijiHenkan("呪", "syu", sleeptime) Kaigyou(kaigyousleeptime) GijiHenkan("即", "soku", sleeptime) GijiHenkan("説", "setu", sleeptime) GijiHenkan("呪", "syu", sleeptime) GijiHenkan("曰", "watu", sleeptime) Kaigyou(kaigyousleeptime) GijiHenkan("羯", "gya", sleeptime) GijiHenkan("諦", "tei", sleeptime) GijiHenkan("羯", "gya", sleeptime) GijiHenkan("諦", "tei", sleeptime) Kaigyou(kaigyousleeptime) GijiHenkan("波", "ha", sleeptime) GijiHenkan("羅", "ra", sleeptime) GijiHenkan("羯", "gya", sleeptime) GijiHenkan("諦", "tei", sleeptime) Kaigyou(kaigyousleeptime) GijiHenkan("波", "ha", sleeptime) GijiHenkan("羅", "ra", sleeptime) GijiHenkan("僧", "sou", sleeptime) GijiHenkan("羯", "gya", sleeptime) GijiHenkan("諦", "tei", sleeptime) Kaigyou(kaigyousleeptime) GijiHenkan("菩", "bo", sleeptime) GijiHenkan("提", "ji", sleeptime) GijiHenkan("薩", "so", sleeptime) GijiHenkan("婆", "wa", sleeptime) GijiHenkan("訶", "ka", sleeptime) Kaigyou(kaigyousleeptime) GijiHenkan("般", "hann", sleeptime) GijiHenkan("若", "nya", sleeptime) GijiHenkan("心", "sinn", sleeptime) GijiHenkan("経", "gyou", sleeptime) Kaigyou(kaigyousleeptime) Kaigyou(kaigyousleeptime) Kaigyou(kaigyousleeptime) } func init() { kb, _ = keybd_event.NewKeyBonding() kbmap = make(map[rune]int, 0) kbmap['a'] = keybd_event.VK_A kbmap['b'] = keybd_event.VK_B kbmap['c'] = keybd_event.VK_C kbmap['d'] = keybd_event.VK_D kbmap['e'] = keybd_event.VK_E kbmap['f'] = keybd_event.VK_F kbmap['g'] = keybd_event.VK_G kbmap['h'] = keybd_event.VK_H kbmap['i'] = keybd_event.VK_I kbmap['j'] = keybd_event.VK_J kbmap['k'] = keybd_event.VK_K kbmap['l'] = keybd_event.VK_L kbmap['m'] = keybd_event.VK_M kbmap['n'] = keybd_event.VK_N kbmap['o'] = keybd_event.VK_O kbmap['p'] = keybd_event.VK_P kbmap['q'] = keybd_event.VK_P kbmap['r'] = keybd_event.VK_R kbmap['s'] = keybd_event.VK_S kbmap['t'] = keybd_event.VK_T kbmap['u'] = keybd_event.VK_U kbmap['v'] = keybd_event.VK_V kbmap['w'] = keybd_event.VK_W kbmap['x'] = keybd_event.VK_X kbmap['y'] = keybd_event.VK_Y kbmap['z'] = keybd_event.VK_Z } func main() { time.Sleep(5 * time.Second) sleeptime := 100 * time.Millisecond kaigyousleeptime := 200 * time.Millisecond DoSyakyou(sleeptime, kaigyousleeptime) }
- 投稿日:2019-05-21T00:18:24+09:00
go-sqlmockを使ってGORMで書いたコードをテストする
gormを使ったコードのテストをするとき、Dockerなどを使ってDBを立ち上げてテストする必要がありますが、
go-sqlmock
を使うと、実際のDBの代わりにモックを使ってテストすることができます。この記事は、簡単なモデルを例にしたサンプルコードです。
パッケージのインストール
gormとgo-sqlmockは以下のコマンドでインストールできます。
go get -u github.com/jinzhu/gorm go get -u github.com/DATA-DOG/go-sqlmockテスト対象のソース
RepositoryパターンでCreateとGetをGormで実装した例です。
user/user.gopackage user import ( "github.com/jinzhu/gorm" ) type User struct { ID string `gorm:"primary_key"` Name string } type Repository struct { *gorm.DB } func (p *Repository) Create(id string, name string) error { person := &User{ ID: id, Name: name, } return p.DB.Create(person).Error } func (p *Repository) Get(id string) (*User, error) { var person User err := p.DB.Where("id = ?", id).Find(&person).Error return &person, err }テストコード
先程のコードの、CreateとGetをそれぞれテストしてみます。
まず、DBモックとGORMのオープンです。
user/user_test.gopackage user import ( "regexp" "testing" sqlmock "github.com/DATA-DOG/go-sqlmock" "github.com/jinzhu/gorm" ) func getDBMock() (*gorm.DB, sqlmock.Sqlmock, error) { db, mock, err := sqlmock.New() if err != nil { return nil, nil, err } gdb, err := gorm.Open("postgres", db) if err != nil { return nil, nil, err } return gdb, mock, nil }これを使ってテストを書いてみます。
Createのテスト
user/user_test.gofunc TestCreate(t *testing.T) { db, mock, err := getDBMock() if err != nil { t.Fatal(err) } defer db.Close() db.LogMode(true) r := Repository{DB: db} id := "2222" name := "BBBB" // Mock設定 mock.ExpectQuery(regexp.QuoteMeta( `INSERT INTO "users" ("id","name") VALUES ($1,$2) RETURNING "users"."id"`)). WithArgs(id, name). WillReturnRows( sqlmock.NewRows([]string{"id"}).AddRow(id)) // 実行 err = r.Create(id, name) if err != nil { t.Fatal(err) } }
mock.ExpectQuery
のところで、期待するSQLとパラメータと、実行結果のレコードを設定しています。
実際に実行した時に、この期待結果と異なる場合、以下のようなエラーが返ります。--- FAIL: TestCreate (0.00s) user_test.go:48: Query 'INSERT INTO "users" ("id","name") VALUES ($1,$2) RETURNING "users"."id"', arguments do not match: argument 0 expected [string - 2222X] does not match actual [string - 2222]Getのテスト
user/user_test.gofunc TestGet(t *testing.T) { db, mock, err := getDBMock() if err != nil { t.Fatal(err) } defer db.Close() db.LogMode(true) r := Repository{DB: db} id := "1111" name := "AAAA" // Mock設定 mock.ExpectQuery(regexp.QuoteMeta( `SELECT * FROM "users" WHERE (id = $1)`)). WithArgs(id). WillReturnRows(sqlmock.NewRows([]string{"id", "name"}). AddRow(id, name)) // 実行 res, err := r.Get(id) if err != nil { t.Fatal(err) } if res.ID != id || res.Name != name { t.Errorf("取得結果不一致 %+v", res) } }
- 投稿日:2019-05-21T00:14:19+09:00
寝ます。おやすみなさい(月
- 投稿日:2019-05-21T00:11:45+09:00
【実験】App Engine(Golang)からCloud SQLに接続する際の同時接続数制約
お題
表題の通り。
Cloud SQLはマシンタイプ別に最大同時接続数が変わる。
例えば、db-g1-small
インスタンスなら「1000
」まで。(ちなみに、今回の実験では、このインスタンスを使用)
マシンタイプ 最大同時接続数 db-f1-micro 250 db-g1-small 1,000 その他のすべてのマシンタイプ 4,000 【参考】
https://cloud.google.com/sql/docs/quotas?hl=ja#fixed-limitsそして、もう1点。
App EngineからCloud SQLへの接続時の制限に関して説明するページに以下の記載がある。
「スタンダード環境で動作する各 App Engine インスタンスは、Cloud SQL インスタンスに対する同時接続数が最大 100 個に制限されます。Java 7、Go 1.6、PHP 5.5 のアプリケーションでは、同時接続数が最大 60 個に制限されます。
」
念の為、英語サイトの方も確認。
https://cloud.google.com/appengine/docs/standard/go/cloud-sql/pricing-access-limits?hl=en#app_engine
「Each App Engine instance running in the standard environment cannot have more than 100 concurrent connections to a Cloud SQL instance. For Java 7, Go 1.6, or PHP 5.5 apps the limit is 60 concurrent connections.
」それぞれのApp EngineインスタンスからCloud SQLインスタンスに接続できる数は「
100
」までという制限がある。
「100
」を超えたらどうなるのか? そもそも超えることはできるのか? これらについて実験してみる。
ちなみに、実験結果のまとめは下記。
https://qiita.com/sky0621/items/8792b54ebf6bd21bcf72#まとめ前提
- GCPは知っている。
- Google App Engineのことも名前とどういったものかくらいは知っている。
- MySQL等、リレーショナルデータベースについて、ざっくりとはわかっている。
以下は済んだ上での作業。
- GCPプロジェクトの作成
- Cloud SDKのインストールと初期化・認証
開発環境
# OS
$ cat /etc/os-release NAME="Ubuntu" VERSION="18.04.2 LTS (Bionic Beaver)"# Cloud SDK
$ gcloud version Google Cloud SDK 246.0.0# Golang
$ go version go version go1.11.4 linux/amd64# Vegeta
$ vegeta -version Version: cli/v12.2.0 Commit: 65db074680f5a0860d495e5fd037074296a4c425 Runtime: go1.11.4 linux/amd64 Date: 2019-01-20T15:07:37Z+0000実践
準備
Cloud SQL上に「
db-g1-small
」タイプのインスタンスを作成後、「fs14db01
」という名前でデータベース作成。
※前回参照。Goアプリのソース
main.go
[パッケージ宣言とインポート文]package main import ( "fmt" "net/http" "os" "github.com/jinzhu/gorm" "github.com/google/uuid" _ "github.com/go-sql-driver/mysql" "github.com/labstack/echo" )[CloudSQL接続]func main() { db, err := gorm.Open("mysql", fmt.Sprintf("root:%s@unix(/cloudsql/%s)/fs14db01?parseTime=True", os.Getenv("PASS"), os.Getenv("CONN"))) if err != nil { panic(err) } defer func() { if err := db.Close(); err != nil { panic(err) } }() // -------------------------------------------------------------- // Pattern 1 //db.DB().SetMaxIdleConns(0) //db.DB().SetMaxOpenConns(0) // Pattern 2 //db.DB().SetMaxIdleConns(0) //db.DB().SetMaxOpenConns(95) // Pattern 3 //db.DB().SetMaxIdleConns(0) //db.DB().SetMaxOpenConns(200) // Pattern 4 //db.DB().SetMaxIdleConns(95) //db.DB().SetMaxOpenConns(95) // Pattern 5 //db.DB().SetMaxIdleConns(200) //db.DB().SetMaxOpenConns(200) // Pattern 5 //db.DB().SetMaxIdleConns(1000) //db.DB().SetMaxOpenConns(1000) // --------------------------------------------------------------↑のコメントアウトした部分が今回の実験の肝。
詳細は後述。[WebAPIサーバの設定と起動]e := echo.New() e.POST("/user", func(c echo.Context) error { id := uuid.New().String() u := &User{ ID: id, Name: fmt.Sprintf("user-%s", id), Mail: fmt.Sprintf("mail-%s@example.com", id), } if err := db.Save(u).Error; err != nil { return c.JSON(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) } return c.JSON(http.StatusOK, "OK") }) e.Logger.Fatal(e.Start(fmt.Sprintf(":%s", os.Getenv("PORT")))) }[userレコード構造体]type User struct { ID string `gorm:"column:id"` Name string `gorm:"column:name"` Mail string `gorm:"column:mail"` } func (u *User) TableName() string { return "user" }app.yaml
[app.yaml]runtime: go111 includes: - secret.yamlsecret.yaml
[secret.yaml]env_variables: CONN: 【自分のGCPプロジェクトID】:asia-northeast1:sample-001 PASS: 【Cloud SQLの対象DBのパスワード】実験の肝
先述の通り、ソース中、コメントアウトした部分(DB接続時のコネクションの扱い方)が肝。
最大アイドルコネクション数の設定
アイドル状態にしておくコネクション数。現時点でデフォルトは「
2
」。
「0
以下」ならアイドリング状態にしない。
後述の「最大オープンコネクション数(が0でなく)」を超えた設定をしても最大オープンコネクション数と合わせられる想定。[go/src/database/sql/sql.go]// SetMaxIdleConns sets the maximum number of connections in the idle // connection pool. // // If MaxOpenConns is greater than 0 but less than the new MaxIdleConns, // then the new MaxIdleConns will be reduced to match the MaxOpenConns limit. // // If n <= 0, no idle connections are retained. // // The default max idle connections is currently 2. This may change in // a future release. func (db *DB) SetMaxIdleConns(n int) {最大オープンコネクション数の設定
オープンできるコネクション数の限界。デフォルトは「
0
」で、0の場合は限度なくオープンしにいく。[go/src/database/sql/sql.go]// SetMaxOpenConns sets the maximum number of open connections to the database. // // If MaxIdleConns is greater than 0 and the new MaxOpenConns is less than // MaxIdleConns, then MaxIdleConns will be reduced to match the new // MaxOpenConns limit. // // If n <= 0, then there is no limit on the number of open connections. // The default is 0 (unlimited). func (db *DB) SetMaxOpenConns(n int) {実験
「最大アイドルコネクション数」と「最大オープンコネクション数」を変えながら、App EngineからCloud SQLへの接続時の制限とされている「
1インスタンスあたりの最大同時接続数=100
」を超えた場合の挙動を確認する。
「Vegeta」という負荷ツールを使って、毎秒 X リクエストを5秒間流してみる。
※「X」は実験内容に応じて変更<コマンド例>
$ echo "POST https://idle0-open-unlimited-dot-fs-work-1978.appspot.com/user" | vegeta attack -output=/tmp/vegeta_result.bin -rate=200 -duration=5s上記は「最大アイドルコネクション数」と「最大オープンコネクション数」ともに「0」の設定で実装したアプリに対して、毎秒 200 リクエストを5秒間実行するもの。
■アイドルせず無制限にコネクションオープン
実験内容
以下それぞれのレートで挙動を確認する。
- 毎秒 100 リクエスト
- 毎秒 200 リクエスト
結果予想
毎秒 100 リクエストは問題なく全てのリクエストで 200 OK レスポンスを返す。
それ以外のケースでは、1App Engineインスタンスがリクエストをさばくために同じインスタンスから際限なくCloud SQLにコネクション張りにいき、処理しきれなく失敗となるレスポンスが発生する。実施
【毎秒 100 リクエストを5回分】
Latencies [mean, 50, 95, 99, max] 384.081067ms, 373.585ms, 509.772127ms, 566.791891ms, 595.885825ms Latencies [mean, 50, 95, 99, max] 358.286961ms, 375.221102ms, 475.857641ms, 580.513767ms, 946.918016ms Latencies [mean, 50, 95, 99, max] 348.681478ms, 300.569866ms, 607.253861ms, 670.309172ms, 711.820514ms Latencies [mean, 50, 95, 99, max] 374.172648ms, 389.652528ms, 538.911238ms, 604.715239ms, 643.523716ms Latencies [mean, 50, 95, 99, max] 433.331276ms, 436.36702ms, 532.807731ms, 566.448641ms, 579.696203msSuccess [ratio] 100.00% Success [ratio] 98.00% Success [ratio] 100.00% Success [ratio] 97.80% Success [ratio] 99.60%【毎秒 200 リクエスト】
Latencies [mean, 50, 95, 99, max] 3.656447528s, 2.87129807s, 8.332759389s, 9.642830969s, 10.964205628s Latencies [mean, 50, 95, 99, max] 3.285328297s, 2.518179725s, 7.513602241s, 8.775689476s, 9.69245987s Latencies [mean, 50, 95, 99, max] 3.221727209s, 2.5302591s, 7.36735507s, 8.529768254s, 9.685437683s Latencies [mean, 50, 95, 99, max] 3.191123034s, 2.453029911s, 7.195667353s, 8.48264967s, 8.992381951s Latencies [mean, 50, 95, 99, max] 3.243005216s, 2.496180582s, 7.379312657s, 8.611970742s, 9.856214946sSuccess [ratio] 100.00% Success [ratio] 100.00% Success [ratio] 100.00% Success [ratio] 99.00% Success [ratio] 99.00%結果
レートに関わらず、エラーが発生する回があらわれた。
アイドリングせず、リクエストがあれば無制限にコネクションオープンする設定においては、明確に以下の制限がわかる挙動にならなかった。それぞれのApp EngineインスタンスからCloud SQLインスタンスに接続できる数は「
100
」までという制限がある。■アイドルせずApp Engineの1インスタンスからCloud SQLインスタンスへの同時接続数は「95」
実験内容
以下それぞれのレートで挙動を確認する。
- 毎秒 100 リクエスト
- 毎秒 200 リクエスト
結果予想
全てのケースで全リクエスト 200 OK レスポンスを返す。
実施
【毎秒 100 リクエスト】
Latencies [mean, 50, 95, 99, max] 452.889849ms, 441.297314ms, 601.168658ms, 658.985005ms, 695.736952ms Latencies [mean, 50, 95, 99, max] 544.325051ms, 524.937348ms, 736.472778ms, 762.562193ms, 797.121262ms Latencies [mean, 50, 95, 99, max] 424.314663ms, 400.245986ms, 624.663188ms, 685.486331ms, 735.182867ms Latencies [mean, 50, 95, 99, max] 359.927434ms, 369.749875ms, 437.679054ms, 459.184288ms, 479.638534ms Latencies [mean, 50, 95, 99, max] 420.811133ms, 410.380115ms, 687.227128ms, 826.540714ms, 1.310744809sSuccess [ratio] 100.00% Success [ratio] 100.00% Success [ratio] 100.00% Success [ratio] 100.00% Success [ratio] 98.00%【毎秒 200 リクエスト】
Latencies [mean, 50, 95, 99, max] 2.884555846s, 2.132597469s, 6.722514625s, 8.060224999s, 9.17968338s Latencies [mean, 50, 95, 99, max] 3.241793149s, 2.570532677s, 7.219441917s, 8.723126801s, 9.771961486s Latencies [mean, 50, 95, 99, max] 3.250406007s, 2.494085705s, 7.375126147s, 8.994445098s, 9.536050535s Latencies [mean, 50, 95, 99, max] 3.349980365s, 2.612510507s, 7.615872613s, 8.907686483s, 10.00627403s Latencies [mean, 50, 95, 99, max] 3.167480322s, 2.393982007s, 7.191928282s, 8.429845004s, 9.732517788sSuccess [ratio] 95.90% Success [ratio] 100.00% Success [ratio] 99.90% Success [ratio] 100.00% Success [ratio] 99.00%結果
やはり、レートに関わらず、エラーが発生する回があらわれた。
エラーの内容は下記。Error Set: Post https://open-95-dot-fs-work-1978.appspot.com/user: http2: timeout awaiting response headers■App Engineの1インスタンスからCloud SQLインスタンスへの同時接続数は「95」、アイドル中の接続数「95」
実験内容
以下それぞれのレートで挙動を確認する。
- 毎秒 100 リクエスト
- 毎秒 200 リクエスト
結果予想
全てのケースで全リクエスト 200 OK レスポンスを返す。
実施
【毎秒 100 リクエスト】
Latencies [mean, 50, 95, 99, max] 63.29288ms, 58.153708ms, 118.887245ms, 150.737303ms, 177.86988ms Latencies [mean, 50, 95, 99, max] 48.615312ms, 36.542679ms, 98.190003ms, 190.009727ms, 220.432868ms Latencies [mean, 50, 95, 99, max] 41.683938ms, 32.352082ms, 83.821587ms, 130.818981ms, 168.576253ms Latencies [mean, 50, 95, 99, max] 44.639441ms, 37.782216ms, 90.53393ms, 178.503114ms, 247.647451ms Latencies [mean, 50, 95, 99, max] 39.65634ms, 27.778945ms, 82.746423ms, 153.609157ms, 194.314567msSuccess [ratio] 100.00% Success [ratio] 100.00% Success [ratio] 100.00% Success [ratio] 100.00% Success [ratio] 100.00%【毎秒 200 リクエスト】
Latencies [mean, 50, 95, 99, max] 67.411761ms, 50.444954ms, 154.428824ms, 235.421379ms, 461.221026ms Latencies [mean, 50, 95, 99, max] 33.590508ms, 25.526145ms, 76.341847ms, 106.224266ms, 164.443519ms Latencies [mean, 50, 95, 99, max] 51.818999ms, 39.502633ms, 123.317561ms, 226.575409ms, 698.368402ms Latencies [mean, 50, 95, 99, max] 44.985059ms, 36.407095ms, 99.617469ms, 155.426199ms, 200.001371ms Latencies [mean, 50, 95, 99, max] 36.939052ms, 27.502926ms, 80.993458ms, 171.622113ms, 221.517229msSuccess [ratio] 100.00% Success [ratio] 100.00% Success [ratio] 97.60% Success [ratio] 100.00% Success [ratio] 100.00%【毎秒 1000 リクエスト】
Latencies [mean, 50, 95, 99, max] 3.961474048s, 3.502336695s, 8.950770485s, 10.394910039s, 11.772457859s Latencies [mean, 50, 95, 99, max] 2.932212248s, 2.387595818s, 7.209837606s, 8.700746092s, 10.033527918s Latencies [mean, 50, 95, 99, max] 3.064551746s, 2.409040168s, 7.537645463s, 8.837644957s, 10.498085764s Latencies [mean, 50, 95, 99, max] 3.286309375s, 2.521588034s, 8.251770507s, 9.744014212s, 11.461399857s Latencies [mean, 50, 95, 99, max] 2.781278591s, 2.147507557s, 6.995549716s, 8.492080548s, 10.129869204sSuccess [ratio] 98.90% Success [ratio] 100.00% Success [ratio] 98.76% Success [ratio] 100.00% Success [ratio] 100.00%結果
やはり、レートに関わらず、エラーが発生する回があらわれた。
「MaxIdleConns
」と「MaxOpenConns
」ともに以下ぎりぎりの数値にしてもエラーが発生する。「
1インスタンスあたりの最大同時接続数=100
」Latencies [mean, 50, 95, 99, max] 61.774114ms, 45.738899ms, 158.331009ms, 260.890918ms, 452.438621ms Latencies [mean, 50, 95, 99, max] 35.424859ms, 27.079778ms, 72.247126ms, 154.002554ms, 203.513374ms Latencies [mean, 50, 95, 99, max] 44.677085ms, 33.469162ms, 99.395694ms, 168.410454ms, 274.188638ms Latencies [mean, 50, 95, 99, max] 39.311035ms, 29.595308ms, 83.610205ms, 177.312437ms, 219.554742ms Latencies [mean, 50, 95, 99, max] 55.206895ms, 45.538022ms, 110.174877ms, 156.045373ms, 199.244258ms■App Engineの1インスタンスからCloud SQLインスタンスへの同時接続数は「200」、アイドル中の接続数「200」
実験内容
以下それぞれのレートで挙動を確認する。
- 毎秒 100 リクエスト
- 毎秒 200 リクエスト
結果予想
全てのケースで全リクエスト 200 OK レスポンスを返す。
実施
【毎秒 100 リクエスト】
Latencies [mean, 50, 95, 99, max] 61.774114ms, 45.738899ms, 158.331009ms, 260.890918ms, 452.438621ms Latencies [mean, 50, 95, 99, max] 35.424859ms, 27.079778ms, 72.247126ms, 154.002554ms, 203.513374ms Latencies [mean, 50, 95, 99, max] 44.677085ms, 33.469162ms, 99.395694ms, 168.410454ms, 274.188638ms Latencies [mean, 50, 95, 99, max] 39.311035ms, 29.595308ms, 83.610205ms, 177.312437ms, 219.554742ms Latencies [mean, 50, 95, 99, max] 55.206895ms, 45.538022ms, 110.174877ms, 156.045373ms, 199.244258msSuccess [ratio] 100.00% Success [ratio] 100.00% Success [ratio] 100.00% Success [ratio] 100.00% Success [ratio] 100.00%【毎秒 200 リクエスト】
Latencies [mean, 50, 95, 99, max] 65.297997ms, 46.328684ms, 193.928049ms, 430.665045ms, 1.035112389s Latencies [mean, 50, 95, 99, max] 47.575763ms, 39.133648ms, 93.807154ms, 175.933489ms, 226.582291ms Latencies [mean, 50, 95, 99, max] 61.042032ms, 45.306004ms, 177.094469ms, 288.253016ms, 360.405968ms Latencies [mean, 50, 95, 99, max] 86.173406ms, 55.221664ms, 246.068954ms, 362.140172ms, 794.767386ms Latencies [mean, 50, 95, 99, max] 71.282898ms, 53.339071ms, 164.91989ms, 334.030611ms, 391.249248msSuccess [ratio] 100.00% Success [ratio] 100.00% Success [ratio] 100.00% Success [ratio] 100.00% Success [ratio] 100.00%【毎秒 500 リクエスト】
Latencies [mean, 50, 95, 99, max] 175.614195ms, 163.973076ms, 357.234646ms, 556.376964ms, 1.061742841s Latencies [mean, 50, 95, 99, max] 836.468549ms, 669.739234ms, 2.126049478s, 3.004736981s, 4.667805048s Latencies [mean, 50, 95, 99, max] 621.899782ms, 473.544358ms, 1.565957858s, 2.353069857s, 3.175849897s Latencies [mean, 50, 95, 99, max] 531.594674ms, 424.57262ms, 1.302940472s, 1.757911144s, 3.494456259s Latencies [mean, 50, 95, 99, max] 116.755608ms, 100.680707ms, 254.908827ms, 351.484135ms, 2.023495735sSuccess [ratio] 99.84% Success [ratio] 100.00% Success [ratio] 100.00% Success [ratio] 100.00% Success [ratio] 97.04%■App Engineの1インスタンスからCloud SQLインスタンスへの同時接続数は「1000」、アイドル中の接続数「1000」
実験内容
以下それぞれのレートで挙動を確認する。
- 毎秒 500 リクエスト
- 毎秒 1000 リクエスト
結果予想
全てのケースで全リクエスト 200 OK レスポンスを返す。
実施
【毎秒 500 リクエスト】
Latencies [mean, 50, 95, 99, max] 568.785372ms, 440.275159ms, 1.332036036s, 2.033318787s, 2.992619632s Latencies [mean, 50, 95, 99, max] 494.718679ms, 345.014322ms, 1.37466731s, 1.957682831s, 2.884500171s Latencies [mean, 50, 95, 99, max] 585.637774ms, 435.852227ms, 1.505050567s, 2.182076798s, 3.13677678s Latencies [mean, 50, 95, 99, max] 564.618249ms, 426.371331ms, 1.502413537s, 2.206958146s, 4.03235266s Latencies [mean, 50, 95, 99, max] 382.541357ms, 325.446928ms, 877.323339ms, 1.282798842s, 2.280180238sSuccess [ratio] 100.00% Success [ratio] 100.00% Success [ratio] 100.00% Success [ratio] 100.00% Success [ratio] 97.32%【毎秒 1000 リクエスト】
Latencies [mean, 50, 95, 99, max] 2.966114857s, 2.254049409s, 7.525979114s, 8.963435191s, 10.119524818s Latencies [mean, 50, 95, 99, max] 3.029569385s, 2.502902361s, 7.436747535s, 9.017578718s, 10.497356724s Latencies [mean, 50, 95, 99, max] 3.210386112s, 2.682305189s, 7.752421804s, 9.184400957s, 10.409681587s Latencies [mean, 50, 95, 99, max] 3.123610982s, 2.622561986s, 7.491094617s, 8.867739566s, 10.348725103s Latencies [mean, 50, 95, 99, max] 3.27176631s, 2.829572453s, 7.693434499s, 9.049754142s, 10.556765042sSuccess [ratio] 100.00% Success [ratio] 98.62% Success [ratio] 100.00% Success [ratio] 100.00% Success [ratio] 99.44%まとめ
想定と違っていて、困惑。
それぞれのApp EngineインスタンスからCloud SQLインスタンスに接続できる数は「
100
」までという制限がある。とのことなので、「
SetMaxIdleConns
」及び「SetMaxOpenConns
」で指定すべきは「100
未満」と想定していた。
が、実際は、「1000
」を指定していたとしても、それによってメモリを圧迫して異常終了するでもない。どのような設定であっても一定の割合で以下のようにタイムアウトは発生する。
Error Set: Post https://open-95-dot-fs-work-1978.appspot.com/user: http2: timeout awaiting response headers傾向としては、負荷に対して十分な
MaxIdleConns
を設定していた場合にレイテンシーの改善とエラー発生率の減少が見られる。
(ただ、それも毎秒 100 ないし 200 リクエストまでの傾向で、毎秒 500 リクエスト以上になってくると、それほどはっきりとは言えない。)【毎秒 100 リクエストを5回分】
No MaxIdleConns MaxOpenConns Latencies
(mean)Success
(%)01a 0 0 384.081067ms 100 01b 0 0 358.286961ms 98 01c 0 0 348.681478ms 100 01d 0 0 374.172648ms 97.8 01e 0 0 433.331276ms 99.6 02a 0 95 452.88984ms 100 02b 0 95 544.325051ms 100 02c 0 95 424.314663ms 100 02d 0 95 359.927434ms 100 02e 0 95 420.811133ms 98 03a 95 95 63.29288ms 100 03b 95 95 48.615312ms 100 03c 95 95 41.683938ms 100 03d 95 95 44.639441ms 100 03e 95 95 39.65634ms 100 04a 200 200 61.774114ms 100 04b 200 200 35.424859ms 100 04c 200 200 44.677085ms 100 04d 200 200 39.311035ms 100 04e 200 200 55.206895ms 100 【毎秒 200 リクエストを5回分】
No MaxIdleConns MaxOpenConns Latencies
(mean)Success
(%)01a 0 0 3.656447528s 100 01b 0 0 3.285328297s 100 01c 0 0 3.221727209s 100 01d 0 0 3.191123034s 99 01e 0 0 3.243005216s 99 02a 0 95 2.884555846s 95.9 02b 0 95 3.241793149s 100 02c 0 95 3.250406007s 99.9 02d 0 95 3.349980365s 100 02e 0 95 3.167480322s 99 03a 95 95 67.411761ms 100 03b 95 95 33.590508ms 100 03c 95 95 51.818999ms 97.6 03d 95 95 44.985059ms 100 03e 95 95 36.939052ms 100 04a 200 200 65.297997ms 100 04b 200 200 47.575763ms 100 04c 200 200 61.042032ms 100 04d 200 200 86.173406ms 100 04e 200 200 71.282898ms 100 【毎秒 500 リクエストを5回分】
No MaxIdleConns MaxOpenConns Latencies
(mean)Success
(%)01a 200 200 175.614195ms 99.84 01b 200 200 836.468549ms 100 01c 200 200 621.899782ms 100 01d 200 200 531.594674ms 100 01e 200 200 116.755608ms 97.4 02a 1000 1000 568.785372ms 100 02b 1000 1000 494.718679ms 100 02c 1000 1000 585.637774ms 100 02d 1000 1000 564.618249ms 100 02e 1000 1000 382.541357ms 97.32 【毎秒 1000 リクエストを5回分】
No MaxIdleConns MaxOpenConns Latencies
(mean)Success
(%)01a 95 95 3.961474048s 98.9 01b 95 95 2.932212248s 100 01c 95 95 3.064551746s 98.76 01d 95 95 3.286309375s 100 01e 95 95 2.781278591s 100 02a 1000 1000 2.966114857s 100 02b 1000 1000 3.029569385s 98.62 02c 1000 1000 3.210386112s 100 02d 1000 1000 3.123610982s 100 02e 1000 1000 3.27176631s 99.44 う〜ん、わからない。