jtwp470’s blog

日記とかプヨグヤミングとか

Goの勉強がてらにPythonで書いたCLIをGoで書き直した話

こんにちは。社会人2ヶ月が経過したたんごです。 社内研修も終わり部署配属されてはや2週間が過ぎました。本当に早いw

さてさて、部署配属されてからほとんどRubyばっかり書いていてPython触っていないんですが最近RustとGoに興味が出てきまして、せっかくなので少し前に書いたCLIGolangで書き直してGoを学ぼうという魂胆です。

gopher

前提条件

  • CLI として機能する。
  • 外部APIにGETでリクエストして返ってきたJSONレスポンスをパースして、必要な値を取り出す。

こう書くだけだと2つしかやること無いし楽じゃんと思いましたがそううまくは行きませんでした。

文法を学ぶ

まず、Goのデータ構造と文法を知らないのでそこを学ぶ必要がありました。 とりあえず公式にあるA Tour of Goをそのまま読み進めていきました。Pythonで書いていたときと所々異なる点があったのでそれだけメモしておきます。(とはいえ公式を読めばわかるんだけどね)

基本的には A Tour of Go をそのまま読み進めていくものです。ただ個人的に気になったことやメモを残しておこうと思い記事にしました。

配列

Pythonだと何も考えずに [ ] に値を放り込むだけですが、Golangでは配列の長さを1度決めたら変えることはできないし、型も決まっています。

定義の基本

var <変数名> [長さ]<型>

また、2通りあります。1つは var で型を確定させて定義するもの。また、2番めのように初期値をそのまま決め打ちで初期化することもできます。

var a [2]string
a[0] = "Hello,"
a[1] = "world"

primes := [6]int{2, 3, 5, 7, 11, 13}

スライス

配列は固定長のサイズであるのに対し、スライスは可変長サイズです。容量が足りなくなると自動的に領域を広げてくれます。 生成にはビルトイン関数の make を使います。

a := make([]int, 5)

また、スライスに値を追加したい場合は append という関数を用います。append 関数は第1引数が値を追加したいスライスを入れ、残りの引数は追加したい値を入れます。

package main

import "fmt"

func main() {
    var s []int

    s = append(s, 0)
    printSlice(s)

    s = append(s, 1, 2, 3, 4, 5)
    printSlice(s)
}

func printSlice(s []int) {
    fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}

実行結果は以下のようになる。

len=1 cap=1 [0]
len=6 cap=6 [0 1 2 3 4 5]

Pythonでは、[1, 2].append(10) のようにしますがGoはオブジェクト指向言語ではなく命令形言語なのでこうなります。

Range

RangeははPythonで言うところの enumerate と同じ挙動をしてくれます。雰囲気的にはスライスにアクセスする際にイテレータ操作ができるすぐれものです。

Pythonでこのような処理は

pow = [1, 2, 4, 8, 16, 32, 64, 128]
for i, v in enumerate(pow):
    print("2**{i}={v}".format(i=i, v=v))

Golangではこのように書けます。

var pow = []int{1, 2, 4, 8, 16, 32, 64, 128}
for i, v := range pow {
    fmt.Printf("2**%d=%d\n", i, v)

辞書

Pythonではdict型をGolangではMapといいます。用語の違いなので覚えるしかなさそうです。 スライス同様で make 関数でも定義できます。

Goではmapはミュータブルなものであるので、最初にmapを定義してしまえば値を挿入したり、キーによる参照や削除が可能になっています。

package main

import "fmt"

func main() {
    m := make(map[string]int) // stringがキー。値はint
    m["answer"] = 42
    fmt.Println("The value: ", m["answer"]) // 取得

    m["answer"] = 28
    fmt.Println("The value: ", m["answer"]) // 上書き

    delete(m, "answer")
    fmt.Println("The value: ", m["answer"]) // 削除

    v, ok := m["answer"]
    fmt.Println("The value: ", v, "Present?", ok) // 存在するか
}

削除した後に参照すると例外等は発生せず、初期値が変えるようになっています。また第2引数で存在可否が判定できます。

Exercise: Maps

A Tour of Goの練習問題として文字列sに含まれる単語を数え上げるプログラムを書きました。

package main

import (
    "strings"

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

func WordCount(s string) map[string]int {
    m := make(map[string]int)
    for _, key := range strings.Fields(s) {
        _, exist := m[key]
        if exist {
            m[key] += 1
        } else {
            m[key] = 1
        }
    }
    return m
}

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

ちなみに実行結果はこんな感じになります。

$ go run aaa.go
PASS
 f("I am learning Go!") =
  map[string]int{"I":1, "am":1, "learning":1, "Go!":1}
PASS
 f("The quick brown fox jumped over the lazy dog.") =
  map[string]int{"jumped":1, "over":1, "lazy":1, "dog.":1, "The":1, "quick":1, "brown":1, "fox":1, "the":1}
PASS
 f("I ate a donut. Then I ate another donut.") =
  map[string]int{"I":2, "ate":2, "a":1, "donut.":2, "Then":1, "another":1}
PASS
 f("A man a plan a canal panama.") =
  map[string]int{"man":1, "a":2, "plan":1, "canal":1, "panama.":1, "A":1}

関数

Golangの関数には引数に関数を与えることができるよという話でした。またGoの関数はクロージャらしいです。

例題としてフィボナッチ数列クロージャを使って書いてみました。

package main

import "fmt"

func fibonnacci() func() int {
    p, n := 0, 1
    return func() int {
        p, n = n, p+n
        return n
    }
}
func main() {
    f := fibonnacci()
    for i := 0; i < 10; i++ {
        fmt.Println(f())
    }
}

こんな感じ。

メソッド

Goにはクラスがないため、メソッドは存在しないはずです。けれども型にメソッドを定義できるという話みたいです。このときはfuncキーワードとメソッド名の間に引数リストを書きます。これらをポインターレシーバーといい、ポインターレシーバーがないと、自分の型に定義したメソッドで値を書き換えることができません。

例えばA Tour of Goのコードを拝借して自分なりの言葉でまとめます。

package main

import "fmt"

type Vertex struct {
    X, Y float64
}

func (v *Vertex) Scale(f float64) {
    v.X = v.X * f
    v.Y = v.Y * f
}

func main() {
    v := Vertex{3, 4}
    v.Scale(10)
    fmt.Println(v)
}

Scaleは型 Vertex のメソッドで、引数に10を与えると結果は次のようになります。

{30 40}

これに対し、もし変数レシーバーとした場合、つまり

- func (v *Vertex) Scale(f float64) {
+ func (v Vertex) Scale(f float64) {

のように書くと次のような結果が得られます。

{3 4}

つまり、変数レシーバーの場合は元のVertex変数のコピーを操作しているため、呼び出し元の値は変更されません。

interface

最初公式ドキュメントを読んだときは全く意味がわからなかったのですが、簡単に言うと自分で好きな型を決めることができるみたい。

後々JSONマッピングする際にこれを使いまくります。多分ですが、The empty interfaceから先は読んでおいたほうがいいと思います。

その他

今回は使いませんでしたが、Go Routine等もきちんと勉強しておくと並行処理をする際に役立つかもしれません。

さて、Go言語の大体の学習にPythonとの比較を踏まえてやっていたため、だいたい2時間程度を要しました。ここからは実際にCLI形式のプロジェクトをPythonからGoに書き直していきます。

CLI の設計

移植前のPythonのコードはこちらになります。

github.com

挙動としてはREADMEに書いてあるとおりですが、factordb という数字を投げつけると素因数分解の結果を返してくれるサイトがあります。(ちなみに毎度素因数分解を実行しているのではなく、DBというようにデータベースから既知の値を返してくれるだけです) 例えば、16なら 2 ^ 4 のように返してくれます。CLIでは次のように使います。

$ factordb 16
2 2 2 2

また、オプションに --json をつけるとAPIからの返り値を使いやすいJSONの形式に変形して出力してくれ、かつもう少し詳細を出してくれます。

$ factordb --json 16
{"id": "https://factordb.com/?id=2", "status": "FF", "factors": [2, 2, 2, 2]}

Golangで実際にCLIを作成するにあたり、

developers.eure.jp

のコードを参考に書きました。

最初に↑を参考にオプションを引き取って適当にCLIっぽい挙動をするコードを書きました。

package main

import (
    "fmt"
    "os"

    "github.com/codegangsta/cli"
)

func main() {
    app := cli.NewApp()
    app.Name = "factordb"
    app.Usage = "The CLI for factordb.com"
    app.Version = "0.0.1"

    // Global option
    app.Flags = []cli.Flag{
        // --json
        cli.BoolFlag{
            Name:  "json",
            Usage: "Return response formated JSON",
        },
    }

    app.Action = callAction
    app.Run(os.Args)
}

func callAction(c *cli.Context) error {
    var isJson = c.GlobalBool("json")
    if isJson {
        fmt.Println("Response json type")
    }

    var paramFirst = ""
    if len(c.Args()) > 0 {
        paramFirst = c.Args().First()
    }

    fmt.Printf("Hi, I am receiving the number %s\n", paramFirst)
    return nil
}

https://github.com/ryosan-470/factordb-go/blob/acd2fbb6c5e9bf7f84e7c2bd7e90001c3337b012/cli.go

↑のコードはもし、--json がついていると標準出力に Response json type と、あとは引数をHi, I am receiving the number 16 みたいな形で出力してくれるプログラムです。このままどの環境でも動かせると思います。

JSONの読み込みと解析

次に、JSONを読み込んでGolangの構造体に置き換えます。Golangでは

qiita.com

などにあるように、json.Unmarshal などで解析して構造体に置き換えることでコード内でよしなに扱うことができるみたいです。しかしFactorDBのAPIにアクセスしてみればわかりますが返ってくるJSONはお世辞にもキレイなものとはいえません。

$ http http://factordb.com/api\?query\=10
HTTP/1.1 200 OK
Connection: Keep-Alive
Content-Encoding: gzip
Content-Length: 67
Content-Location: api.php
Content-Type: application/json
Date: Wed, 07 Jun 2017 09:23:37 GMT
Keep-Alive: timeout=5, max=100
Server: Apache
TCN: choice
Vary: negotiate,Accept-Encoding

{
    "factors": [
        [
            "2",
            1
        ],
        [
            "5",
            1
        ]
    ],
    "id": "10",
    "status": "FF"
}

この factors をどう表せばよいのかわからずつまりました。そこで色々調べてみると次のようなブログに当たりました。

www.kaoriya.net

書いてある通りで申し訳ないのですが、何も考えずに interface に投げ込めばいいみたいです。あとは前学んだように自分でよしなに型を変えていきます。今回は2つの構造体を用意しました。

type FactorDB struct {
    Status  string
    Id      string
    Factors []Factor
}

type Factor struct {
    Number int
    Power  int
}

FactorDB 構造体は返ってくるレスポンスを表しています。StatusIdstring で問題ないでしょう。Factors だけはArrayで返ってくるので自分でさらに中身を定義し直します。 公式APIドキュメントなど存在しないので自分で勝手に解釈していますが、factors は文字列の方を残りの数でべき乗するというような形になっていると推定しましたw なのでこのような構造体で扱えると思います。

最終的にJSON文字列を引数にとり、FactorDB 構造体を返すGoのプログラムは次のようになりました。

func ConvertToFactorDB(b []byte) FactorDB {
    var base interface{}
    err := json.Unmarshal(b, &base)
    if err != nil {
        log.Fatal("Cannot parse your input")
    }
    s := base.(map[string]interface{})

    var factor FactorDB
    factor.Status = s["status"].(string)
    factor.Id = s["id"].(string)

    factors := s["factors"].([]interface{})

    for _, f := range factors {
        tmp := f.([]interface{})
        number, _ := strconv.Atoi(tmp[0].(string))
        power := int(tmp[1].(float64))
        factor.Factors = append(factor.Factors, Factor{number, power})
    }

    return factor
}

https://github.com/ryosan-470/factordb-go/blob/af37df2ea6527c3e02aae1140dfaeee69844a28e/handler/response_handler.go

(こんな感じで簡単に書いてありますがだいたいここまでに3時間を要しましたw)

また、さりげなくですが、packageを分けました。まだGoのパッケージの切り方があまり良くわかっていませんが How to Write Go Code - The Go Programming LanguageYour first library という部分を参考に切り分けました。 書いてある通りに $GOPATH/src/github/ryosan-470/factordb-go のような場所にファイルを置き、go build で何も文句が言われなければとりあえず OK というスタンスで行きますw

テストコードを書く

最近、コードを書くときにテストを追加していくというのが私の中で流行っているので、テストも書きましょう。 同様にHow to Write Go Code - Testingの章を見て、書きました。 Goには assert のような関数はなく、比較演算子 == も基本型以外では動作しないようでしたので、自分で比較するような関数を別途テストに組み入れます。

APIに実際に接続

次に実際にAPIに接続し、リクエストを送って、先程↑で作った構造体に直してやるプログラムを書きます。 なお、先程の FactorDB 構造体は汎用的すぎたため FactorDbResponse という構造体になっています。

https://github.com/ryosan-470/factordb-go/commit/c928408522e91d8dc22888c4f9a5bd82181f37b9

さて、まず構造体の定義をしました。

type FactorDB struct {
    Number int
    Result handler.FactorDbResponse
}

こうすることでmain関数内で先に var factordb FactorDB として初期化し、CLIで第1引数として数字を受け取ったあとに factordb.Number = 16 みたいな形で、値を保持できます。

Pythonで言えばこんな感じで使いたいという気持ちだけでも伝わってほしいですw

class FactorDB():
    def __init__(self, n):
        self.n = n
        self.result = None

あとはGoから実際に接続して値を取得しましょう。

func (f *FactorDB) Connect() error {
    values := url.Values{}
    values.Add("query", fmt.Sprintf("%d", f.Number))
    resp, err := http.Get(fmt.Sprintf("%s?%s", ENDPOINT, values.Encode()))
    if err != nil {
        return errors.New("cannot connect" + ENDPOINT)
    }

    defer resp.Body.Close()
    b, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        return errors.New("empty body")
    }

    response := handler.ConvertToFactorDB(b)
    f.Result = response
    return nil
}

出ました。これが ポインターレシーバーですねw こうするオブジェクト指向プログラミングで言うところのメソッドのように扱えます! あとはmain 関数等で以下のような感じで呼び出せば OK です。

   if err := factordb.Connect(); err != nil {
        fmt.Println(err)
    }

また、Pythonのメソッドでいうところの

  • ID を取得する: get_id
  • Status : get_status
  • get_factor_list

に相当するポインターレシーバー関数を定義します。ちなみに最後の get_factor_listAPIから返ってくるふざけた形式をいい感じに返してくれるメソッドで以下のようなコードで書かれています。

    def get_factor_list(self):
        """
        get_factors: [['2', 3], ['3', 2]]
        Returns: [2, 2, 2, 3, 3]
        """
        factors = self.get_factor_from_api()
        if not factors:
            return []
        ml = [[int(x)] * y for x, y in factors]
        return [y for x in ml for y in x]

リスト内包表記をバリバリ使っているので読みづらいですが動きとしては書いてあるとおりです。

Goでは次のように書きました。

func (f *FactorDB) GetFactorList() ([]int, error) {
    if f.Empty() {
        return []int{}, errors.New("Empty Result")
    }
    var ret []int
    for _, f := range f.Result.Factors {
        for i := 0; i < f.Power; i++ {
            ret = append(ret, f.Number)
        }
    }
    return ret, nil
}

うーんめっちゃ愚直w

テストコードとこれらのメソッドのDiff差分は以下です。 https://github.com/ryosan-470/factordb-go/commit/1657c0397571762061f1c7112c512bd11498c99d

CLIの完成

さて、これら3つを組み合わせてCLIを作成します。動作としては単純で第1引数の値を受取り、FactorDB 構造体の Number に代入し、FactorDB.Connect を呼び、あとはよしなに各関数で値を整形して標準出力に表示するというものです。

強いて言えば JSON 形式で返すときに物凄く無理やりArrayからJSON文字列を書き出しているところが少し気持ち悪いです。

func callAction(c *cli.Context) error {
    var number = ""
    if len(c.Args()) > 0 {
        number = c.Args().First()
    }

    n, err := strconv.Atoi(number)
    if err != nil {
        log.Fatal("Your input is not number")
    }

    f := factordb.FactorDB{Number: n}
    if err := f.Connect(); err != nil {
        log.Fatal("Connection Error")
    }

    factors, _ := f.GetFactorList()

    var output string

    var isJson = c.GlobalBool("json")
    if isJson {
        id, _ := f.GetId()
        status, _ := f.GetStatus()
        var fs []string
        for _, f := range factors {
            fs = append(fs, fmt.Sprintf("%d", f))
        }

        facs := fmt.Sprintf("[%s]", strings.Join(fs, ", "))
        output = fmt.Sprintf("{\"id\": \"https://factordb.com/?id=%s\", \"status\": \"%s\", \"factors\": %v}", id, status, facs)
    } else {
        output = strings.Trim(fmt.Sprintf("%v", factors), "[]")
    }

    fmt.Printf("%s\n", output)
    return nil
}

とりあえず、動くっぽいコードは以下のコミットハッシュでできました。

https://github.com/ryosan-470/factordb-go/commit/796c3a6828209561d17bda8813721355b74dfc5e

次に Ruby で言うところの Gemfile 的なもので外部パッケージのバージョンを管理します。Goではglideというものを使うとできるそうです。

https://github.com/ryosan-470/factordb-go/commit/ffeb93d6f289e3a35082c04aaaf574f762a0c1c6

CircleCI 2.0を使ってテストする

さて、ここまで来たら今度はCIを回しましょう。有名なCIサービスにはTravisやCircleCIがありますが最近はCircleCI 2.0の高速さに驚かされているため、CircleCIを採用しました。CircleCI 2.0とはなんぞやという人は私が過去に書いた記事を読んでみてください。 ビルドにVMを利用せずDockerコンテナがそのまま扱えるため、ビルド開始も高速に行われ非常に便利です!

jtwp470.hatenablog.jp

config.yml は以下のとおりです。

version: 2
jobs:
  build:
    docker:
      - image: circleci/golang:1.8  # CircleCI Go images available at: https://hub.docker.com/r/circleci/golang/

    working_directory: /go/src/github.com/ryosan-470/factordb-go


    steps:
      - checkout
      - run: curl https://glide.sh/get | sh
      - run: make deps
      - run: make
      - run:
          name: Running test code
          command: make test

さりげなくビルドはすべて Makefile に書いてありますが make test の挙動としては

go test -v $(go list /... | grep -v /vendor/)

となっています。これにより、Glideで入ってくる外部ライブラリをテストせず自分のコードのみをテストすることが可能になっています。

テストカバレッジの計測

今回はテストカバレッジの計測とビジュアル化のために、Codecov を使いました。

codecov.io

Goでcoverageを測定するには難癖があるみたいでいろいろ調べてでてきたものを使用しています。

https://github.com/ryosan-470/factordb-go/commit/25e3dc1277678e883560191b7944285eab58da67

記事執筆の段階でカバレッジ率が59%でGo lintなどの測定もはいっていないのでそれらも入れていきたいなと思っているところです。

Windowsで動作を保証するためにAppveyorを使う

ここまで来たからにはWindowsでビルドし動くことも確かめたいなと思いました。WindowsでCIを行えるサービスのうち有名なものにAppveyorがあります。はじめて使ってみましたがそこまで難しくなかったのでその情報も共有しておきます。

www.appveyor.com

特にいじらなければ、Windows Server 2012上でテストが行われるようです。

appveyor.yml

version: 1.0.{build}
platform: x64

clone_folder: c:\gopath\src\github.com\ryosan-470\factordb-go

environment:
  GOPATH: c:\gopath

install:
  - set PATH=C:\msys64\mingw64\bin;%PATH%
  - echo %PATH%
  - echo %GOPATH%
  - go version
  - go env
  - go get -v -t -d ./...

build_script:
  - go build -o factordb.exe
  - go test -v -race ./...

artifacts:
  - path: factordb.exe
    name: binary

そんなに難しいことはしていないですね。ただ、Makefileが使えないのでその部分だけ手書きしたという感じでしょうか。一応テストも行っているのでもしWindowsではビルドできないみたいな状況が起きてもテストで落ちてくれると思います。

WindowsのCIの結果: Appveyor

Python版のCLIでもWindowsで動くことには動きますが、Windowsには元々Pythonはいっていませんし、文字コードの問題で動かなかったりといろいろあるのでバイナリが各環境で吐けるGoはいいですね!

ここまでで役に立った資料、手法など

基本的に1次情報はGoの公式ドキュメントで、あそこを読めば大体のことはわかります。ただ、如何せん初心者なので意味を汲み取れないなどのときに適当に検索していると大抵同じようなものにぶつかった人がいるのでそこから情報を得ました。

fmt モジュールが便利

fmtモジュールでは、値をただ出力するだけでなくその値の型や値の中身を表示することができます。

fmt - The Go Programming Language

一番上に書いてあるとおり、

  • %v: デフォルトフォーマットで値を表示する。構造体を表示するときは %+v を使う
  • %#v: 値のGo言語構文表現
  • %T: 値の型のGo言語構文表現

これらを使っていい感じにデバッグしていました。型エラーが大抵多い気がするのでこれでprintfデバッグして確認しましょう。

gore を使うと REPL が動かせる

github.com

毎度毎度ちょっとしたコマンドの確認にコンパイルして実行は面倒くさいのでREPLがあるとはかどります。

疑問

  • Packagingの切り方がよくわからない
  • Go routineを使う場所がなかったのでその辺を勉強したい

まとめ

GoはPythonから移行してもそこまで難しくなく簡単に書くことができました。また、Windows用のバイナリをはけるなどLinuxMacで日々開発している人にとって他のディストリビューション用にも開発できるのは非常にありがたいです。

もし、興味を持ってくれたら、このリポジトリを Star してね♡

github.com

今度はSlackのBOTをGoで書き直してみようと思っています。

お一人様マストドンを運営してみて2週間が過ぎ去った

こんにちは。約2週間前のマストドンブームに乗っかりAWSに建ててから大体2週間が経過しました。

jtwp470.hatenablog.jp

上の記事は帰りの電車でツイッターを見ていたらマストドンが流行っているらしいということを知ったのでそのまま夕飯を食べつつ1時間程度で建てたものを記事にしたものですが、2週間経って実際おひとりさまマストドンってどうなのよというあなたの質問に答えるよwというようなものです。

現状

instances.mastodon.xyz のリストによると現状ではこんな感じです。

f:id:jtwp470:20170430223600p:plain

Uptimeは99 % 超えなのでダウンタイムは総計で140分程度です。(結構多いなw)

弊ブログの前の記事からお試し登録をしてくださる方が幾人かいたので登録者数は15人になっていますが実質ローカルタイムラインは自分のトォートのみが占めています。

f:id:jtwp470:20170430223843p:plain

また、弊サーバーの https://mastodon.jtwp470.net/about/more のページをいい感じに書き直してみました。

f:id:jtwp470:20170430224013p:plain

書いてある通りですがサーバースペックは以下のようになります。

  • AWS EC2 t2.micro @ 東京リージョン
  • vcpu: 1
  • mem: 1 GB
  • ELB: 30 GB

Datadogによる監視

AWSインスタンスを建ててからすぐにログ取りをするためにDatadogをインストールしました。なんでDatadogを使っているかというと個人の好みなので、日本人ならはてなが提供しているmackerelもおすすめです。

CPU使用率

f:id:jtwp470:20170430225259p:plain

みてわかるように極めて平和的な使用量です。アクセス数が少ないのでこんな感じかなと思っています。 たまにCPU使用率が伸びている場所がありますが大抵その部分はマスタドンの新しいバージョンアップによる際のインスタンス内での docker-compose build 等でCPUを食っている部分です。

メモリ使用率

f:id:jtwp470:20170430225446p:plain

メモリ使用率に関しては4/17の午後2時ごろから一度大きく下がっている部分があります。

f:id:jtwp470:20170430230315p:plain

もともと初めてインスタンスを建ててマストドンを入れておくとだいたいメモリ使用率が80 % から 90 % 程度になっており、この状態で新しいリリースのための docker-compose build などをするとメモリが爆発して死んでしまう現象が結構起きました。そこでこの時間からスワップを作成して適用してみたため、スワップ適用後はかなり快適なメモリ使用量になりました。

スワップ

スワップの作成は次のコマンドでできます。

sudo fallocate -l 4G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab

これでディスクを4GB分犠牲にしてスワップメモリが生成されます。

f:id:jtwp470:20170430230439p:plain

スワップは大体400 - 500 MB分程度利用されているみたいです。

ディスク使用量

f:id:jtwp470:20170430230524p:plain

ディスク使用量に関しては基本的に右肩上がりで伸びていますが最近は止まってきました。 はじめのでかいディスク使用率の上昇に関しては何度も述べているとおり上でスワップを作ったためです。 向上的に上昇しているのは自分が画像をアップロードしているというよりもpawooのインスタンスのアカウントをフォローしていると連合タイムラインで流れるようになりますが流れてきた画像すべてを保存するようになっているらしく右肩上がりで上がっていく状況になっているようです。しかし最近は私のTLで流れなくなってきたためかディスク使用量も圧迫しないようになりました。

またディスクが足りなくなってきたら docker-compose build でビルドした古いイメージを削除してしまえば少しではありますがディスク使用量が空いてくれます。

その他補足情報

ツイッターでのツイートをマストドンに流す

まだマストドンよりもツイッターのほうが常駐している確率が高いのでツイートしたら自分のマストドンにも流すということをしています。そのためツイートがほとんどそのままマストドンに流れています。

やり方は Qiitaの記事 Twitterの投稿をIFTTTでMastodonへ転送する をみてやってください。ただ手法としてはBashコマンドを打てないとちょっとつらいところがあるのでもしかすると以下のようなサービスを使ったほうが簡単かもしれません。

sync.twi2mstdn.space

とりあえず、ツイッターのほうがまだ活発だよ〜という人はこういう感じで流すとみた感じマストドンにもいるみたいな感じになってよいかなぁと思います。

Android, iPhone 用クライアントアプリ

使ってみてのおすすめですがクライアントアプリでは先日pixivがリリースしたpawooです。 別にpawoo.net にアカウントを持っている必要はなく自前インスタンスにもログインできて使えるので重宝しています。ありがとうpixiv!

play.google.com

マストドンアプリ「Pawoo」

マストドンアプリ「Pawoo」

  • pixiv Inc.
  • Social Networking
  • Free

pixivさんに感謝の気持ちを込めて僕がインターンに行ったときの過去記事を上げておくので学生さんぜひインターンに行ってあげてください。(誰目線だよw)

jtwp470.hatenablog.jp

これからやってみたいこと

AWSの無料枠があるうちにRedisとDBをそれぞれDockerコンテナから剥がしてしまい、それぞれAWS RDSとElasticCacheに載せ替えてみたいなぁと思っています。

まとめ

お一人様マストドンなる状況に陥る時代になるとはびっくりだなぁしていますが、お一人様だとpawooやmstdn.jp のような高速TLをお目にかかることもなく極めて平和に進んでいきます。時たまpawooなどにログインしてTLを眺めると早すぎて目が回ってしまいそうです。 上に記した通り案外お一人様ならあまりメモリやCPUを必要とせずに頑張れます。みなさまもこれを機におひとりさまマストドンいかがですか。

TypeScriptとWebpackとReactを使って簡単なTODOリストを作ってみた

最近フロントエンドの勉強をはじめました。フロントエンド界隈(JavaScript界隈)は変化が激しいようで今最新だーと思っていた技術も数ヶ月後には古いみたいなのがざらにあるようですが、最近私が興味を持っているTypeScriptとFacebookが作っているReact.jsをもとにWebpackを組み合わせて使ってみたというものです。

環境構築

nodeやnpmは既に入っているという前提にしておく。

$ npm init

で出てくる質問に片っ端から答える。yesをするとpackage.jsonが生み出される。

次にTypeScriptをインストールし、トランスコンパイル設定ファイルを生成する。

$ npm install typescript
$ (npm bin)/tsc --init

すると配下にtsconfig.jsonが生成されているはず。 また、tsconfig.jsonはとりあえず以下のように書き直しておく。

{
  "compilerOptions": {
    "module": "commonjs",
    "target": "es5",
    "jsx": "react",
    "noImplicitAny": false,
    "sourceMap": false
  }
}

次にWebpackをインストールする。WebpackはJavaScriptCSSなどのモジュール依存関係を解決しながら変換してくれるものらしい。

$ npm install webpack ts-loader -D

次にWebpackの設定をJavaScriptでかく。webpack.config.jsという名前で保存しておく。

const path = require('path');

module.exports = {
    entry: {
        app: "./index.ts",
    },
    output: {
        path: path.resolve(__dirname, "./dist"),
        filename: "[name].bundle.js",
    },
    resolve: {
        extensions: ["*", ".ts", ".tsx", ".js", ".jsx"],
    },
    module: {
        rules: [
            {
                test: /.tsx?$/,
                use: [{ loader: "ts-loader" }],
            },
        ],
    },
};

次に, Reactをインストールする。

$ npm install react react-dom @types/react @types/react-dom -S

後ろの @types から始まる部分のものは、すべてTypeScriptを使う際に必要でこれらは型定義となっている。

Hello, World

ここまで来たら、Hello Worldと出力させる部分だけをやってみる。

まず、index.htmlを以下のように作成する。

<!DOCTYPE html>
<html>
    <head>
        <title>Tiny TODO app</title>
    </head>
    <body>
        <div id="app"></div>
        <script src="./dist/app.bundle.js"></script>
    </body>
</html>

次に、index.tsを作成する。

import * as React from "react";
import * as ReactDom from "react-dom";
import TodoApp from "./todoapp";

ReactDom.render(
    React.createElement(TodoApp),
    document.getElementById("app")
);

最後にTODOリストを定義するファイルとして、todoapp.tsxを作成する。

import * as React from "react";

export default class TodoApp extends React.Component<any, any> {
    render() {
        return (
            <div>Hello, World</div>
        );
    }
}

これで、webpackを使ってコンパイルしてみる。

$ $(npm bin)/webpack -p

こうしてから普通にindex.htmlへブラウザからアクセスしてみる。

f:id:jtwp470:20170430214006p:plain

こんな感じ。

TODOリストを作成してみる

まず入力フォームを作っておく。入力フォームは以下のような感じ。

import * as React from "react";

export default class TodoApp extends React.Component<any, any> {
    render() {
        return (
            <div>
                <h3>TODO</h3>
                <TodoList items={this.state.items} />
                <form onSubmit={this.handleSubmit}>
                    <input onChange={this.handleChange} value={this.state.text} />
                    <button>{'Add #' + (this.state.items.length + 1)}</button>
                </form>
            </div>  
        );
    }
}

一応できたけど、またstateの値やhandleSubmit, handleChangeと言った部分が実装されていない。

まず、コンストラクタを実装する。

    constructor(props: any) {
        super(props);
        this.handleChange = this.handleChange.bind(this);
        this.handleSubmit = this.handleSubmit.bind(this);
        this.state = {items: [], text: ''};
    }

こんな感じ。次にhandleSubmitとhandleChangeをそれぞれ書く。

handleChange(e: any) {
        this.setState({text: e.target.value});
    }
    handleSubmit(e: any) {
        e.preventDefault();
        let newItem = {
            text: this.state.text,
            id: Date.now()
        };
        this.setState((prevState) => ({
            items: prevState.items.concat(newItem),
            text: ''
        }));
    }

最後に今まで入力した値をきちんと出力するためのクラスとして TodoList を定義する。

class TodoList extends React.Component<any, any> {
    render() {
        return (
            <ul>
                {this.props.items.map(item => (
                    <li key={item.id}>{item.text}</li>
                ))}
            </ul>
        );
    }
}

これでおしまい。実際に実行してみる。Webpackでコンパイルした後、ブラウザで普通にindex.htmlを開いてみるとこんな感じになっているはず。

f:id:jtwp470:20170430221833p:plain

ちなみに書いたコードとかはこれ。

github.com

まとめ

環境構築あたりがものすごくしんどいという印象。フロントエンドが正しく動いているというようなテストを書くにはどうするのかまだよくわからないので調べていきたいと思っております。はい。

まだTypeScript自体も写経と言うような感じで使いこなせているわけではないので精進していきたいところです。

GitHubのプロジェクトをCircleCI 2.0でテストできるように移行してみる

前回書いたマストドンの記事が地味にバズっていてびっくりしているたんごです。

今日はマストドンとは全く関係ないですが、自分のGitHubリポジトリでCircleCIでテストをしているプロジェクトをCircleCI 2.0でテストするようにしたので、簡単にそのメモ書きを残しておきます。

CircleCI 2.0ってなんぞ

circleci.com

を読め、という気持ちなんですが簡単に言うと以下のような違いがあります。

  • 基本的にDockerコンテナがそのままCircleCIで動かせるよ。
  • ローカルでビルドできるようになるよ。
  • キャッシュがめっちゃ効かせられるよ

はい。意味がわかりませんね。一番でかいのはやっとNativeのDockerをサポートしたということではないでしょうか。今までだと、例えば docker -v でマウントしたり、docker exec でコンテナ内のコマンドが実行できなかった気がしますが、2.0を使えばそれも簡単にできます。

どうやって始めるの

CircleCI 2.0はまだベータ版なので使いたい人は自分で使いたいよ〜という申請をする必要があります。先程のリンクの下の方にあるAccess CircleCI 2.0’s Beta Nowという部分に行って入力すると、数分後か数日後にメールが届き使えるようになります。

f:id:jtwp470:20170416232320p:plain

とはいっても、すぐに自分のリポジトリがすべて2.0でビルドされるわけではありません。

まず、今までだとプロジェクトルートに circle.yml というファイルを作っていました。2.0を適用させるには、.circleci/config.yml というディレクトリに設定を書く必要があります。

今回私が移行したプロジェクトはこちらです。

github.com

基本的にはPythonのみのコードで構成されておりテストも簡単にユニットテストを動かすだけでした。

machine:
  python:
    version: 3.6.0

dependencies:
  cache_directories:
    - "~/.cache/pip"

test:
  override:
    - python setup.py test

さてこれを2.0用のYAMLファイルに書き直していきます。 ドキュメントとしては以下の2つを読めば事足ります。

circleci.com

circleci.com

はじめにDockerのイメージを指定します。今回は公式イメージから最新のイメージを利用することにしました。

ここまでの設定を書くと次のようになります。

jobs:
  build:
    docker:
      - image: python:3

次にテストの前の依存関係のインストールPythonなら requrements.txt の読み込み、RubyならGemfileからのインストールなどをすると思いますが、そのステップも各必要があるみたいです。

version: 2
jobs:
  build:
    docker:
      - image: python:3

    steps:
      - checkout
      - run: apt-get update && apt-get install -y libgmp3-dev
      - run:
          name: Install dependencies
          command: pip install -r requirements.txt

stepsのはじめの行である checkout によりGitHubからコードを取得してくることができます。

また、runはnameとcommandというキーを持つことができ、これらはWeb UI上で次のように表示されるようになります。

f:id:jtwp470:20170416233407p:plain

お好みで書けばよいでしょう。

最後にテストの部分を追加します。

version: 2
jobs:
  build:
    docker:
      - image: python:3

    steps:
      - checkout
      - run: apt-get update && apt-get install -y libgmp3-dev
      - run:
          name: Install dependencies
          command: pip install -r requirements.txt
      - run:
          name: Run tests
          command: python setup.py test

ローカルでCircleCIのビルドをテストしてみる

さて、今までのCircleCIであればこのあと実際にこれが正しく動くかどうかに関してはコミットしプッシュしてみないとわからないという状況でした。なので初歩的にはYAMLの構文が間違っていたとか、設定に誤りがあった場合その修正コミットを作成してプッシュするという屈辱的な状況に追い込まれていました。

そこでCircleCIコマンドの出番です。

circleci.com

インストールは簡単で1行で済みます。

curl -o /usr/local/bin/circleci https://circle-downloads.s3.amazonaws.com/releases/build_agent_wrapper/circleci && chmod +x /usr/local/bin/circleci

ちなみにDockerが必要なのでよしなにインストールしておいてください。

あとはおもむろにプロジェクトルートで

circleci config validate

  config file is valid

でコンフィグの検証も行え、 circleci build で一貫したテストを行うことができます。

circleci build

====>> Spin up Environment
Build-agent version 0.0.3052-5be8345 (2017-04-15T11:24:20+0000)
Starting container python:3
  using image python@sha256:0a979dbff79f466aad8d92a89980ebe19cbd481b135d8cc9c0d843d8cf802ef9

Using build environment variables:
  CI=true
  CIRCLECI=true
  CIRCLE_BRANCH=master
  CIRCLE_BUILD_NUM=
  CIRCLE_BUILD_TOKEN=**REDACTED**
  CIRCLE_JOB=build
  CIRCLE_NODE_INDEX=0
  CIRCLE_NODE_TOTAL=1
  CIRCLE_REPOSITORY_URL=git@github.com:ryosan-470/cocoa.git
  CIRCLE_SHA1=f8cdb852ca92b53ae61768852ad23a9871e3ea8b

====>> Checkout code
Warning: skipping this step: running locally
====>> apt-get update && apt-get install -y libgmp3-dev
  #!/bin/bash -eo pipefail
apt-get update && apt-get install -y libgmp3-dev
Get:1 http://security.debian.org jessie/updates InRelease [63.1 kB]
...

...
writing manifest file 'cocoa.egg-info/SOURCES.txt'
running build_ext
============================= test session starts ==============================
platform linux -- Python 3.6.1, pytest-3.0.6, py-1.4.32, pluggy-0.4.0 -- /usr/local/bin/python
cachedir: .cache
rootdir: /home/ubuntu/cocoa, inifile: setup.cfg
collected 5 items

tests/test_factorize.py::TestFactorizeTestCase::test_fermat_success PASSED
tests/test_factorize.py::TestFactorizeTestCase::test_fermat_timeout PASSED
tests/test_factorize.py::TestFactorizeTestCase::test_pollard_rho PASSED
tests/test_factorize.py::TestFactorizeTestCase::test_small_prime PASSED
tests/test_factorize.py::TestFactorizeTestCase::test_trial_division PASSED

=========================== 5 passed in 3.73 seconds ===========================
Success!

こんな感じです。あとはコミットしてプッシュすればおしまいです。

あとはWeb UIにアクセスしてみると表示が2.0 になっています。 f:id:jtwp470:20170416234405p:plain

参考

engineer.crowdworks.jp

masutaka.net