IntelliJ IDEAを使ったGo言語のセットアップ

Go言語のlocalへのインストール方法からIntelliJ IDEAへのプロジェクトのインポートと設定について記述します。

Go言語のインストール

Go言語のインストールにはgoenvを使用します。

$ git clone https://github.com/syndbg/goenv.git ~/.goenv

bashを使用している場合は以下のコマンドを実行して .bash_profile に設定を追加します。

$ echo 'export GOENV_ROOT="$HOME/.goenv"' >> ~/.bash_profile
$ echo 'export PATH="$GOENV_ROOT/bin:$PATH"' >> ~/.bash_profile
$ echo 'eval "$(goenv init -)"' >> ~/.bash_profile

Go言語をインストールします。

$ goenv install 1.10.4
$ goenv local 1.10.4
$ go version
go version go1.10.4 darwin/amd64

InteliJ IDEAにGoプラグインをインストール

Configure -> Plugins をクリックします。

image

Browse repositories をクリックします。

image

Go プラグインをインストールします。

image

インストールが完了したら、IntelliJ IDEAをリスタートします。

InteliJのプロジェクトのインポートとGoの設定

Import Project からプロジェクトを選択してを開きます。

image

image

Create project from existing sources にチェックして、 Next をクリックします。

image

Next をクリックします。

image

Next をクリックします。

image

Finish をクリックします。

image

これでプロジェクトのインポートが完了します。

IntelliJ IDEAのGo言語の設定

GOROOTとGOPATHの設定

GOROOTGOPATH を確認します。

$ go env
...
GOROOT="/Users/user_name/.goenv/versions/1.10.4"
GOPATH="/Users/user_name/go"
...

Languages & Frameworks -> Go -> GOROOT を開きます。

image

GOROOTを指定します。パスを入力するにはShift + Cmd + Gでダイアログを表示することができます。

image

Languages & Frameworks -> Go -> GOPATH を開きます。

image

Project GOPATHGOPATH を指定します。パスを入力するにはShift + Cmd + Gでダイアログを表示することができます。

image

Building a CI for Golang test

I built a CI with Jenkins for Golang test. We run go test on a Docker container and even run Jenkins on a Docker container.

Directories

app
├── docker
│   ├── dockerfiles # Dockerfiles for unit test
│   └── test
│       ├── init-db.sh # This initializes DB before testing
│       └── test.sh # Testing script
└── Jenkinsfile # The configuration for Jenkins pipeline

Environment of CI

Our Jenkins server uses an EC2 instance of t2.large, and the server runs on Docker container, and even a unit test run on Docker container on the container Jenkins runs with /var/run/docker.sock.

Jenkins loads Jenkinsfile and then execute it on the Jenkins pipeline.

How to build an execution environment

Create an AWS EC2 instance

We prepare the instance of EC2 installed Docker CE. Please see Get Docker CE for CentOS installation guide.

Create a Docker image for golang unit test

Jenkins

Dockerfile

FROM jenkins/jenkins:lts

# Switch to root user
USER root

# Install Docker
RUN apt-get update
RUN apt-get install -y \
     apt-transport-https \
     ca-certificates \
     curl \
     gnupg2 \
     software-properties-common

RUN curl -fsSL https://download.docker.com/linux/debian/gpg | apt-key add -
RUN add-apt-repository \
   "deb [arch=amd64] https://download.docker.com/linux/debian \
   $(lsb_release -cs) \
   stable"
RUN apt-get update
RUN apt-get install -y docker-ce
RUN echo "jenkins ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers

# Switch back to jenkins user
USER jenkins

# Set system timezone JST

ENV TZ Asia/Tokyo

Run on a Jenkins host.

$ docker build --rm --tag jenkins-docker:latest .

Golang

Dockerfile

FROM circleci/golang:1.9

# Install goose
RUN curl https://glide.sh/get | sh
RUN go get bitbucket.org/liamstask/goose/cmd/goose

# Set system timezone JST
ENV TZ Asia/Tokyo

Run on our Jenkins host.

$ docker build --rm --tag golang:latest .

Launch Jenkins

$ sudo docker run --env JAVA_OPTS=-Dorg.apache.commons.jelly.tags.fmt.timeZone=Asia/Tokyo -v /var/run/docker.sock:/var/run/docker.sock --name jenkins -d -p 80:8080 -p 50000:50000 -v jenkins_home:/var/jenkins_home jenkins-docker:latest

-v /var/run/docker.sock:/var/run/docker.sock is used to manipulate host’s Docker because we want to launch containers on host-side.

-v jenkins_home:/var/jenkins_home is used to store everything of our Jenkins configurations and build results on our host’s filesystem. If you
move your Jenkins to another host or backup your Jenkins data, read this page.

Add a job to Jenkins

We need to enable Jenkins to hook Pull Requests when some developer does it.

Below settings on Jenkins.

Add a credential of GitHub enterprise

Because we use GitHub enterprise our development.

Credentials -> Jenkins -> Global credentials -> Add Credentials

key value
Kind Username with password
Scope Global
Username ci
Password ****

Add our GitHub enterprise

Configure System -> GitHub Enterprise Servers

key value
API endpoint http://***/api/v3
Name GitHub Enterprise

Create a job

New Item -> GitHub Organization -> OK

Configure the job’s settings

<Job> -> Configure -> Projects

key value
API endpoint GitHub Enterprise (http://***/api/v3)
Credentials ci/****
Owner some-repogitory
Script Path Jenkinsfile

<Job> -> Configure -> Projects -> Behaviours

key value
Filter by name (with regular expression) some-repogitory
Discover pull request from forks – Strategy Merging the pull request with the current target branch revision
Discover pull request from forks – Trust Everyone

Create a webhook in GitHub

To hook PR in Jenkins, We need to create a webhook in GitHub. Note that we must use the user has right permission.

<your repository> -> Settings -> Hooks

key value
Payload URL http://***/github-webhook/
Content type application/json
Which events would you like to trigger this webhook? Send me everything
Active true

Disable Jenkins’ authentication

Because we use Jenkins in a secure place, there are no incoming packets from the internet.

Manage Jenkins -> Configure Global Security -> Access Control -> Authorization -> check Anyone can do anything

Upgrade Jenkins

Since jenkins_home Docker volume has all Jenkins’ setting files, We pull a latest Docker image and relaunch Docker container, that’s it!

$ sudo docker stop jenkins
$ sudo docker rm jenkins
$ sudo docker pull jenkins/jenkins:lts
$ cd app/docker/dockerfiles/jenkins
$ docker build --rm --tag jenkins-docker:latest .
$ sudo docker run --env JAVA_OPTS=-Dorg.apache.commons.jelly.tags.fmt.timeZone=Asia/Tokyo -v /var/run/docker.sock:/var/run/docker.sock --name jenkins -d -p 80:8080 -p 50000:50000 -v jenkins_home:/var/jenkins_home jenkins-docker:latest

Jenkinsfile template

The Jenkinsfile we use, almost same, like this;

pipeline {
    agent any

    stages {
        stage('Checkout') {
            steps {
                step($class: 'GitHubSetCommitStatusBuilder')
                checkout scm
            }
        }

        stage('Start up containers') {
            steps {
                sh "sudo docker network create ci${env.EXECUTOR_NUMBER}"

                sh "sudo docker run -d --name mysql${env.EXECUTOR_NUMBER} --network ci${env.EXECUTOR_NUMBER} -p 3306${env.EXECUTOR_NUMBER}:3306 circleci/mysql:5.7"
                sleep(10)
                sh "sudo docker run -d --name redis${env.EXECUTOR_NUMBER} --network ci${env.EXECUTOR_NUMBER} redis:4.0"

                script {
                    if (sh (
                            script: "sudo docker create --name golang${env.EXECUTOR_NUMBER} --network ci${env.EXECUTOR_NUMBER} golang:latest bash /go/src/app/docker/test/test.sh",
                            returnStatus: true
                    ) == 0) {
                        sh "sudo docker cp ${env.WORKSPACE} golang${env.EXECUTOR_NUMBER}:/go/src/leo-server"
                    }
                }
            }
        }

        stage('Initialize containers') {
            steps {
                // Initialize something like DB
            }
        }

        stage('Unit test') {
            steps {

                script {
                    if (sh (
                            script: "sudo docker start -a golang${env.EXECUTOR_NUMBER}",
                            returnStatus: true
                    ) != 0) {
                        currentBuild.result = 'FAILURE'
                    }
                }

                // Copy test report and convert it into junit xml report
                sh "sudo docker cp golang${env.EXECUTOR_NUMBER}:/go/src/app/report.xml ."

                step([$class: 'JUnitResultArchiver', testResults: 'report.xml'])
            }
        }
    }

    post {
        always {
            sh script: "sudo docker stop mysql${env.EXECUTOR_NUMBER}", returnStatus: true
            sh script: "sudo docker stop redis${env.EXECUTOR_NUMBER}", returnStatus: true

            sh script: "sudo docker rm mysql${env.EXECUTOR_NUMBER}", returnStatus: true
            sh script: "sudo docker rm redis${env.EXECUTOR_NUMBER}", returnStatus: true
            sh script: "sudo docker rm golang${env.EXECUTOR_NUMBER}", returnStatus: true

            sh script: "sudo docker network rm ci${env.EXECUTOR_NUMBER}", returnStatus: true
        }
    }
}

Our test script, test.sh, like this;

#!/bin/bash

sudo chown -R circleci:circleci /go/src

cd /go/src/leo-server

echo 'Installing go-packages...'
glide i

echo 'Migrating DBs...'
go get bitbucket.org/liamstask/goose/cmd/goose
goose -env=ci -path=database/user up

echo 'Installing testing libraries...'
go get -u github.com/jstemmer/go-junit-report

echo 'Testing...'
go test -v ./... 2>&1 > tmp
status=$?
go-junit-report < tmp > report.xml

exit ${status}

Golangでユニットテスト書くテクニック

Goは他のフレームワークにあるような大きなアサーションツールを持っていません。Goでは testing.T オブジェクトのメソッドがテストに使われます。

  • T.Error(args ...interface{}) または T.Error(msg string, args interface{}) はメッセージを受け取ってテストを失敗させるために使用されます
  • T.Fatal(args ...interface{}) または T.Fatal(mst string, args interface{})T.Error() と似ていますがテストが失敗すると、それ以降のテストは実行されません。テストが失敗した時それ以降のテストも失敗する場合、 T.Fatal() を使うべきです

以下ではGoのテスト使用される2つのテクニックを紹介します。

モックとスタブにインターフェースを使用する

外部ライブラリに依存したコードを書いていて、その外部ライブラリが正しく利用されているかテストしたいときを考えます。

Goのインターフェースはメソッドの期待する動作を表しています。 例として io.Writer を見てみます。

type Writer interface {
    Write(p []byte) (n int, err error)
}

io.Writer インターフェースは引数で受け取ったバイト列を書き込みますが、このインターフェースは os.Fileなどで実装されています。Goのtypeシステムではどのインターフェースを使うか明示する必要がありません。既存のtypeのプロパティと一致するインターフェースを宣言することで、外部ライブラリの動作を変更することができます。

例を見ていきましょう。

以下のようなメッセージを送信する外部ライブラリがあります。

type Message struct {
     // ...
}

func (m *Message) Send(email, subject string, body []byte) error {
     // ...
     return nil
}

これをそのまま使うのではなくMessageを使うMessagerインターフェースを作成します。

type Messager interface {
    Send(email, subject string, body []byte) error
}

Alertメソッドでメッセージを送信することを考えます。Message typeを直接渡すのではなくMessager引数で受け取って、インターフェースのSendメソッドを呼び出すようにします。

func Alert(m Messager, problem []byte) error {
    return m.Send("example@example.com", "Critical Error", problem)
}

このようにMessageを抽象化したmessagerを使うことで簡単にモックを作成してテストすることができます。

具体的には以下のようになります。

package msg

import (
    "testing"
)

type MockMessage struct {
    email, subject  string
    body            []byte
}

func (m *MockMessage) Send(email, subject string, body []byte) error {
    m.email = email
    m.subject = subject
    m.body = body
    return nil
}

func TestAleart(t *testing.T) {
    msgr := new(MockMessage) // モックのメッセージを作成します
    body := []byte("Critical Error")

    Alert(msgr, body) // Aleartメソッドを実行します

    if msgr.subject != "Critical Error" {
        t.Errorf("Expected 'critical Error', Got '%s'", msgr.subject)
    }
}

Messagerインターフェースを実装するためにMockMessage typeを作成します。MockeMessageではMessagerと同じSend()が実装されています。このSend()はメーセージを実際に送信するのではなくデータをオブジェクトに保存しておくことでテストしやすくなります。

また、このようにインターフェースを使った抽象化をすることで、後にSend()の動作を変えなければいけなくなった時に簡単に変えられるようになります。

カナリアテスト

外部ライブラリを使っているとメジャーバージョンアップの時などにメソッドの引数が変わることがあります。

例えば、io.Writerを新しく実装していたとします。これをライブラリとして公開していて、他のコードがこれを使用しています。以下のようなコードです。

type MyWriter struct{
     // ...
}

func (m *MyWriter) Write([]byte) error {
     // どこかにデータを書き出す
     return nil
}

ぱっと見io.Writeを実装しているように見えますが、正しくはWrite(p []byte) (n int, err error)です。なのでio.Writeを実装できていません。

次に、type assertionを使ってコードを書いてみます。

func main() {
    m := map[string]interface{}{
        "w": &MyWriter(),
    }
}

func doSomething(m map[string]interface{}) {
    w := m["w"].(io.Writer) // runtime exceptionになる
}

このコードはコンパイルとは通りますが、runtimeでexceptionになります。

これを防ぐために以下のようなカナリアテストを追加します。(ちなみにカナリアテストは”canary in the coal mine”から来ているようです)

func TestWriter(t *testing.T) {
    var _ io.Writer = &MyWriter{} // コンパイラにtype assertionをやってもらう
}

このテストはもちろん失敗します。このようにtype assertionを使ってテストすることで、インターフェースを正しく実装できているか確認することができます。また、外部ライブラリのシグネチャの変更にも気づくことができます。

I went to builderscon tokyo 2017

I went to builderscon tokyo 2017 from August 3rd to 5th.

The eve of the conference

I heard some fascinating talks, but I was told not to tell anyone about them, so unfortunately I can’t write about them.

I think the organizers should keep this event next time.

Day 1

Recognizing Japanese idols with Deep Learning

@sugyan spoke about recognizing Japanese idols with Deep Learning.It helped me to learn about Deep Learning recognizes images.

He gave me some hints on image recognition. Thank you @sugyan.

Building high performance push notification server in Go

@cubicdaiya[https://twitter.com/cubicdaiya] spoke abount a push notification server.

Gaurun might be able to use my project. I’ll look up detailed information about it.

After party

I went to the after party, which was held after the end the day 1. Almost all of the dishes were delicious.

Day 2

Factory Class

Jesse Vincent told me the exciting episode that’s about developing keyboards from scratch.

He said it’s important to meet up with a supplier and keep a good relationship with them if you want to make a good product in China.

Finally

The name card the organizers made was useful for me, and I think the voting system (It uses a QR code) to choose best speakers was convenient for us.

There was only one thing I was dissatisfaction at the lunch session. I think that I and the other audience members were crammed into there, so I think the room was a little small.

But I had a totally enjoyable time at the conference, and I would love to go again!