emahiro/b.log

Drastically Repeat Yourself !!!!

Cookpad TechConf2018に行ってきた!

こちらに参加してきました

techconf.cookpad.com

基調講演

遅れて参加したので聞いてません。

クックパッドの “体系的” サービス開発

BMLループ

buildの失敗

  • 手戻り?

mesureの失敗

  • ログの取り忘れ

learnの失敗

  • 数字は動いた
  • しかしイマイチ効果がわからない
  • 数字は得られたが学びがない
  • 属人的

Buildの前に行っておくこと

  • BMLループを前から順に行わない
    • 手戻り防止
    • 逐次にやろうとすると結構大きな手戻りが起こる
    • 最初にサイクル全体を設計する
  • 効率的な学び
    • 施策結果に対する予想
      • 現実の理解
      • サービスに対する理解
      • 想定との良し悪し->なんで??
    • どういう結果が出そうなのか?
      • mesureやlearnのフェーズで考える内容を考えておく

Mesureの前にしておくこと

  • 計測結果の選定
  • KPI設定
    • 指標やKPIはそれ単体で存在することは珍しい
    • 通常は別の指標とバッティングする
    • 事前にバッティングする箇所を予想しておく
  • ログの確認、SQLの実行

Learn前にしておくこと

  • 指標解釈の整理
  • 結果の想定
  • 👉成功のイメージを共有する
    • その施策が成功した時に、ユーザーはどういう体験をしているのか?

社内ツール

  • Build
    • 価値仮説シート
    • Chanko
    • EasyAB
  • Mesure
    • 社内ツールがある
  • Learn
    • Report.md
      • 施策の分析レポートをMarkdownで作成し、PRベースで管理していく
      • Report.mdを先に作ってサイクルを決める

まとめ

  • 仮説の実行から学びを得るサイクルを先に設計する
  • 前から逐次体に行わない

クックパッドクリエイティブワークフロー

Cookpadは1サービスを作るのに140人のエンジニアがいる

  • ユーザーのシーンごとにGroup分けしている。

料理きろくのアプリ版チーム

  • Missionオーナー
  • デザイナー
  • エンジニア ✕ 2
  • エンジニアとデザイナーがお互いを補完する

目的と仮説を明確に

  • 常になんのためにしているのか意識する

Github issueでのアイデア発散

デザインレビュー

  • 目的・背景・コンテキストを明確化
  • 職種関係なく、横串でデザインを評価する
  • 品質向上

数値は全員が見れる場所に

テスト

  • 考慮漏れ・見つめ直す
    • TsuyoiUI・・・社内のチェックツール
      • issue作ったら、issueテンプレートに自動的に反映する

リリース・分析

  • あの時誰かがしてたな???(゜゜)問題
    • チーム内外の共有をすぐしたい
  • Report.mdの誕生
    • 見たい時にすぐ見れるレポートの管理

デザイナの役割

  • デザインリリースマネージャ
    • リリース単位で体験やUIの変更箇所を把握、デザイン周りを一貫してみる役割

What/How to design test automation for mobile

テスト自動化について

  • 開発サイクルの効率化?
  • 自動化がモチベーション?

テストで話が噛み合わないところのコミュニケーションの方法

SPLIT がキーワード

-よくわからないものを分割していく

Scope

  • どこをテストの対象範囲とするか?
    • モバイルアプリを使っているユーザー
    • どこまで踏み込んでいってテストをするのか?

Phase

  • 開発中?orリリース後?
    • in Production
      • 世に出したあとのテスト
        • A/Bテスト
        • カオスエンジニアリング

Level

  • なにをテストするのか?
  • どこを自動化するのか?

sIze

  • テストの種別

    • UnitTest
    • IntegrationTest
    • ...
  • テストサイズを分割する

  • どういう形に落とし込んでいくのか?

Type

  • テストを目的別に区分

範囲、時間的なもの、どこまでするのか、どこまで自動化するのか?にどんどん分割していく。

Cookpad Android for Globalでの事例

  • 事前のコミュニケーションが大事
    • 理想的なプロダクトのラインを決める
  • Scope事例
    • ユーザーが実際に使う状態に近しい環境
    • ネットワークが関係する環境のテスト
  • Phase
    • in developent
      • 開発中だった
      • sizeを再定義 3つから4つ
        • UnitTest
        • IntegrationTest
        • UI Components based
        • User Senario based

iOSはパフォーマンスをテスト自動化しようとしている

  • 以前似たような状況にあって、パフォーマンスが劣化した例があった。

Rubyの会社でRustを書くということ

CookpadRubyの会社

  • 多くのサービスがRuby on Rails
  • 但し全てのサービスがrubyで書かれているわけではない
    • Middlewareなどはjava、Goで書かれてたりする
  • CookpadRubyの会社だけではあるが、Rubyだけの会社ではない。

Push通知を配信基盤をなんとかする

  • 都度配信

    • イベントごと
  • 一斉配信

    • 特定層に一斉配信

Rustで書き直す前のPush通知配信基盤とは?

  • 殆どの部分をアプリケーションが担っていた。
  • 基盤というにはあまりに機能が少なすぎた

もともとは世界最大のRailsアプリケーション

  • Pushするアプリケーションは1つ以上にある
    • しかし今はマイクロサービス化が進んでいる。
    • 1つのときは基盤が貧弱でもなんとかなった
    • 同じロジックが色んなアプリケーションにコピペされている。
      • DBを共有することは避けたい
      • DBに接続することも避けたい

新規版では全てのステップを配信基盤で担う

  • UserId指定
    • 配信先を受信設定に基づいてfilterする
    • Push通知の配信見積もり

厳しい性能要件があるアプリケーションをrubyで書くのはしんどい

  • Rustには色んなメリットがある。
    • 型、安全性、並行性
    • トレイト
    • データ競合のあるロジックはコンパイラが起こってくれる

Rust is not magic

  • Rustで書いたからと行って勝手に高速になるわけではに。
  • 早いソフトウェアを書くのはプログラマ市議

Facebookのデータローダの考え方を使う

  • クエリをまとめる

OSSを開発

github.com

github.com

※ ここからはさきは説明形式なのとrustなので詳しくわからず

  • ブロッキングな処理をFutureに変換しているとみなせる
    • JSのプロミス的な概念

Rustのいいところ

  • マルチスレッドが安全

    • マルチスレッドとイベントループを混ぜ込むみたいな複雑なアーキテクチャも採用できる。
  • とりあえずロックとかなくなる

  • 安心して高速化
  • 型安全、強力

cookpad storeTV 〜クックパッド初のハードウェア開発〜

StoreTVは三者にメリットがあるサービス

  • スーパー
  • ユーザー
  • 食料品メーカー

ハードウェアでも改善サイクルを回していく - ユーザーにあててサイクルを回す

サイクル

  • 第1サイクル
    • 最小価値影響
      • 売り場で料理動画を探す
        • 料理動画を探す・見れる
        • 300台配布
      • キッティング
        • 1台10分
        • 週ごとに料理動画を作った
    • 価値検証
      • 電話調査
        • 動画見てて楽しい!
        • 自分の作りたい動画探せる!
      • アプリの利用ログ
      • 売上 -> Posデータの提供を受けた
        • StoreTVの採用した売り場は他の売り場の1.3倍の効果アリ
    • その他の問題
      • 動画の数
        • 注力商品のみで良いと思っていた
          • 売り場にあってない
      • 見た目の問題
        • 目立たない
      • キッティング
        • 手作業が手間
      • 端末管理
        • アプリの更新が手作業
  • 第2サイクル
    • スケール
      • なにをやって、なにをやらないか
      • 得意なことは自分たちでやる、不得意なことは誰かに任せる
    • 動画の数
      • 毎月100本
    • 見た目
      • 不得意
      • ケースを作成する
        • 業者に依頼
          • 中国製
          • 中国に行ってきた
            • ロゴがバグってるw
      • 不得なことは1人でやらない、でも任せっきりはだめw
    • キッティング
      • 不得意
        • 業者に依頼
          • どうしても人力に頼らざる得ない
            • アプリケーションを整えた
    • 端末管理
      • 不得意
        • 自動アップデート
          • MDMによる一元管理(Mobile Device Managiment)
    • 問題点
      • サイネージサイズ
        • 大きすぎる
          • 売り場に置けない
      • 開発新興
        • バグが増え始める
  • 第3サイクル
    • 収益化
      • 広告配信の基盤開発
      • 広告接触者数のカウント
    • サイネージサイズ
    • 開発進行
      • 長時間安定して動画
      • バグの検出
        • 時限製のバグが発覚した
          • コードフリーズ後の長時間再生
      • 顔認識の機能
        • 端末計算リソースを食う
        • エグザイルを利用したww
          • なお、エグザイルに1台やられたww

Challenges for Global Service from a Perspective of SRE

クックパッドのグローバル・サービスってなに?

  • 海外向けのレシピサービスを提供している
    • 22言語68カ国で提供
    • イギリスで提供している

2017年のグローバルサービスの成長

  • 対応言語数
    • 15->22言語
      • 7言語の増加について
        • 全言語対応 ≠ 世界対応
        • 去年のスライド見てね

techconf.cookpad.com

現状の課題や挑戦

  • 特定の国のユーザー体験が悪い

    • 国ごとに差が出始める。
      • 原因がわからないと改善できない。
  • 世界中のユーザー体験を測定する

    • CatchPointを利用した。
      • CatchPoint Systemsのメトリクスを利用した
    • 原因を調査できるようになった。
    • インドネシアと米国で比較
      • TLS接続
      • Time to First Byteが遅い
      • -> 米国からの距離が遠い。
        • Cookpadのグローバル版は米国リージョンにある。
    • データセンターをユーザーに近い場所に移動
      • マルチリージョン化
        • しかし管理コストがかかってくる
    • CDNの導入
      • Fastlyを導入
      • 米国から遠い国のユーザー体験が改善
  • イベントのバリエーションが多い
    • ex.
      • アルゼンチンは独立記念日パステリートというお菓子を食べる
      • 日本だとバレンタインの時にユーザーが増える
    • 展開国が増えるとイベントバリエーションが増大する
      • SREは「毎月バレンタイン」
    • 課題改善
      • Amazon Auroraを導入
        • オートスケーリング
      • Dockerアプリ開発環境の提供
        • ECS + hakoでのデプロシステムの導入
        • hako-consoleを使って状態を管理
          • 日本で培った技術をグローバルに応用
  • デプロイのオペレーションコストが高い
    • ネットワークが安定しない、日常的に停電が起きる国とかある。
      • デプロイを他の人に依頼する問題
        • 世界中どこからでもデプロイ出来るようにする
        • 米国にデプロイサーバーを用意
          • デプロイサーバーでのマニュアルオペレーションの課題もある
    • 改善策
      • slackによるbotデプロイ
  • toilが急増する
    • toilとは?
      • 骨の折れる業務
      • アカウント管理業務とか
    • SREの対応する依頼業務が増大
      • toilの割合が増大
      • SREが本来したい業務ができなくなった
    • 世界中に社員がいるからこその課題。
    • アカウント管理
      • nginx + omniauth
      • アカウント管理のセルフ管理化(移譲)
    • SREのマルチリージョン対応
      • 時差の壁を超えたりとか

動き出したクックパッドのCtoCビジネス

  • komercoの発表
    • もので毎日の料理を楽しくするプラットフォーム
      • 料理を盛る器や鍋がいいと、もっと料理が楽しくなる

komercoにはサーバーサイドエンジニアがいない

  • サーバレス
  • Firebaseで開発している

(このあたりやたらFirebaseの宣伝というか、いいこと話してること多かった)

※ Firebase Japan User Group入っておこう。

firebase.asia

OpenSourceにすること

  • 少人数で品質を担保するため
    • Opensourceすると品質が向上する
    • 外部の人がチームのリソースになる
    • 再利用できる
      • 他チームで利用できる

技術一覧はこちら

Pring

  • firebase Model framework

github.com

Orderable

  • Order processing framework

github.com

開発の高速化の先になにがあるのか?

  • 大胆な戦略変更
    • 複数回の仕様変更
    • エンジニアが消耗しない
    • コミュニケーションコストがかからない
    • たくさん試すことができる
  • 事実は我々の中にない、市場に聞くべき
  • たくさん試すべき

Solve "unsolved" image recognition problems in service applications

画像分析のお話

昨今の画像分類問題

  • 理想的な状況下では「解けた」と言われている
    • 適切なラベルの付与
    • 適切なカテゴリの設計
    • closed set
      • 現実世界はopen setである
      • ラーメンの画像分析にケーキの画像がくることなんて容易にある。
  • 私たちが本当にときたい問題はなんなのか?
  • 解くべき問題の多くは「間違っている」
    • 本当に時対問題をtry and errorで探していく

料理きろくの進化と現在

  • 機械学習の観点から見る重要な点
    • クイックスタート
      • 要素技術が成長している
    • モデルの改善と苦手なカテゴリの考慮
      • 間違いやすいやつら
      • 植物はサラダと分類される
      • テストデータの拡充
      • 局所性を取り込むためのpatch化
        • 画像の一部だけに料理が移っていた時
        • ふと、お店で出てきた画像とかどうするんだろう?
          • 自分で作った料理ではなさそう???
          • それは分類されてもいいのかな???
    • 料理きろくのその先へ
      • 勝利写真のレシピカテゴリに分類
      • 単純な分類に見えて実は非常に難しい
        • openset における予測
    • 類似カテゴリをどう予測するか
    • 類似画像に対する分類

類似モデルは生まれるのか?

  • カテゴリを設定した時にどういう画像が生まれやすいのか?

モバイルへの移植

  • どうサービスに載せるか
  • 次に来そうな領域

Beyond the Boundaries

※ 基調講演

  • 技術を正しく理解し、「ふつうに使う」
  • 技術スタック
    • web
    • mobile
      • Swift
      • Kotlin
      • プロトタイピング
        • React Native
        • Firebase
    • Infra
      • AWS
      • docker
      • Hako
  • 自分たちの道具は自分たちで持続可能にする
    • コミュニティへの還元
  • 「境界」を認識し、乗り越える強い組織へ

まとめ

2年ぶりにCookpad Tech Confに参加してきました。
領域としてはプロダクト作りからグローバルでのサービス展開、機械学習まで幅広い話が聞くことができました。
個人的には、SREの話やPush基盤作りの話が面白いなーと感じましたが、HTTPやCookpadのプラットフォーム作りの話をもう少し聞いてみたかったと思いました。

HTTPリクエストでReferrerを送信しない

Referrerの制御方法について調べたので備忘録です。 下記ページを参考にしてましたがIE11での挙動など自分で追加で調査した箇所があるので付け足して行きます。

qiita.com

Referrerとは?

HTTPリクエストを投げたときの参照元です。
ブラウザがHTTPリクエストを投げる時、Referrerヘッダーに参照元(元いたページ)のURLが入ります。

Referrerヘッダーを制御する意図

特定のページ上でSNSウィジェットだったり、Youtubeウィジェットだったり、外部サイトの情報をiframeで埋め込むことは今時のウェブサービスであれば使われている画面が数多くあります。
しかし、このAPI呼び出しをする場合でも外部サイトへのリクエストとみなされるのでブラウザの開発者ツールで外部サイトのリクエストの中身を見ると Referrer: ヘッダーに参照元(元いたページ)のURLが表示され、Referrerヘッダーに乗って、外部サイトにURLが送信されます。

例えば、URLが非公開URLであったり、ワンタイムでなく時限token付きURLであった場合、外部に漏れては行けない情報が漏洩する可能性があります。

こういう時、セキュリティ上のリスクを抑えるため、参照元ページのページ情報を Referrer: ヘッダーに乗せずにリクエストを送信しないといけなくなります。

これはReferrerを制御する意図です。

制御方法

イマドキのブラウザ

イマドキのブラウザと古いブラウザで対応方法が異なります。

今時のブラウザ

などは最初に記載した参考のQiitaの記事の内容で大丈夫です。

<meta name="referrer" content="no-referrer">

こいつをheadのmetaタグに入れてやれば、Referrerは送信されません。

しかし問題があります。
このmetaタグの content="no-referrer" はIE11では使えません。

つまり、IE11でアクセスされたら上記のmetaタグを埋め込んでいても、Referrerが送信されて情報が外部サイト漏れます。

参考: https://webtan.impress.co.jp/e/2015/04/14/19750

IE11対策

IE11を始めとした古いブラウザには以下で対応します。

<meta name="referrer" content="never">

support ブラウザを調べる

以下で調べられます。

Can I use... Support tables for HTML5, CSS3, etc

確認したいブラウザにカーソルを合わせてみると使える contentを知ることが出来ます。

IE11の場合

f:id:ema_hiro:20180204030004p:plain

Edgeの場合

f:id:ema_hiro:20180204030018p:plain

(Edgeも対応してないんかい!!!!)

まとめ

Referrerを使った脆弱性について調べることができました。
何気なく使っているiframe埋め込みでも使い方次第では脆弱性になるので、ブラウザのサポート状況は常に確認する必要がありますね

(ほぼ)初めての自宅(リモート)作業をしてみた話

注) プログラミングネタではないです。

自宅作業の経緯

職場でインフルエンザが流行し、感染防止のために学級閉鎖の如く、所属している部署全体で自宅作業推奨になったため、期せずして自宅作業をする機会を得ました。
リモートで仕事するのはほぼはじめてだったので、実際にやってみて感じたこと、工夫したことをまとめました。

※ これは職場の緊急的な措置で、決してこの状態を肯定しているわけでも、期待しているわけでも、リモートにすべきだみたいな意見を持っているわけはありません。ただ、個人としては少しワクワクはしていました(笑)
※ なお、普段は基本的に出社するのがルールになっています。

自宅作業時期

1末〜2頭の3日間。
※ 本当は4日の予定でしたが最終日は予定があったので出社しました。

さまり

先に結論を述べておくと、僕個人の感覚としては、「やっぱり仕事は会社でしたい」ということを再確認しました。
エンタープライズ向けサービスの企業ではリモートが推奨されてますし、世の中の流れとしてもリモート推し的な空気を感じますが、個人の感覚としては会社の方が生産性が上がると感じます。
これは僕の主観なので、リモートの方があっている人ももちろんいると思います。
ただ、やはり100%リモートにするには色々超えないと行けない壁が大きい気がします。

ちなみに、リモートで業務をしてみて、仕事をする上でよかったことや、普段と違うことで気づいたこともありました。
仕事をする上でにリモートも選択肢としてあるのはプラスなことだと思いました。

リモートをしてみて

  • よかったこと
  • わるかったこと
  • 気づき/工夫したこと

よかったこと

通勤時間分寝れる

重要です。起きた時に「あ、そうか今日いかなくて良いんだ」って思って30分余計に寝ました。(起きろよ)

ひたすら自由

会社は公共の場なので、当たり前ですが周囲に気を使います。
それがないのでストレスフリーでした。
ジャージや寝癖のまま仕事しても誰もに文句言われませんし、誰かの目を気にする必要もありません。(ただ、この弊害はあります。後述します。)

細切れ時間を有意義に使える

ビルドしている最中やテスト回している間だったり、チャットのレス待ちのちょっとした時間に洗濯機回したり、食器洗ったり、掃除機かけたりしてました。
会社にいると、手持ち無沙汰の時間にコーヒー買いに行ったり、スマホゲームでガチャ回したり、ニュース見たり、Twitterしたりと、あまり「生産的でないこと」に意識をもってかれてますが、家事をすると少し生産的なことをした気分になって良いです。

自宅に引きこもるのでエコ

僕は基本家にいると決めた時はずっと引きこもるのでほぼ自炊です。
光熱費はかかりますが、外食したり飲料水買ったりするお金を使わずに過ごせるので、僕にとっては非常にお財布に優しくエコでした。

わるかったこと

メリハリつかない

多分、僕がリモートに向いてない理由の1番はコレです。
生活の延長で仕事しているので、よく言われるようにメリハリつかないです。
なんかダラダラしてるなーって感じることがありました。
今回は4日間限定だったので、なんとかなりましたが、これ、ずっと続いたら確実に自分はサボり始めると思いました。

プライベート見られるのはいい気分はしない。

オンラインMTGでビデオ通話したんですが、インカメで部屋の一部が映るのはあんまりいい気分しませんでしたw
こういうとき書斎とかあるとすごくいいんだろうなー。

(たぶん)太る

手の届くところにご飯やお菓子があるので、ひたすら何か食べながら仕事してました。コレ続けたら多分太ると思います。

気づき/工夫したこと

ここからは個人の感覚レベルでの気付きや、工夫したことを書き出します。

身だしなみは整えたほうが良い

寝間着でデスクに座ってもなにも始められません。
顔洗って、髭そって、着替えましょう。
コレだけで大分違いました。

家事大事

お昼ごはん作ったり、細切れ時間に洗濯もの干ししたり、業務とはコンテキストが全く違うことをするのでいい意味で頭の切り替えになります。というか勝手に切り替わります。
職場だと昼時にもネットニュース見たりとか、中度半端にエンジニア脳が残るので、実はこれ、脳のエネルギーを無駄に消費しているだけなんじゃないかって思いました。

オンラインMTGは十分MTGになる。

ハングアウトでオンラインMTGしたんですが、もうこれ十分MTGになりますね。
オンラインMTGの進歩を身をもって痛感しました。
段取りをちゃんとしてもらったというのもありますが、別にその場にいなくても会議はできるもんですね。
(ただ、インカメは大敵ですw👆上述)

ちょっとしたコミュニケーション大事

お昼行く時にslackに「昼行ってきます〜」と書いて、戻ってきたら「戻りましたー」と書いて自分のステータスをちゃんと伝えること大事だと思いました。
不要な憶測や不信感軽減のため。

お昼ご飯に行く

f:id:ema_hiro:20180202030033p:plain

お昼ご飯から戻る

f:id:ema_hiro:20180202030037p:plain

※ 同部署にいるyoichiroさんにリモートで働く上での教訓を聞いていたので意識的に行いました。

ちゃんと説明するようになる

基本チャットしかコミュニケーションツールないので、自分のステータスをちゃんと説明するようになります。(上述の内容と少し被りますが。)
会社にいると対面・口頭確認に逃げられますが、リモートだとそれが効かないので、冗長でもちゃんと説明するようになります。
ただ、神経使いますし、チャットするだけで疲れちゃうなーってこともありました。
ここらへんは慣れなのかもと思いますが、ぶっちゃけあんまり意識してなかったツケですね。普段からもっと丁寧にコミュニケーションしとけよっていう話でもあります。

当たり前ですが、コミュニケーションって大事。 とはいえ、やはりチャットだけだと100%意図伝えきれないこともあるのかなーと感じて、会社で対面でコミュニケーション取った方が文章だけのコミュニケーションよりストレスレスだし、意思疎通は格段に速いと思います。
(言葉遣いにとかに対して厳しい人だと余計に疲れそうな気もします。)

椅子にはお金をかけたほうがいい。

自宅にはアーロンチェアを入れてます。同僚でも自宅の椅子が劣悪なのでやはり出社しますという方がいて、リモートするなら自宅の設備投資はマストだと感じました。

通勤時間が気分転換になるんだなーと再認識

僕には通勤時間にぼーっと景色眺めてたりするのが案外気分転換になってたんだなーって再認識しました。
会社にいかないとそもそも外に出ないので、それはそれで精神衛生上よろしくないなと。外の空気は吸ったほうがいいという教訓。
(※ 完全に個人的な感覚です。)

結論

最初にも述べましたが、やっぱり自分には会社の方が仕事するにはあってる気がしました。
理由はコミュニケーションコストと仕事モードへの切り替え。

やはり、対面のコミュニケーションによるスピーディーな意思疎通をリモートでは超えられないと思いました。
また、個人的には場所が仕事モードに切り替わるための重要な要素だったので、自宅のままだとどうしても勢いが出ませんでした。

ただ、リモートしてみて普段自分があんまり意識してなかったことにも気づけたこともありました。
リモートでの綿密なコミュニケーションだったり、通勤ストレスなかったり、ちょっとしたコミュニケーションの重要性認識したり。

働き方改革が標榜されてるからか、会社にいかないと仕事できないなんて時代遅れだ!リモート導入しろ!的な会社にいかないことが正であるみたいな空気を感じるのですが、実際にしてみた身としては、制度うんぬんの問題というよりも、個々人の状況に応じて取りうる選択肢に幅があることが大事なんじゃなかろうかと思いました。
いつもは会社に行くけど、たまにのっぴきならない理由で出社できなかったり、休むほどでないけど、風邪っぽくて周囲に感染させるのを防止させるために自主的に自宅作業にしたり、その他家族の問題があったりと、色々な理由があるときにリモートもいいよって言われるとすごく心理的に楽だろうなとは思います。実際そういう声聞くし。
(まぁ、書いてる僕自身は独身ひとり暮らしなので、ここらへんはあまり優先度上がってこないことではありますがw)

あんまり極端でどちらかに寄った意見ばっかり目にする機会が多いので、もっとその中間というか、時と場合によってどちらも選べるよみたいな考え方が広まっていくと良いと思いました。

GraphQLを書いてみた話

仕事ではjson-shemaを使ったRESTを使っています。

API定義がドキュメント化されていることの意義

マイクロサービスアーキテクチャを元に開発していると、コンポーネント間のインターフェースの定義なしにはやっていけないと感じています。
もしjson-schemaがなかったら、各コンポーネントのroutingを見て、対応するhandlerがどんなjsonを返すのかそのコードを見に行くしかないです。
また、server sideの側の実装次第ではResponseの内容が急に変わってしまってクライアント側が予期せぬjsonが帰って来るなんてことがあるかもしれないです。

全てをserverサイドで行ったり、モノリシックなアーキテクチャで大規模サービスを運用することが難しくなってきている今、コンポーネント間のインターフェース定義を外出して、それを見ればインターフェースの内容がわかるようになるというのは開発の効率化を鑑みても必要な事になりつつあるように感じます。

幾つか試そうと思ったのですが、少し前から話題になっていて、個人的にも興味があった GraphQL を実際にgoで書きながら実験的に実装してみようと思います。

graphqlの基礎単語

Query

データの取得をする時に使う。
RESTでいうGETの時に使うと思われるクエリ。

Mutation

データの更新をする時に使う。
RESTでいうPOST/PUT(PATCH)/DELETE のときに使うと思われる。

Sample

https://github.com/graphql/graphiql でGraphiQLのライブデモ を触ることが出来る。 このライブでもを元にQueryを勉強してみると、

まずRootのQueryがあります。

f:id:ema_hiro:20180130014423p:plain

この中で allFilms のQueryの定義の中をみると

f:id:ema_hiro:20180130014653p:plain

以下のように引数の各型とResponseの型が決まっています。(ResponseはFilmsCollection型)

f:id:ema_hiro:20180130014710p:plain

I/Fの定義は

  • args(引数)
    • after: String型
    • first: int型
    • before: String型
    •  last: int型

※ FilmのListを取得するメソッドなので、◯話~□話以内とかを指定するための first/last、◯話以内/以上全てを指定するための before/after みたいな定義方法なのかと思います。なんでbefore/afterがStringかは不明(intでいいじゃん)

  • I/FのResponse
    • FilmsCollection(アプリ内で定義)

と読むことが出来ます。 また FillsCollection もどういう定義なのかを追っていくことが出来ます。

graphqlを動かす環境の用意

今回は毎度おなじみgolangで書いてみようと思います。

http://graphql.org/code/#go によると公式にgoのライブラリがあるのでこれを使います。

使用ライブラリ: https://github.com/graphql-go/graphql
Godoc: https://godoc.org/github.com/graphql-go/graphql

depでgraphql-goをinstall

$ mkdir graphql_samples
# direnvでプロジェクトルートをGOPATHに指定します。
$ echo "export GOPATH=$(pwd)" >> .envrc
$ direnv allow
$ mkdir src && mkdir src/graphql_samples && cd src/graphql_samples
# dep を使ってインストール
$ dep init
$ dep ensure -add github.com/graphql-go/graphql                                         
Fetching sources...

"github.com/graphql-go/graphql" is not imported by your project, and has been temporarily added to Gopkg.lock and vendor/.
If you run "dep ensure" again before actually importing it, it will disappear from Gopkg.lock and vendor/.

これで準備完了です。

実際にスキーマを書いてみてる

graphql-go のREALDMEを参考にして書いてみます。

package main

import (
    "encoding/json"
    "fmt"
    "log"

    "github.com/graphql-go/graphql"
)

func main() {
    // schema定義
    fields := graphql.Fields{
        "hello": &graphql.Field{
            Type: graphql.String,
            Resolve: func(p graphql.ResolveParams) (interface{}, error) {
                return "world", nil
            },
        },
    }

    rootQuery := graphql.ObjectConfig{
        Name:   "RootQuery",
        Fields: fields,
    }

    schemaConfig := graphql.SchemaConfig{
        Query: graphql.NewObject(rootQuery),
    }

    schema, err := graphql.NewSchema(schemaConfig)
    if err != nil {
        log.Fatalf("failed to create schema error. err: %+v", err)
    }

    query := `{
      hello
  }`

    params := graphql.Params{
        Schema:        schema,
        RequestString: query,
    }

    r := graphql.Do(params)

    if len(r.Errors) > 0 {
        log.Fatalf("Failed to execute graphql operation. err:%+v", r.Errors)
    }

    j, err := json.Marshal(r)
    if err != nil {
        log.Fatalf("Failed to marshal json. err: %+v", err)
    }

    fmt.Printf("%s, \n", j)
}

動作させてみると

$ go run main.go
{"data":{"hello":"world"}},

Query(GETのリクエスト)では data というプロパティにGETした結果が入ってきます。
schemaFields で定義した "hello" プロパティに返さえるデータの型と結果 Resolve (ここではfuncで文字列を返しているだけですが)が入ってきます。

Getの結果をFiledType指定出来るのは非常に安全にリクエストを定義できていいなと思います。
このあたりは json-schema でも似たような異してますね。

ちなみに以下のような形で int型を入れてみた場合でも

// schema定義
fields := graphql.Fields{
  "hello": &graphql.Field{
    Type: graphql.String,
    Resolve: func(p graphql.ResolveParams) (interface{}, error) {
      return 1, nil
    },
  },
}

実行していみると

# int型
$ go run main.go
{"data":{"hello":"1"}},

nilにしてみます。

// schema定義
fields := graphql.Fields{
  "hello": &graphql.Field{
    Type: graphql.String,
    Resolve: func(p graphql.ResolveParams) (interface{}, error) {
      return nil, nil
    },
  },
}

出力結果は以下

$ go run main.go
{"data":{"hello":null}},

intの場合はjsonでも "1" とString型に、nilの場合は null がちゃんとjsonのdataプロパティ内の hello プロパティに入ってきました。
GraphQLにおいてはResponseの定義は FieldType に絶対変換されてくるので、定義を外出しておけば、クライアントサイドのエンジニアは定義を読めばクライアント側の実装には迷うことなさそうです。

まとめ

まずは簡単なクエリから書いてみました。
普段仕事でjson-schemaを使っているのですが、json形式でなく、コードの形で定義を管理できるのはこれはコレで見やすいと感じました。

次は簡単なAPIを書いてみたり、定義の外出しをやってみようと思います。

勉強記録はこちらに書いていこうと思います。

github.com

参考

developers.eure.jp

speakerdeck.com

Goにおける簡易だけど最低限の情報が入っているerror出力方法の知見

errorについての検討

golangはerrorまで含めて愚直に書くことが求められたりして、それはそれで慣れではあるなーと感じているのですが、errorのformatが人に依存したり、1人で個人開発とかしているとerrorをちゃんと書いてるのがめんどくさくなったりするので、ある程度簡易にかけて、どんなerrorが返っているのかわかりやすい方法はないものか、を考えていたところ、同じチームの人の知見が非常に役に立ったので、検討して備忘録としてまとめてみました。

最低限の要件

errorを書くのであれば、多少冗長でも、どういうerrorが返ってきていて、さらにはどのpackageで出力されたのかがわかってかつ、formatが大体統一されていれば、大体十分かなという程度で考えていきます。

要件

  • 冗長でもメッセージがわかる
  • できればどのpackageで吐かれたかがわかる
  • formatが大体統一されている

fmtパッケージのformatを駆使する

最低限の要件を満たすために、どういうerrorの書き方が良いのかを検討したところ、おなじみのfmtパッケージをうまく使えば、特にerrorの形式を規約等で決めることなく標準の機能だけでそれらしいエラーを出力できるのでは?と思ったので実際に書いてみました。

formatの出力に関する詳細は以下を参考にしました。

blog.y-yuki.net

上記のエントリーを参考にすると、fmtパッケージで出力する場合

  • %#v -> Go言語のリテラル表現で対象データの情報を埋め込む
  • %T -> errorの型を明示してくれる

というようなルールがあるので、このルールを元に以下のような出力形式を考えます。

errorを発生させるコードは以下です。

package main

import (
    "encoding/json"
    "fmt"
    "os"
    "strings"
)

type User struct {
    ID int64 `json:"id"`
    Name string `json:"name"`
}

func main() {
    j := `{
      "id": 1,
      "name": "taro",
  }`

    r := strings.NewReader(j)

    u := User{}
    if err := json.NewDecoder(r).Decode(&u); err != nil {
        // error出力箇所
        os.Exit(-1)
    }

    fmt.Printf("user: %#v", u)
}

errorの出力内容は以下の用に2パターン考えてみました。
この他にもわかりやすい書式はあると思いますが、最低限欲しい情報のみをまとめた形になります。

fmt.Printf("%#v\n", err) 
//&json.SyntaxError{msg:"invalid character '}' looking for beginning of object key string", Offset:33}

fmt.Printf("Error:%T message: %v", err, err) 
//Error:*json.SyntaxError. message: invalid character '}' looking for beginning of object key stringexit status 255

ここで検討したいのは # を付けるとpackageまで含めてerror messageを標準で出力してくれるということです。
また %T のformatにした場合、errorが起きたpackageのtypeを型として出力してくれるので、これもどこのpackageで出力されたかを一目で理解することが出来ます。

まとめ

golangでerrorを書くときはプロジェクトごとに決まりがあるかと思いますが、もしない場合でerrorの書き方が属人的になってしまっている場合は、golangの標準のformatに準拠してみると、馴染みやすく、また簡潔で、ひと目でなんのエラーか理解できるので使えるなと思いました。

errorはめんどくさいですが多少冗長なくらいでちょうどいいと感じています。

CookieのDomainとPath属性の仕様について調べた話

CookieのDomain属性について調べたのでその備忘録(rfcの該当箇所を抜き出しているだけです。)

調べた背景はCookieを送信する先のDomainとPathについてです。

Domain属性

www.example.com , admin.example.com の用にdomainは同じでも複数のサブドメインでに向けてCookieを送信したい場合のDomainの書き方についてブラウザで見ると .example.com の等に書かれているが、この記法をそのまま使っていいのかわからなかったので調べました。

Domain属性についての邦訳
https://triple-underscore.github.io/RFC6265-ja.html#section-4.1.2.3

rfcではこちら
https://tools.ietf.org/html/rfc6265#section-4.1.2.3

5.2.3における邦訳 cf. https://triple-underscore.github.io/RFC6265-ja.html#section-5.2.3 抜粋は以下

cookie ドメイン :← [ 属性値 の最初の文字 = %x2E( "." ) ならば 属性値 から最初の文字を除いた値 / 他の場合は 属性値 ]
cookie ドメイン ← cookie ドメイン を小文字化した結果
cookie 属性リスト に ( 名前: Domain, 値: cookie ドメイン ) の属性を付加する 

rfcではこちら https://tools.ietf.org/html/rfc6265#section-5.2.3

Domain属性についての規約からわかることはブラウザ上でDomainが .example.com と書かれていても、example.com とサーバー側では指定する

Path属性

上記と同じでドメインまたがりをする場合にCookieを送信できるpathを指定する方法を調べました。

Pathを指定するとどうなるのかというととあるCookieAを example.com に送信するという例で考えたときに

path=/foo

という指定をすると example.com というドメインを持つサイトでは /foo 以下のpathでしかCookieAを使うことはできない。

Cookieを送信できるpathは以下のように分類できます。

/foo => ⭕️

/foo/bar => ⭕️

/foobar => ❎

邦訳はこちら
https://triple-underscore.github.io/RFC6265-ja.html#section-4.1.2.4

邦訳の抜粋ですが、以下の点参考になりました。

所与のホストの中で,パスが異なる cookie を互いに隔離することは、一見 有用であるが,セキュリティは Path 属性に依存できない

pathを分ければセキュアになるわけではないと書いてあります。
(僕は当初pathを厳密に分けるべきだと思ってました。)

Domain跨ぎでCookie送信先を決めるためには Path=/ を指定しておくと www のサブドメインでも admin のサブドメインでも両方でCookieを共有できます。

まとめ

知識があやふやな状態だったので一度ちゃんと見てみました。
HTTPの仕様に困ったらちゃんとrfcまで立ち戻って見返すことが大事です。

structのomitemptyの挙動と使い所の検討

golangのstructでjsonのencodingのためのpropertyに omitempty をつけた時の挙動とその使いどこを検討します。

omitemptyタグとは

https://golang.org/pkg/encoding/json/#Marshal には以下の用に記載されている。

The "omitempty" option specifies that the field should be omitted from the encoding if the field has an emptyvalue, 
defined as false, 0, a nil pointer, a nil interface value, and any empty array, slice, map, or string. 

omitempty属性をつけたstructのjsonのpropertyははその値がfalse, 0, nilのpointer,長さが0の文字列と配列、sliceの時はjsonにencodeされるときに、propertyごと省略される

type Person struct {
  Name string `json:"name"`
  Age  int64 `json:"age,omitempty"` //省略可能
}

上記hのようなstructを考える。

jon := Person {
  Name: "Jon",
  Age: 20
}

// jsonにencodeしたときのoutput
/*
{
  "name": "jon",
  "age" : 20
}
*/

taro := Person {
  Name: "taro",
}

// jsonにencodeしたときのoutput
/*
{
  "name": "taro"
}
*/

jsonにencodeしたときに omitempty 属性をつけておくとstructのinstance化のときに指定したなかったkeyについてはencodeしたjson文字列のpropertyとして含めない(省略される)。
ここまでは一般的なgoのencoding/jsonに関する知識。

ではこのomitemptyを実際のプロジェクトに使うときの注意点について検討する

omitemptyの使い所の検討

使い所はどこなのか。

omitempty 属性を使ういいところは必要ないstructのkeyをjsonにencodeしたときに自動で省略してくれるところであるので、例えば特定のリクエストのクエリパラメーターの値からjsonを生成して、別のAPIにPOSTするような動作を考えたときに、パラメータの有無によって生成するstructを返るなどの手間がいらなくなると思います。

具体的には以下のようなtokenと送信元をクエリパラメーターにタグとしてくっつけて、POSTした先でtokenと送信元をロギングするような挙動があったとします。

curl -i https:sample.com/user/1?token=hogehoge&from=yahoo.co.jp

こんなURLを叩くと以下のようなhandlerにリクエストが入ってくるとします。

package handler

type Logging struct {
  Token string `json: "token"`
  From  string `json: "from, omitempty"`
}

func SenderLogging(w http.ResponseWriter, r *http.Request) {
  values := r.URL.Query()
  token := values.Get("token")
  from := values.Get("from")
  
  // tokenもfromもクエリパラメーターがなければ空文字が入る。
  logging := Logging{
    Token: token,
    From:  from,
  }
  
  b, _ := json.Marshal(&logging)
  
  // encodeされたjsonのbyte配列になる。
}

このとき from が指定されたリクエストの時は from に値が入ったjsonが生成され、from が指定されない(直叩きされた)場合は from のpropertyが省略され、ログにもfrom要素は記録されません。 from の有無を見て、条件分岐等はする必要がありません。

omitemptyをうまく使うと、そこで差分を吸収してくれます。

懸念点

omitemptyの懸念点は上記にも記載しましたが false or "" or 0 or nil pointer などそれ自体が意味を持っていそうな値の場合も omiempty 属性が付いていることでjson化したときにkeyは省略されてしまいます。
つまり、ともすると omitempty 属性をつけていることで意図するjsonが生成されないことが想定されます。

もし、認証のプロセスで必須のパラメータに omitempty をつけていたら、何かの問題で空になってしまい、想定している正しいjsonが作られず、認証が通らない、ということが起きるかもしれません。

特に false or 0 のようなそれ自体が意味を持ちそうな値の時ですら省略されてしまうのは気をつけないと、生成されるつもりだったということが起きかねないので注意が必要です。

まとめ

omitemptyは便利な属性だけど、使い方を見誤ると想定したjsonを生成しないということになるので注意が必要です。
ただし、使い勝手のいい機能なので、ちゃんと使っていきたい。

structに埋め込んだmapの要素を上書きする

やりたいこと

struct内に埋め込んだmapのfieldの値を上書きするという処理を考えます。
下記のようなサンプルコードがあったときに originalSamples のItemsの中身を上書きするような処理です。

type Samples struct {
  Items map[string]string
}

originalSamples := Samples{
  Items: map[string]string{
    "first": "apple",
    "second": "orange",
  },
}

やることはmapの中身を更新するだけなのですが、いくつかハマったのでメモとして残して起きます。

ハマったところ

assignment to entry in nil map が発生した

mapを初期化しなかったケースです。
mapを初期化せずに新規の値をappendしようとすると panic: assignment to entry in nil map のエラーが発生します。

var m Samples

for k, item := range originalSamples.Items{
  item = "iPhone"
  m.Items[k] = item
}

この場合は var で定義するのではなく Sample{} で初期化することが必要になります。

otiai10.hatenablog.com

structを初期化してもassinment to entry in nil map が起きた

structを初期化しても同様のエラーが発生しました。
具体的なコードは以下です。

originalSamples := Samples{
  Items: map[string]string{
    "first":  "pc",
    "second": "book",
  },
}

m := Samples{}

for k, item := range originalSamples.Items {
  item = "iPhone"
  m.Items[k] = item
}

これはなぜ起きるかというと、struct内に埋め込まれているmapはstructをインスタンス化してもnilでになってしまうために assignment to entry in nil map のエラーが発生します。

こちらを回避策を検討した結果、mapをnilしないようにするので下記の2パターンが考えられると思います。

  1. mapをinstace化して後で上書きしたいstructに代入する
  2. structのinstance化と合わせて埋め込まれているmapもinstance化する
// mapをinstance化する
newSamples := Samples{}
m := make(map[string]string)

for k, item := range originalSamples.Items {
  item = "iPhone"
  m[k] = item
}

// structに代入する
newSamples.Items = m
fmt.Printf("%v", newSamples)
// structのinstance化のタイミングでmapも初期化する
newSamples := Samples{
  Items: map[string]string{},
}

for k, item := range originalSamples.Items {
  item = "iPhone"
  newSamples.Items[k] = item
}
fmt.Printf("%v", newSamples)

まとめ

mapを動的に扱いたい場合は、ちゃんと初期化されているか確認する。
structを初期化しても中のpropertyまで初期化されているわけではない。

goで任意引数でtemplateでの表示を変えたりしたい場合の考察

golangで関数に任意引数を取り、その任意引数にmapを指定することで、template場でmapで定義したinterfaceを見て表示を変えたいという実装を考えます。

やりたいこと

以下のようなことをしたいと想定します。

func RendarHTML (flags ...map["string"]interface{}){
  // flags に各種表示を分けるstatusを入れてtemplate側で制御する
  
}

golangにおける任意引数とは?

golangでは、LL言語はじめとしたオブジェクト指向言語にあるようなデフォルト引数という機能を使うことができません。
rubyとかだと

def renderHtml(templ, flag=false)
  # flagを使って何かする
end

# callする側でflagを使わないとき
renderHtml("index.html")

# flagの値を使うとき
renderHtml("detail.html", true)

上記のようにmethodに対してdefatult引数を指定して、その引数はcallする側で指定されなければdefatult引数に与えられた値がmethod内部で使用されるということが可能です。

しかし、golangではこのdefault引数を取ることはできません。
また、静的言語のため、指定した引数は必ず呼び出し側で関数にsetすることが求められます。
setしないとコンパイルが通りません。

// flagでfalseをデフォルトで指定するようなことはできない
func renderHTML(tmpl string, flag bool){ 
  // 何かしらの処理
}

// 呼び出す側

// ◯ コンパイルが通るとき
renderHTML("index.tmpl", false)

// × コンパイルが通らない
renderHTML("detail.tmpl")

// 関数に指定されている引数は満たさなければならない。

ここでgolangで使うのが 任意引数。 言葉通り、呼び出す側でsetしてもいいし、しなくてもいい任意の引数をgolangでは関数に使うことができます。
可変引数が正しい呼称らしいですね

qiita.com

任意引数を使った実装パターンの検討

まず任意引数の仕様として 任意引数をとる場合は必ずsliceとして扱う というのがあります。

func renderHTML(tmpl string, flags ...bool){
    // flagsはslice型。この場合では []book となっている。
}

// 引数をとる
renderHTML("index.tmpl", true)

// 引数をとらない
renderHTML("detail.tmpl")

// どちらでもコンパイルは正常に通ります。

しかし、引数一つでも任意引数をとっている関数内ではsliceから特定の値を取り出さないと一番最初に意図していたようなflagによってtemplateの表示を変更するような実装はできないです。 上記の例では []bool の中からお目当のstatusを持っているかを判別するような実装をすることはしません。

ここで実際に具体的な実装方法を検討します。

任意引数は関数内ではsliceとして扱われるので、flagsという直接boolを扱うことを連想させるような引数をやめて、mapをとります。
このとき関数内の変数名は、追加分なので extra のような変数名にしておくと可読性が高くなると思います。

sample実装

具体的な実装方法として、以下のようなmethodを最初考えます。

fumc renderHTML (tmpl string, extra ...map["string"]interface{}){
  // extraを使って描画する
  // extras は []map(string)interface{}
  
  RenderPage(ctx, "templete/path.tmpl",{"Extra":extra})
}

// こんな感じの引数にする
renderHTML("index.tmpl", map["string"]interface{}{"isIOS": true, "isAndroid": false})

// 引数をとらないことも可能
renderHTML("detail.tmpl")

※ 関数名は適当。

renderHTML内の引数に任意引数を使うことで、メソッドの汎用性が高くなります。
何か余分なデータや追加でデータを持たせないときはextrasに全て突っ込んで仕舞えば問題ないです。

これをtemplate側で使うにはいくつか方法があります。

  • templateでsliceのelementを扱う関数を使う
    • range
    • index
<!-- rangeで一つずつ取り出してloopを回す -->
{{ $ext := range $.Extra }}
{{ end }}

<!-- 到底のindex位置の取得する -->
{{ $ext := index $.Extra 3 }}

しかし、extraがそのまま渡されてきてtemplate側で条件分岐を使ったり、indexを制御したりするのはあまり筋がいいとは言えません。
何より、Extraに紐づく値がなければtemplateのコンパイルエラーが起きる場合もあります。

よりベターな実装方法の検討

任意のsliceで返されるextra引数をそのままとるのではなく、ルールを決めます。

  • 任意引数でとる値は必ず一つ
  • 関数側でslice型になっても、slice型のindex:0が必ずflagsをとることが決まっていれば問題ありません。
  • privateなinterfal関数を用意する

以下のような実装になります。

func renderHTML(tmpl string, extra ...map[string]interface{}){
    ext := extra[0] //先頭に来ることが確定済み
    internalRenderHTML("index.tmpl", ext) 
}

func internalRenderHTML(tmpl string, extra map[string]interface{}){
    // extraを使ってstatusを取り出す
}

任意引数のindex0番が使いたいflagなにで以下のような風に呼び出し側で使います。

renderHTML("index.tmpl", map["string"]interface{}{"IsIOS": true, "IsAndroid": false})

html側で利用するときはindexやreangeの関数を使わずとも、extraはslice型なくmapで受け取れるようにします。

{{ /* iOSモードもとき */ }}
{{ if $.Extra.IsIOS }}
{{ end }}

{{ /* Androidモードのとき */ }}
{{ if $.Extra.IsAndroid }}
{{ end }}

こういった感じでinternalで共通のmethodを呼ぶことで環境ごとにtemplateで使えるようにします。変なelement管理等は必要ないです。

任意引数でslice型をそのまま使うより単体として扱った方が使い勝手がいいですし、templateでもすぐにカンマ区切りで呼び出せるので便利だと思いました。

goのテストのカバレッジを計測する

関わっているプロダクトでテストのカバレッジ取得してみようと思ったので、標準で動作している機能を使ってgoのテストカバレッジを計測してみました。

coverageを計測する

coverageを測定するpackageを指定して、カバレッジを図るオプション -cover を指定します。

$ gotest -cover ./path/to/package_name

## test 実行中

coverage: 45.9% of statements

coverageのパーセンテージが出力されます。
goではpackage単位でカバレッジが計測されます。

テストされている箇所とされてない箇所を視認する

htmlを生成してブラウザで開く

以下の記事を参考にcover.htmlを生成してブラウザで表示させてみました。

qiita.com

GPPATH配下にプロジェクトがある場合は任意のディレクトリにて以下を叩きます。

$ go test -coverprofile=cover.out ./path/to/package
ok      ./path/to/package 0.12s  
coverage: 75.0% of statements
$ go tool cover -html=cover.out -o cover.html
$ open cover.html

ブラウザにテストされている箇所とされていない箇所が表示されます。
されている箇所は緑色で、されてない箇所は赤でコードがハイライトされているので、条件分岐箇所などをみながらどこがテストされていて、どこがテストされていないのかを確認することが可能です。

こういうツールが標準でついてるんだから本当に便利だなと思います。

テスト戦略にも関わってくるので、例えばキャッシュの有無など、動作に影響ないところのコード等はある程度テストを書かずともいいですが、ビジネスロジックに関わるところはちゃんとテストされているかを確認してみてください。

ハマったところ

direnvでプロジェクトごとにGOPATHを指定している場合に少しハマりました。 カバレッジを計測したhtmlファイルをプロジェクトとは別ディレクトリでgo tool cover -htmlをしようとしてもGOPATHが違うと言われて動作しなかったので、direnvを使っている場合は、direnvで指定したGOPATH配下に出力ファイルを作ると正常にhtmlに変換されます。

// デフォルトのGOPATHに移動
$ cd $GOPATH
$ ~/go go tool cover -html=cover.out -o cover.html
cover: can't find "application.go": cannot find package "GAE_PROJECT_GOPATH/model" in any of:
  /usr/local/Cellar/go/1.9.2/libexec/path/to/model (from $GOROOT)
  path/to/model (from $GOPATH)

ただ、gitでトラッキングする際にノイズになるので、 .gitignore~/.gitignore_global でノイズは取り払ってもおくといいかと思います。

『お金2.0』を読んだ

『お金2.0』を読んだので、ちょっとした感想をメモって起きます。

感想

帯やamazonの説明文、レビューコメントを参照した方が内容をざっと俯瞰するにはいいと思うので、ここで詳しくは載せませんが、それなりにこれからの経済のあり方がわからない人向けに丁寧に説明されている書籍でした。
普段から本書で述べられている内容に触れている身としては若干冗長さも感じつつも、改めて丁寧に説明されることで「ああ、確かに」とか「こういうことだったのか」といった知識の再確認やモヤっとしていたところが整理された感覚があり、すごくリズムよく読むことができました。
一方で「評価経済」「トークン/シェアリングエコノミー」に馴染みのない方には、正直何をいっているのか多分わからないのでは?的な感想を抱いてます。
そんなこと考えてたら、著者のTwitterでも似たようなことを呟いてて納得しました。

「お金」っていうキャッチーなフレーズを使っているので、内容が全然お金関係ないじゃんと思ったり、経済の枠組みや、歴史的背景といった内容まで一気に飛躍したりと書籍の中であっちにいったりこっちにいったりを繰り返しているのですが、今までいわば価値の王様として絶対的だった「お金」というものが、これから少しずつ変わってくるのではないか、ということが言いたかったのかなと思います。

これからは「お金」の意味も変わってくるし、国家が管理している経済以外の枠組みで色々な経済が生まれてくるだろうし、その各々の経済でエコシステムが成り立つだろうということは僕自身なんとなく感覚的には感じていました。

正直なところ。本書を読んだからといって、明日から何かが変わるとかということは一切ないと思います。
学術書ではないので、何か専門的な知識が身につくわけでもありません。
ただ、これからの経済を考える一つの観点を提供している書籍だと思います。何か新しい概念が出てきたときに、歴史からみてどうしてそういった概念、サービス、仕組みが現れてくるのか、それを俯瞰して、大きな流れの中の一つの点であるという観点を持つことができるようになるくらいのことだと思います。

お金っていう言葉が非常にキャッチーだし、内容の中でも、これからのお金の考え方そのものが変わってくるという趣旨の内容がメインなので、それにちなんだタイトルになっているのかなとも思いますが、個人的にはは「経済2.0」といってもいいのかもしれないと感じてます。
これからの経済、エコシステムを考えた時に、単一のエコシステムだけではなく、エコシステムにも多様性と、どのエコシステムで生きるのか選ぶ自由が出てくるのであろうという知識なり、一つの世界の見方、観点を持っておくことで、もし本当に本書で述べられているような時代が来たときに、面食らうことなくスムーズに受け入れることができるのではないかなーとかそんなことを感じながら本書を読み終えました。

自分でも不思議なくらい、読み終わった後に色々感が混んでしまってました。
頭の中が整理された一方で、またさらにふわふわした、モワッとした思考を延々繰り返してしまっています。

【学習】「GOならわかるシステムプログラミング」~Chapter 3~

※「GOならわかるシステムプログラミング」の3章の学習記録です。

主に

  • PNGファイルを分析してみる
  • PNGファイルに秘密のテキストを入れてみる

の2節の内容の学習記録です。

sample

sampleコードは以下

package main

import (
    "bytes"
    "encoding/binary"
    "fmt"
    "hash/crc32"
    "io"
    "os"
)

const (
    OFFSET = 8
)

func dumpChunk(chunk io.Reader) {
    var l int32
    binary.Read(chunk, binary.BigEndian, &l)
    // png をbyteに変換した時の入れ物
    buf := make([]byte, 4)
    chunk.Read(buf)
    fmt.Printf("chunk '%v' (%d byte)\n", string(buf), l)
    if bytes.Equal(buf, []byte("tExt")) {
        rawText := make([]byte, l)
        chunk.Read(rawText)
        fmt.Printf("%s\n", string(rawText))
    }
}

func readChunks(file *os.File) []io.Reader {
    // pngをchunkに分けたときのchunkを格納する入れ物
    var chunks []io.Reader

    // pngの最初の8byteを飛ばして9byte目から読み込む
    file.Seek(OFFSET, 0)
    var ofs int64 = OFFSET

    for {
        // data長のいれもの
        var l int32
        err := binary.Read(file, binary.BigEndian, &l)
        if err == io.EOF {
            break
        }

        chunks = append(chunks, io.NewSectionReader(file, ofs, int64(l)+12))

        // 次のchunkの先頭に移動
        ofs, _ = file.Seek(int64(l+8), 1)
    }

    return chunks
}

func textChunk(txt string) io.Reader {
    byteData := []byte(txt)
    var buf bytes.Buffer
    binary.Write(&buf, binary.BigEndian, int32(len(byteData))) // 書き込むためのbufferを確保。長さは入力文字長。
    buf.WriteString("tExt")                                    //bufferに書き込む
    buf.Write(byteData)                                        // 入力対象の文字をbufferに書き込む
    // CRCを計算して追加
    crc := crc32.NewIEEE()
    io.WriteString(crc, "tExt")
    binary.Write(&buf, binary.BigEndian, crc.Sum32()) // crc分を新しくbufferに書き込む
    return &buf                                       // 書き込み終わったbufferを返す
}

func WriteNewTextChunk() {
    // pngをチャンクに分けて読み込み
    file, err := os.Open("./imgs/Lenna.png")
    if err != nil {
        fmt.Printf("error! err: %v", err)
        os.Exit(-1)
    }
    defer file.Close()

    newFile, _ := os.Create("./imgs/result.png")
    defer newFile.Close()

    chunks := readChunks(file)
    // pngのシグニチャー書き込み
    io.WriteString(newFile, "\x89PNG\r\n\x1a\n")

    // 先頭にIHDR chunkを書き込む
    io.Copy(newFile, chunks[0])

    // TextChunk を追加する
    io.Copy(newFile, textChunk("Test Text Chunk"))

    // 残りのチャンクを新しく追加する
    for _, c := range chunks[1:] {
        io.Copy(newFile, c)
    }
}

func main() {
    // 私いchunkを書き込んだfileを生成する
    WriteNewTextChunk()

    file, err := os.Open("./imgs/result.png")
    if err != nil {
        fmt.Printf("error! err: %v", err)
        os.Exit(-1)
    }

    chunks := readChunks(file)
    for _, c := range chunks {
        dumpChunk(c)
    }
}

githubはこちら

github.com

TOPIC

エンディアン変換について

エンディアン変換(以下 Byte Swapping)がgoには標準で装備されています。
CPUはデータの先頭には小さい桁から格納されるリトルエンディアンが採用されていますが、ネットワーク(TCP/IPプロトコル)ではデータの先頭に大きな桁から格納されるビックエンディアンが採用されています。

そのため、ビックエンディアンで格納されているネットワークで受け取ったデータをリトルエンディアンに修正する必要があります。

それが何度か出てくる以下の部分

binary.Read(r io.Reader, binary.BigEndian, data interface{})

TCP/IPプロトコルではデータは BigEndian なので、rは BigEndian で格納されているので、binary.Read するときにorderに BigEndian を指定することで、そのorderでio.Readerからデータを読み込み、dataの中に格納し直します。格納するときはリトルエンディアンに修正して格納されます。

binaryパッケージ

encoding/binary についてのメモです。

binary.Read

encoding/binary パッケージ - golang.jp

Read関数

func Read(r io.Reader, order ByteOrder, data interface{}) os.Error Readは、構造化されたバイナリデータをrから読み込みます。データは固定サイズの値へのポインタ、または固定サイズの値のスライスでなければいけません。

バイナリかされたio.Reader型のデータから、任意のdata(interface型)へ変換する

binary.Write

encoding/binary パッケージ - golang.jp

func Write(w io.Writer, order ByteOrder, data interface{}) os.Error Writeは、wへdataのバイナリ表現を書き込みます。データは固定サイズの値、または固定サイズの値へのポインタでなければいけません。

data := "Hello World"
var buf bytes.Buffer
binary.Write(&buf, binary.BigEndian, int32(len(data))) // dataサイズの長さをbufに書き込む
buf.Write([]byte(data)) // 確保したbufferにdataの内容を書き込む

binary表現の書き込みがどう行われているのか、知る機械になってよかった。こういったことはシステムプログラミングをしながらわかることなので、引き続き読み進めようと思います。

json.Unmarshalではnilの参照型へのmappingはできない

httpレスポンスを構造体にmappingする際に以下のようなコードを書くケースは多々あると思います。 ※ error handlingは割愛

// responseの取得
resp, _ := http.Get(url)
body := resp.Body
defer body.Close()

// []byteへの変換
b, _ := ioutil.ReadAll(body)

var data *Data

// Data structへのjsonのmapping
if err := json.Unmarshal(b, data); err != nil{
    panic(err)
}

しかしこのコードは一見正しいように見えますが、期待する動作はしません。
以下のようなerrorが発生します。

json: Unmarshal(nil *model.Data)

参照型でもnilの構造体にはUnmarshal(復元)して構造体へのmappingはできないというエラーです。

実は json.Unmarshal(src, dist) において dist にとるべきinterface型の変数はnilではいけません。
nilではなく空の参照型の構造体を指定する必要があります。
そのため、上記のコードは以下のように書き換える必要があります。

// responseの取得
resp, _ := http.Get(url)
body := resp.Body
defer body.Close()

// []byteへの変換
b, _ := ioutil.ReadAll(body)

// ここで空の構造体を指定する。
data := &Data{}

// Data structへのjsonのmapping
if err := json.Unmarshal(b, data); err != nil{
    panic(err)
}

data := &Data{} とすることで json.Unmarshal でstrcutにmapping可能になります。

なぜこのような挙動になるかを調べました。

json.Unmarshalの公式ドキュメントによると

Unmarshal parses the JSON-encoded data and stores the result in the value pointed to by v. If v is nil or not a pointer, Unmarshal returns an InvalidUnmarshalError.

と冒頭に記述されており、 If v is nil or not a pointer, Unmarshal returns an InvalidUnmarshalError にある通り、「nilの参照型」or 「参照型でない」場合はErrorを返すことが公式に書かれています。

つまり、答えはまんま「nilの参照型は取れない」ということで完結します。

json.Unmarshal(src, dist) において、nilの参照型を取れないことについては以下のstackoverflowも参考になると思います。

Why does json.Unmarshal work with reference but not pointer?

エントリーの中で記述されている

To unmarshal JSON into a pointer, Unmarshal first handles the case of the JSON being the JSON literal null. In that case, Unmarshal sets the pointer to nil. Otherwise, Unmarshal unmarshals the JSON into the value pointed at by the pointer. If the pointer is nil, Unmarshal allocates a new value for it to point to.

の中で、実際にUnmarshalメソッドがjsonを参照型に復元するときの挙動について記載してあって、これによると、unmarshalメソッドは復元プロセスの中で参照型をnilにする、しかし、復元対象の格納先の参照型の変数がすでにnilの場合、nilnilにするという挙動が発生し、そのためにerrorが発生するのだという解釈をしました。

追記

jsonのUnmarshalについてはブログの以下のエントリでも追記してます。

ema-hiro.hatenablog.com

ema-hiro.hatenablog.com

2018年 ~抱負~

エンジニアとしてコミットしたと言えるプロダクトをつくる

コミットしそこねたプロダクト、コミットしたくてもできなかったプロダクト、いままでのキャリアで「これをしてきた!作ってきた!」と言える仕事に巡り合ってこなかったので、今年こそはこの目標を達成したい。
まずは今仕事で関わっているサービスにコミットすること、エンジニアとして技術でコミットすることから逃げないこと。
そして、「ただ作ること」から「よく作ること」へ自分自身の価値観のスタンダードをあげていきたいと思っている。

月2冊以上の技術書を読む

技術書を読むことは引き続き継続していきたい。
書籍の種類や内容によっては時間がかかるものがあるかもしれないけれど、今年はとにかく量を読むことに挑戦したい。
書いてあること全てを覚えなくてもいいので、量をこなして、知識のindexを色々な所に張り巡らせる1年にして行きたいと思っている。

qiita.com

githubへの1commit/weekをしていくこと

テーマ、規模は問わず、週一以上はgithubに草生やすことを目標にしたい。これの延長で何かサービスを作れたらと思っている(けどそこまで自分に期待するのはやめておく。)

github.com

新しいサービスは積極的に使っていく。

引き続き新しいサービスは積極的に触っていきたい所存。
そろそろお金がかかるサービスであっても積極的に使っていくようにしたい。これについては service safari にはいつもお世話になっているので、今年もよろしくお願いします。

www.service-safari.com

新しい言語を一つ仕事レベルで習得すること

去年はGolangを習得したので、今年も何かを実用レベルで習得したい。
今の所チェックしているのはnode。あまり触れてこなかったけど、習得していると今後サーバーサイドだけでなく、フロントエンドや、BFFといった設計にも役立ちそうななので、今年はぜひ習得したいと思っている。

とりあえず思いつく限りではこんな感じ。 技術以外では

  • 自炊のレパートリーを増やすこと
  • ブログを引き続き継続して書くこと
  • 資産運用や税制といったファイナンシャルの教養を身につけること

このあたりをテーマにやっていきたいと思う。

今年もよろしくお願いしますmm

2017年 ~振り返り~

一年も終わりなので今年の振り返りをします。
振り返り観点は以下

  • スキルスタック
  • 個人的トピック

技術スタック (個人的達成度)

Skill Commit Comment
Rails 4からあげる時に1から色々触ってました
合わせてrubyも1からいろんな書籍、資料を読んで学んだけど、いまいちrailsのエコシステムそのものには馴染めないなーと思いました。
Swift iOSとはいえ、スマホアプリ開発を理解できるようになったことはポジティブに技術の幅が広がってよかった。
とはいえ、実践で使うにはまだ稚拙...。
swift自体は非常に開発しやすくていい言語
関数型っぽいパラダイムにはいまいち馴染めないなと感じました。
Golang 今年のメイントピックの一つ
部署を異動して、Goでの開発を始めましたが、シンプルな言語仕様に加えて、エコシステムが出来上がっているので非常に馴染みやすかったです。
web系と言われるLL言語を使ってた時に感じていた課題感を大体クリアすることができていて、web系の新しいスタンダードになると思う言語だと実感しました。
ライブラリを改めて読むくせがついたことが今年の最大の進歩だと思います。(これは開発環境をIDEに移行したことも大きく寄与していると思います。)
GAE Goと並んで今年覚えたトピックの一つ。
インフラを管理しなくていい世界になったことに単純に感動しました。
このブラックボックスを毛嫌いする人はもちろんいそうだけど、個人的にはこれで十分なんじゃないかと思います。
大規模なプロジェクトでも採用実績はあるわけだし、これはインフラの第一選択肢になってしかるべきツールだと思いました。
GAEに付随して、Datastoreだったり、TaskQueueだったり、GAEの中のサービスもいくつか業務で使いながら理解することができました。
NoSQLのデータ設計はRDBMSの時とは全く違っていて、最初はとっつきづらかったです。
まだまだ深く触っていないツールもありますが、いわゆるフルマネージドな環境を使うことに慣れることができました。
API開発全般 json-schemaだったりAPIのリソース設計だったりといったことに本格的に触れました。
今までも経験してきたことではありますが、プラットフォーム開発をする上で必要となること(主に開発以外の部分ですが)を経験できたのは業務の幅が広がったと思います。

個人的トピック

好きな言語ができたこと

まだまだ経験が浅く、自分のエンジニアとしてのキャリアそのものに自信がなかったので、「言語や技術は問わない」ことを強みと思ってましたが、Goを書き始めて今まで感じていた課題感を全て真っ正面から解決されてしまい、ずっぽりこの言語にはまってしまいました。
言語にこだわりがあること自体はネガティブな捉え方をされることがしばしばありますが、もし、好きな技術を選択できるならGoを選びたいですし、働くならGoを書いて働きたいと思えるようになったは、進歩なのかもしれません。
(もちろん技術にこだわりがないことに変わりはありませんが、できることならGo書きたいなと思っています。)

個人でも何か作るときはほとんどGoを選んで使うことが多いです。

プラットフォーム開発を経験できた(現在進行形でしている)こと

プラットフォーム戦略的なことはよく聞くし、そこを目指しているサービスは星の数ほどあると思います。
今までも「〇〇のプラットフォームになる!」みたいな言葉をたくさん聞いてきたし、それは前職でも変わらずみんな言いたがることではあったと思ってます。
ただ、一開発者として、いざそれを作ろうと思って、「それ、どう作るの?」、「何が必要なの?」、みたいなことから「何をもってプラットフォームなのか?」みたいなことを全く知らなかったことに気づき、その中でプラットフォームの開発に関われたのは個人的にはすごくよかったです。
(今年発売されたこの本が非常に参考になったのは言うまでもありません。。。)

1サービスを開発しているときとは比べものにならないほどのステークホルダーの数と、調整、資料準備、コミュニケーションの数々、最初は面食らいましたが、でもこれらは全て自分がエンジニアのキャリアの中でしてこなかったことであり、今ここで身につけるべきキャリアなのかなと思って、技術以外のこともかなり時間を使っていました。

特にめちゃめちゃコンフル書いてました。ここ半年考えたら wiki:コード = 6:4くらいの比率なんじゃないかと思ってます。
多分これからもこれは続いていくんだと思います。

綺麗なコンフルの書き方とかすごい勉強しましたし、自身の文章のわかりづらさとかトンマナの不揃い感とか身にしみました。。。

ちゃんとブログ書くようになったこと

これは上に記載している、wiki力をあげる一環でちゃんと書くようにしました。
コード書く力も求められますが、文章書く力が実は地味に必要であることに、今年途中くらいから気づき始めたので、人の目に触れるものを書く練習しようと思ったのがことの発端です。

まずは、内容よりも書くことを習慣づけるために、個人の備忘録とか、ちょっと触って見たツールとかちまちま書いてました。
この傾向はしばらく続くだろうと思います笑。

ただ、アウトプットし始めたら、副産物でgithub整備するようになったし、本だけでなく、海外の記事等も時間見つけて読むように最近なってきたので、実はいい循環じゃないかと思っています。
あと、今更ですが、タイピング速度が少し上がりました。これは資料作成においては単純な効率UPに繋がってます笑。

まずは書くこと、ちゃんとした文章を素早く書くことができることを念頭においているので、あまり書いてて疲れないです。
来年はもう少しちゃんと質の高いエントリにも挑戦したいです。

総括

年度始めにアプリ方面に関わっていきたいという目標を立てたけど、結果的にはどっぷりサーバーサイド沼に浸かった1年でした。(年度って書いてあるからちょっと時期ずれます...)

ema-hiro.hatenablog.com

とにかくたくさんのことがあった1年だったし、キャリアとしては現在の開発チームに入ってからエンジニアというものを1から学び直している実感があって、しんどいことがほぼ大半でした。
3年間エンジニアを続けてきたけど、まだまだ自分なんてひよっこの部類で、しないといけないこと、できるようにならないといけないことがたくさんあって、それが実は技術に寄らないことまで広がっていて、本当に「ものつくり」は奥が深いと実感している毎日です。
技術の奥深さ、サーバーサイドの奥深さ、プラットフォーム開発の奥深さ、そしてなによりチームでのもの作りの面白さ、しんどさを痛感したので、来年はもう少し苦労しようかなと思います。