Open Source Fridayで業務としてGoのライブラリを書いてます

どうも、ヒカキン似の taiyo と言います。 この記事はfreee develpers Advent Calendar 2017の9日目です。

私は2017年に新卒でエンジニアとしてfreeeに入社しました。現在はGoとRubyを使用してプロダクトのマイクロサービス化を行うプロジェクトチームに所属し、開発をしています。

そのチームの取り組みの一つに「Open Source Friday」というものがあり今回はその紹介をしたいと思います。

Open Source Friday をなぜやるか

「Open Source Friday」とは、Github社のオープンソースへの貢献を推進する提案のことを指します。

opensourcefriday.com

チームでOpen Source Fridayを実施する主な理由は、

  • Googleの20%ルールに似た取り組みを仕組み化して継続する
  • プロダクトという枠を超えて、プロダクトを支えるOSSへ貢献する
  • コードを集中して書く時間を取る

の三つです。

プロダクトを支えるOSSへ貢献するという点は特に納得感があります。

私たちのビジネスの多くはOSSによって支えられているからこそ、使いっぱなしではなく逆に還元するアクションをすることは、いちエンジニアとして重要なマインドかなと思います。

私自身OSS開発への興味はあったものの、実際に作るに至れていなかったので大変良い機会となりました。

どうやるのか

Open Source Fridayを行うにあたって以下のようなルールが設けられました。

  • 金曜日に入っているチームのミーティングは全て前後に移動する
  • 開発テーマを決める
    • OSSにパッチを出す
    • 自分でリポジトリを立ててコードを書く
    • その他オープンな技術的な活動
  • 取り組んだ内容は、各自発信する

1日で成果を出すために動かせるミーティングはできるだけ移動するという徹底ぶりです。開発テーマはOpen Sourceなものへの何かであれば基本的にはなんでもOKというルールになりました。

また成果を出すこともそうですが、取り組んだ内容をチーム内、社内もしくは社外にしっかり発信していくことも大事かなと思っています。 せっかく作ったものでも、認知されて使ってもらわなければ見つからない課題があるかもしれないですし、むしろOpen Sourceだからこそ世界中のユーザからフィードバックをもらえるチャンスを作ることができると思います。

同じことに困っている人に使ってもらえるようにどんどん発信していきます。この記事はそれの一環です。

成果報告

チーム内でいくつか成果が生まれました。

  • golang/depへのコントリビュート
  • Visual Studio for Macの公式チュートリアルの日本語訳を公式リポジトリにコントリビュート

などなど、メンバーそれぞれの強みを持つ言語、好きな言語において活動をしていたようです。 それらの報告についてはまたの機会にさせて頂いて、今回は私の成果を報告させていただきます。

私はgodateというGoのライブラリを作りました。

github.com

以下に背景と内部設計の簡単な説明をしていきます。

godateとはなにか

Goのプログラム内において日付情報を標準のtimeパッケージよりもシンプルに扱うための軽量なライブラリです。

なぜgodateを作ったか

アプリケーション開発に携わっていると、日付データを扱う場面が少なからずあると思います。

Go言語で日付や時刻について扱う時は、標準のtimeパッケージのTime型を使います。

now := time.Now()
fmt.Println(now.Date()) // => 2017 November 4

しかし、開発をしているとTime型で日付を扱う時に、以下のことが気になり始めてきました。

関心事が多い

Time型は、時刻とタイムゾーンのデータを必ず持ちます。

Time型のインスタンスが持つ時刻やタイムゾーンのデータまで含めて管理しなければならず、日付だけを扱いたいという時に限っては関心事が余計に多い気がします。

また後述する「比較系メソッドが使えない」は、時刻まで気にしなければならないことの一例です。

時刻の情報を含んでいることを気にせず実装を進めていってしまうと思わぬバグを生んでしまうかもしれません。

日付では年月日の情報が重要なので、日付を扱う時は時刻やタイムゾーンなどは関心ごとから外したいところです。

DBのカラムでdate型を使いたい

現状、DBテーブルのカラムとTime型を面倒なく対応させるために、DBカラム側の型をdatetime型にすることがほとんどだと思います。

一方で日付データをDBに保存する時には時刻の情報は必要がなく、時刻情報が入ってしまうdatetime型ではなくdate型にしたい時があります。

Time型でそれを実現しようとする場合は、こちらの記事でも示されているように自前で実装しなければなりません。 okamuuu.hatenablog.com

日付の比較にTime型の持つ比較メソッドが使えない

Time型にはEqual, After, Beforeなどの比較メソッドが実装されてます。

これらは、Timeインスタンスの大小?を判定してくれるメソッドですが、Timeインスタンスが持つ秒もしくはナノ秒で大小同等を判定しています

そのため日付だけで比較をしたい場合はこれらを使うのはあまりベターではありません。

time/time.go

// Equal reports whether t and u represent the same time instant.
// Two times can be equal even if they are in different locations.
// For example, 6:00 +0200 CEST and 4:00 UTC are Equal.
// See the documentation on the Time type for the pitfalls of using == with
// Time values; most code should use Equal instead.
func (t Time) Equal(u Time) bool {
    if t.wall&u.wall&hasMonotonic != 0 {
        return t.ext == u.ext
    }
    return t.sec() == u.sec() && t.nsec() == u.nsec()
}
today := time.Now().Round(time.Hour * 24)
today2 := today.Add(1)

// 同じ日付だけど1秒分だけ進んでいるのでfalseになってしまう。
today2.Equal(today) // => false

システム全体でナノ秒までの管理を完璧にできるのであればその限りではないですが、それは結構コストがかかりますし、そんなことは気にすることなく開発したいところです。

上記を解決するために作ったのが今回のgodateです。

godateの実装

godateで最も重要なのがDate型とNullDate型です。

Date

Time型に代わる日付を扱うための構造体です。

上述の三つの問題点を解決するための機能を付与しました。

最低限の情報しか持たせない

Date型にはシンプルにYear, Month, Dayの3つのフィールドだけを持たせています。

// Date has Year, Month, Day fields.
// Date don't have Location or TimeZone information.
type Date struct {
    Year  int
    Month time.Month
    Day   int
}

それにより時刻の情報やタイムゾーンを気にする必要がなくなり、日付を扱う時の関心ごとが減りました。

時刻情報やタイムゾーンを持たない設計はRubyのDateクラスを参考にしています。 迷った部分ですが、思い切ってタイムゾーンと時刻の情報を捨てた設計にしてみました。

DBのdate型に対応

sqlパッケージのScannerインターフェースを満たすScanメソッドを実装していて、date型に日付の情報のみ挿入できるようになっています。

比較メソッドを用意

After(), Before()メソッドを用意しました。これらは日付レベルでの比較を行うので確実に日付比較ができます。

Equal()メソッドも用意はしていますが、これは==オペレータのエイリアスです。 Date構造体をシンプルにしたことで、Time型とは異なりインスタンス同士が比較可能になりました。

NullDate

guregu/nullを参考にして、Dateでnullを許容するために作りました。

これを使うことでnullableなDBのカラムや、日付データを含むJsonのUnmarshalでnullを受けられるようになります。

その他実装で考慮した点

  • Time型にあるメソッドとインターフェースをほとんど揃えているので、godate導入による修正は最小限で済むはず。
  • Time型との互換性を考えてToTime()メソッドを用意しました。
    • これはTime型の資産を使うために、内部でも多用しています。

悩み

Dateの構造を基準日からの累積日数にするかどうかで迷っています。

フィールドをYear, Month, Dayにしたことで、コード上でも分かりやすくシンプルな構造にはなりました。

しかし例えばgodate.New(-1, 1, -1)というように、日付情報としてはInvalidな値によってオブジェクトが生成された場合に動きを保証できないため、 Valid()などの検証メソッドを設置して各自チェックしてもらうなどの対処が必要になったりします。

累積日数にすると、上限はあるもののInvalidな値になることはほとんどないため上記のような対応は必要なくなるかと考えていたりします。

// 例えばこんな感じ
type Date struct {
  Days uint64
}

試行錯誤していますが、早いとこ方針を決めてみなさんに使ってもらえる形に落とし込んでいきます。

もし上の悩みの件で「こうした方がいいんじゃない?」とかありましたらぜひぜひフィードバックください。よろしくお願いします! ちなみにgodateが落ち着いたら、次は「Go製N+1発見器」を作ってみようかと目論んでいます。もし車輪の再発明だったとしてもやります(強い意志)。

最後に

以前freeeのインターンを経験してから入社した経緯に関してこちらに書かせていただきました。

freeeでは、この冬からの19卒の新卒エンジニア/デザイナーの本選考を始めています。興味がございましたら、ぜひご応募ください。

jobs.freee.co.jp

最後まで読んでいただきありがとうございました!

明日は、freee指折りの名司会兼パリピモバイルエンジニアのabeさんです!お楽しみに!