HHKBのキーキャップをピンクに交換した

インスタの#HHKBタグをフォローしてチェックしていると、海外でキーキャップを交換している方が多く、前々から興味がありました。

Massdrop で見つけて購入を検討していたのですがいつの間にか終わってしまい、 結局AliExpressで購入しました。値段は$32.90 + 送料
海外から配送されるので到着まで10日ほど。

このようにキーキャップとキーキャップを引き抜く工具がついてきました。
(ついていないと思って工具も買っちゃっていた...)

f:id:inari111:20181008221430j:plain

まずは、キーキャップを外します。
半年近く掃除していなかったので、ついでに掃除もしました。

f:id:inari111:20181008221450j:plain

最後にキーキャップを取り付けて完了です。

f:id:inari111:20181008221505j:plain

思ったよりも派手な感じもしますが1週間も使えば慣れました。
ErgoDoxも気になり始めたしキーボードは沼だなー

favclip/testeratorを使ってGAE/Goのテストを高速化する

testeratorはGAE/Goのテストを高速化するライブラリです。
このライブラリを使ってテストの高速化ができたので紹介したいと思います。

contextを必要とするテストでは aetest.NewContext()aetest.NewInstance() を使うかと思いますが、これを使うと goapp test実行時にローカルサーバーが関数ごとに起動し、遅くなってしまいます。

func TestHoge(t *testing.T) {
    ctx, done, err := aetest.NewContext() // ローカルサーバー起動
    if err != nil {
        t.Fatal(err)
    }
    defer done()

    // 略
}

func TestFuga(t *testing.T) {
    inst, err := aetest.NewInstance(&aetest.Options{StronglyConsistentDatastore: true}) // ローカルサーバー起動
    if err != nil {
        return nil, nil, err
    }
    req, err := inst.NewRequest("GET", "/", nil)
    if err != nil {
        inst.Close()
        return nil, nil, err
    }
    ctx := appengine.NewContext(req)
    defer inst.Close()

    // 略
}

テストの数が多くなってくると、結構ストレスが溜まります。。。

ここで使うのが testeratorです github.com

testeratorはテスト間でインスタンスを使い回すことができるため、テストが高速化できるという仕組みです。
では、実際に使っていきます。

import (
    "fmt"
    "os"
    "testing"

    "github.com/favclip/testerator"
    _ "github.com/favclip/testerator/datastore"
    _ "github.com/favclip/testerator/memcache"
    _ "github.com/favclip/testerator/search"
)

func TestMain(m *testing.M) {
    if _, _, err := testerator.SpinUp(); err != nil {
        fmt.Printf("testerator.SpinUp error: %v", err)
        os.Exit(1)
    }

    status := m.Run()

    if err := testerator.SpinDown(); err != nil {
        fmt.Printf("testerator.SpinDown error: %v", err)
        os.Exit(1)
    }

    os.Exit(status)
}

func TestHoge(t *testing.T) {
    instance, ctx, err:= testerator.SpinUp()
    if err != nil {
        t.Fatal(err)
    }
    defer testerator.SpinDown()

    // 略
}

func TestFuga(t *testing.T) {
    instance, ctx, err := testerator.SpinUp()
    if err != nil {
        t.Fatal(err)
    }
    defer testerator.SpinDown()

    // 略
}

これだけです。 TestMainm.Run で各テストを実行していきます。
僕はTestMainを書くのを忘れていて、あれ...全然早くならないぞ...と少し悩んだので忘れないようにしましょう。

If you want to clean up Datastore or Search API or Memcache, You should import above packages.

とGoDocに書いてあるように、必要に応じてimportしておきましょう。

contextやinstanceを必要とする全てのテストを書き換えた結果、改善前の時間より 約30% 短縮することができました

まとめ

  • aetest.NewContext()aetest.NewInstance() はテスト関数ごとにローカルサーバーが起動するので遅い
  • testeratorでインスタンスを使い回すことでテストの時間を30%短縮できた

FirebaseUIでTwitterログイン機能を実装する

FirebaseUIはFirebase Authentication SDK上に構築されるライブラリで、アプリで使用するログイン機能とUIを提供してくれるものです。
メールアドレス、FacebookTwitterGitHub、電話番号といった複数のプロバイダに対応しています。
今回はTwitterでログインできるようにします。

Twitter Appsの作成

https://apps.twitter.com/app/new から新しいAppsを作成します。
Callback URLは後で入力します。

f:id:inari111:20180722221015p:plain

Firebase Authenticationの設定を変更する

Authentication -> ログイン方法 からログインプロバイダの設定を変更します。
今回はTwitterを有効にしました。
APIキーとAPIシークレットは ↑で作成したAppsのものを使いましょう。

f:id:inari111:20180722221353p:plain

コールバックURLをTwitter Appに追加

↑でTwitterを有効にするときに表示されたコールバックURLをTwitter Appに追加します。

コード

Firebaseのドキュメントのコードほぼそのままで動きます。 (僕はconfig間違えてて無駄にハマった。。。) configの値は各自の環境に合わせてください。

<script src="https://www.gstatic.com/firebasejs/5.0/firebase-app.js"></script>
<script src="https://www.gstatic.com/firebasejs/5.0/firebase-auth.js"></script>

<script src='https://cdn.firebase.com/js/client/2.2.1/firebase.js'></script>
<script src="https://cdn.firebase.com/libs/firebaseui/2.5.1/firebaseui.js"></script>
<link type="text/css" rel="stylesheet" href="https://cdn.firebase.com/libs/firebaseui/2.5.1/firebaseui.css" />

<script>
    var config = {
        apiKey: "<API_KEY>",
        authDomain: "<PROJECT_ID>.firebaseapp.com",
        databaseURL: "https://<DATABASE_NAME>.firebaseio.com",
        storageBucket: "<BUCKET>.appspot.com",
        messagingSenderId: "<SENDER_ID>",
    };
    firebase.initializeApp(config);

    var ui = new firebaseui.auth.AuthUI(firebase.auth());

    var uiConfig = {
        callbacks: {
            signInSuccessWithAuthResult: function(authResult, redirectUrl) {
                // User successfully signed in.
                // Return type determines whether we continue the redirect automatically
                // or whether we leave that to developer to handle.
                return true;
            },
            uiShown: function() {
                // The widget is rendered.
                // Hide the loader.
                document.getElementById('loader').style.display = 'none';
            }
        },
        // Will use popup for IDP Providers sign-in flow instead of the default, redirect.
        signInFlow: 'popup',
        signInSuccessUrl: 'http://example.com',// Sign in後の遷移先
        signInOptions: [
            // Leave the lines as is for the providers you want to offer your users.
            firebase.auth.GoogleAuthProvider.PROVIDER_ID,
            firebase.auth.FacebookAuthProvider.PROVIDER_ID,
            firebase.auth.TwitterAuthProvider.PROVIDER_ID,
            firebase.auth.GithubAuthProvider.PROVIDER_ID,
            firebase.auth.EmailAuthProvider.PROVIDER_ID,
            firebase.auth.PhoneAuthProvider.PROVIDER_ID
        ],
        // Terms of service url.
        tosUrl: ''
    };

    ui.start('#firebaseui-auth-container', uiConfig);

</script>
<h1>Welcome to My Awesome App</h1>
<div id="firebaseui-auth-container"></div>
<div id="loader">Loading...</div>

こんな感じで表示されます。 f:id:inari111:20180722220426p:plain

Sign in with Twitter をクリックした後 Authorize app すればログイン完了です。
Firebase Authenticationのコンソールを見るとユーザーが作成されていることがわかります。

f:id:inari111:20180722222559p:plain

まとめ

FirebaseUIを使うと簡単にTwitterログインを実装することができました。
次はFirebase SDKを使ってログイン機能の実装をしていきたいと思います。

BigQueryの集計結果をテーブルに保存した後GCSにエクスポートしてみた

こういう細かいネタもブログに残していこうと思います。
ってブログ書く度に決意しているんだけど続かないですね...

BigQueryの集計結果をテーブルに保存

使うpackageは cloud.google.com/go/bigquery です。このpackageはまだbetaらしいです。
google.golang.org/api/bigquery/v2 でもできるはずですが、cloud.google.com/go/bigqueryのほうが簡潔に書けそうな感じがしました。

コードはこんな感じ
AppEngine上で動かすコードになっていますが、一部変更すれば、AppEngine以外でも動くはずです。

package hoge

import (
    "context"

    "cloud.google.com/go/bigquery"
    "google.golang.org/appengine"
)

// BigQueryのSchema例
type schema struct {
    UserID   string
    UserName string
}

const datasetID = "test"
const tableID = "test"
const query = ""

// 関数名適当で申し訳ない
func bq(ctx context.Context) error {
    client, err := bigquery.NewClient(ctx, appengine.AppID(ctx))
    if err != nil {
        return err
    }

    // 集計結果を保存するDatasetを作成
    d := client.Dataset(datasetID)
    d.Create(ctx, &bigquery.DatasetMetadata{
        Location: "US", // お好みで
    })

    // 集計結果を保存するTable作成
    s, err := bigquery.InferSchema(schema{})
    if err != nil {
        return err
    }
    table := client.Dataset(datasetID).Table(tableID)
    if err := table.Create(ctx, &bigquery.TableMetadata{Schema: s}); err != nil {
        return err
    }

    // Queryの準備
    q := client.Query(query)
    q.WriteDisposition = bigquery.WriteTruncate // お好みで
    q.Dst = &bigquery.Table{
        ProjectID: appengine.AppID(ctx),
        DatasetID: datasetID,
        TableID:   tableID,
    }
    // 最大レスポンスサイズを超えそうだったらtrueにする
    // see: https://cloud.google.com/bigquery/querying-data#large-results
    q.AllowLargeResults = true

    job, err := q.Run(ctx)
    if err != nil {
        return err
    }
    jobStatus, err := job.Wait(ctx)
    if err != nil {
        return err
    }
    if err := jobStatus.Err(); err != nil {
        return err
    }
    return nil
}

BigQueryのテーブル指定してをGCSにexport

CSVファイルにしてexportしてみます

package hoge

import (
    "cloud.google.com/go/bigquery"
    "google.golang.org/appengine"
)

const datasetID = "test"
const tableID = "test"
// export先を指定
const gcsURI = "" // TODO

func export(ctx context.Context) error {
    client, err := bigquery.NewClient(ctx, appengine.AppID(ctx))
    if err != nil {
        return err
    }

    gcsRef := bigquery.NewGCSReference(gcsURI)
    gcsRef.SourceFormat = bigquery.CSV // defaultはCSV
    gcsRef.FieldDelimiter = ","        //defaultは ","
    gcsRef.AllowQuotedNewlines = true

    extractor := client.Dataset(datasetID).Table(tableID).ExtractorTo(gcsRef)
    extractor.DisableHeader = false // お好みで
    job, err := extractor.Run(ctx)
    if err != nil {
        return err
    }
    status, err := job.Wait(ctx)
    if err != nil {
        return err
    }
    if status.Err(); err != nil {
        return err
    }
    return nil
}

まとめ

  • cloud.google.com/go/bigquery を使って集計結果の保存とGCSへのexportをやってみました
  • bigquery packageを初めて使ったので調査のコストは少しありました
  • UTF-8でexportされるのでExcelで開くと文字化けするかも...

Goのstructを比較してdiffを見るなら godebug/pretty が便利

Goのテストでstructを比較するときに reflect.DeepEqual で比較することがあります。

テストを書き、テストを実行するとFAILだったとき...つらい...
structが大きいとさらにつらい。どのフィールドの値が違うのかわかると早くテストを直せて(または実装を直せて)嬉しいですよね。

そんなときgodebug/prettyを使うとdiffを見れて便利です。

github.com

こんなUserのstructがあったとします

type User struct {
    ID string
    Name string
    Email string
}

テストでstructを reflect.DeepEqual で比較し一致するかをチェックします
一致しなかった場合、pretty.Compare でdiffを表示してみます

func TestCompare(t *testing.T) {
    tests := []struct{
        user User
        want User
    } {
        {
            user: User{
                ID: "id",
                Name: "inari",
                Email: "inari111@example.com",
            },
            want: User{
                ID: "id",
                Name: "inari111",
                Email: "inari111@example.com",
            },
        },
        {
            user: User{
                ID: "id",
                Name: "inari111",
                Email: "inar222@example.com",
            },
            want: User{
                ID: "id",
                Name: "inari111",
                Email: "inari111@example.com",
            },
        },
    }

    for _, test := range tests {
        if !reflect.DeepEqual(test.user, test.want) {
            t.Errorf("error: %s", pretty.Compare(test.user, test.want))
        }
    }
}

テスト実行結果

$ go test
--- FAIL: TestCompare (0.00s)
    user_test.go:49: error:  {
          ID: "id",
        - Name: "inari",
        + Name: "inari111",
          Email: "inari111@example.com",
         }
    user_test.go:49: error:  {
          ID: "id",
          Name: "inari111",
        - Email: "inar222@example.com",
        + Email: "inari111@example.com",
         }
FAIL
exit status 1
FAIL    github.com/inari111/godebug-pretty  0.011s

小さいstructなら必要ありませんが、大きいstructの場合は助かりますね。

Goでsliceを結合するときのパフォーマンスについて

sliceを結合するときに、どういう書き方をするのがパフォーマンスがいいのか気になって検証してみた。 検証したのは2パターン。

パターン1: slice同士を結合

func appendSlice(n int) []string {
    sliceA := make([]string, n)
    sliceB := make([]string, n)

    for i := 1; i <= n; i++ {
        sliceA = append(sliceA, "a")
        sliceB = append(sliceB, "b")
    }
    sliceA = append(sliceA, sliceB...)

    return sliceA
}

パターン2: forで回しながらappend

func appendSlice2(n int) []string {
    sliceA := make([]string, n)
    sliceB := make([]string, n)

    for _, s := range sliceB {
        sliceA = append(sliceA, s)
    }

    return sliceA
}

パフォーマンスを計測するためにベンチマークを書く。

package main

import "testing"

func BenchmarkAppendSlice(b *testing.B) {
    b.ResetTimer()
    appendSlice(b.N)
}

func BenchmarkAppendSlice2(b *testing.B) {
    b.ResetTimer()
    appendSlice2(b.N)
}

パターン1のベンチマーク

$ go test -count 10 -test.bench BenchmarkAppendSlice

BenchmarkAppendSlice-8       2000000           631 ns/op
BenchmarkAppendSlice-8       2000000           596 ns/op
BenchmarkAppendSlice-8       2000000           631 ns/op
BenchmarkAppendSlice-8       2000000           595 ns/op
BenchmarkAppendSlice-8       2000000           602 ns/op
BenchmarkAppendSlice-8       2000000           600 ns/op
BenchmarkAppendSlice-8       2000000           596 ns/op
BenchmarkAppendSlice-8       2000000           598 ns/op
BenchmarkAppendSlice-8       2000000           597 ns/op
BenchmarkAppendSlice-8       2000000           596 ns/op
PASS
ok      github.com/inari111/test    18.915s

パターン2のベンチマーク

$ go test -count 10 -test.bench BenchmarkAppendSlice2
BenchmarkAppendSlice2-8      5000000           253 ns/op
BenchmarkAppendSlice2-8     10000000           243 ns/op
BenchmarkAppendSlice2-8     10000000           206 ns/op
BenchmarkAppendSlice2-8     10000000           204 ns/op
BenchmarkAppendSlice2-8     10000000           198 ns/op
BenchmarkAppendSlice2-8     10000000           205 ns/op
BenchmarkAppendSlice2-8     10000000           200 ns/op
BenchmarkAppendSlice2-8     10000000           201 ns/op
BenchmarkAppendSlice2-8     10000000           201 ns/op
BenchmarkAppendSlice2-8     10000000           202 ns/op
PASS
ok      github.com/inari111/test    22.293s

まとめ

パターン1のslice同士の結合のほうがパフォーマンスよさそう。
もし間違ってたら教えていただけると嬉しい。

fish shellでgoenvを使う

メモ書きとして残す。

$ brew install goenv

config.fishを編集する まだ書き方に慣れない

eval (goenv init - | source)
set -x PATH $HOME/.goenv/bin $PATH
set -gx PATH ‘/Users/inari111/.goenv/shims’ $PATH
set -gx GOENV_SHELL fish
goenv install 1.9.2
goenv global 1.9.2