20190521のGoに関する記事は15件です。

自分の要件定義

自分のやりたい事(要件定義)

  • だんだんとズレてきたので、自分のやりたい事(要件)を再定義
  • 元々は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
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Lチカセブン

7つの言語でLチカ。

  1. C
  2. Go
  3. Node.js
  4. Python
  5. Ruby
  6. Rust
  7. 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/bash

Swift以外は上記だけで動作可能です。

Lチカ

1秒間隔で7回チカらせます。
wiring.png


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.3
import 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_piper
require '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.3

buildが完了したら他の言語と同様 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が選ばれることが多いですが、たまには他の言語でやってみるのも楽しそうですね。

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

goでソケット通信

口上

そういえばソケット通信とかの、ネットワークプログラミングの基本を全然理解していなかったので、入門中のgoでとりあえず書いてみた。
ググってもいまいち書きたい仕様にマッチするものがなかったので、試行錯誤でできた内容を、備忘録兼ねて記載。

サーバー

まずはサーバー側。

server.go
package 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.go
package 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.go
package 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.go
package 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プログラムを書いてみる予定。

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

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)
        }
    }
}

The Go Playground

上記と同様にこの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")
        }
    }
}

The Go Playground

このように、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")
    }
}

The Go Playground

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できる関数

参考

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

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-world
main.go
package main

import "fmt"

func main() {
    fmt.Printf("hello, world\n")
}

↓↓↓結果
image.png

HTTPサーバーとして起動してみる

  • Ginのインストール
$ go get -u github.com/gin-gonic/gin 
main.go
package 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

↓↓↓サーバー起動
image.png

image.png
→ちゃんとアクセスできてる

↓サーバーサイドのログ
image.png
ログがμ秒単位なのがいかにも処理速度に自信がある感じがして良いですね(笑)

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

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-world
main.go
package main

import "fmt"

func main() {
    fmt.Printf("hello, world\n")
}

↓↓↓結果
image.png

HTTPサーバーとして起動してみる

  • Ginのインストール
$ go get -u github.com/gin-gonic/gin 
main.go
package 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

↓↓↓サーバー起動
image.png

image.png
→ちゃんとアクセスできてる

↓サーバーサイドのログ
image.png
ログがμ秒単位なのがいかにも処理速度に自信がある感じがして良いですね(笑)

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

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 things

labelは使わない

処理の流れが把握しづらくなるため。

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における良いコードとは」という話もしており、冗談も交えつつで話も面白いので、もし英語に抵抗がなければ動画を全部実際に見てみることをおすすめします。

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

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 things

labelは使わない

処理の流れが把握しづらくなるため。

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における良いコードとは」という話もしており、冗談も交えつつで話も面白いので、もし英語に抵抗がなければ動画を全部実際に見てみることをおすすめします。

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

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
}

こうなります。

参考

Go Generated Code - Protocol Buffers

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

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

参考

How to ignore Generated files from Go test coverage

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

golangのテスト機能を使って競プロを快適にテストしてみる

はじめに

最近少しずつgolangで競技プログラミングを始めた駆け出しの競技プログラマです。
競プロを組んでると思うのが、プログラムを作って例題でテストして…と進めていくと毎回コンソールで値を入れてチェックというのが面倒くさく感じます。
色々調べた結果、golangのテスト機能を使えば簡単にテストできることがわかったのでテスト用ソースセットをつくってみました!

なお、ソースは以下にあります。
https://github.com/Atoyr/CompetitiveProgramming

実装

※こちらの記事を参考にして作成しました
競プロのテストを「go test」でやりたかったので作った - Qiita
Go言語で標準入出力をスタブする方法 - Qiita

golangのテストの実行については、様々な記事がありますので、本記事では割愛します。

仕組み

テストソースは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 AtCoder

type 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を実行してください。
gotest1.gif

おまけ

vimを使うとすごく早くテストできます。
必要なプラグインはvim-goになります。
様々な記事で使い方が書いてあるはずなので、ここでは割愛します。

使い方はmain.gomain_test.goのあるディレクトリでvimを開いて、ソースコードを記載。
最後に:GoTestとやってあげればOKです。
gotest2.gif

NGのパターンはぜひ自分で試してみてください。
(gif作ったら容量オーバーでアップできなかったのは内緒。)

これが一番早いと思います。

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

「写経」を自動化をリスペクトしてGolangで実装してみた

参考資料

「写経」を自動化し、オートで功徳を積める仕組みを作ってみたのでございます。

実行風景

syakyou2.gif

ちょっと説明

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)
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

go-sqlmockを使ってGORMで書いたコードをテストする

gopher.png

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.go
package 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.go
package 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.go
func 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.go
func 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)
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

寝ます。おやすみなさい(月

今日は寝る

  • 所用で遅くなったので今日はやらずに寝よう
  • その前に明日へ向けて前日のまとめ

昨日実施した事

  • ローカルリポジトリを作成
  • ローカルリポジトリへはコミット出来た
  • リモートリポジトリ(GitHUB)へはプッシュ出来ない
  • プロトコルはhttps→sshへ変更した
  • GitHUBには公開鍵を登録済

Todo(やる事)

  • ローカルリポジトリからリモートリポジトリへpush出来るようにする
  • GOの勉強を進める
    • いつものことだけど本題を進める上で便利な事、あったらいい事などサイドプロジェクトがどんどん本題になってしまい本末転倒になる事が今まで多々あった
    • 大切なのはGO言語のお勉強なので
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【実験】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.yaml

secret.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.696203ms
Success       [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.856214946s
Success       [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.310744809s
Success       [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.732517788s
Success       [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.314567ms
Success       [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.517229ms
Success       [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.129869204s
Success       [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.244258ms
Success       [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.249248ms
Success       [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.023495735s
Success       [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.280180238s
Success       [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.556765042s
Success       [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

う〜ん、わからない。

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