emahiro/b.log

Drastically Repeat Yourself !!!!

【go】FWに頼らないオレオレroutingを実装する

goでFWに頼らず、net/http だけで簡単なWeb Serverを立てたいと思ったので作ってみた。

routing設定

まず躓いたのはroutingをどうするかということ。
net/httpパッケージを使用する場合、全てmain.goにroutingを書いてしまうことになりますが、FWの構造に則ってここはhttpリクエストをやりとりする箇所はちゃんとhandlerとしてディレクトリを用意したいと考えました。

(ginやechoを使っていたときはFW側で用意されたroutingをよしなに使ってましたが、いざ自分でroutingをかんがえると???ってなるものだなーと感じるなど(笑))

routingを設定する場合 www.gorillatoolkit.org github.com

上記のパッケージを使うのが一般的らしいということはわかっので、今回は gorilla/mux を使用。

gorilla/muxのinstall

$ dep ensure -add github.com/gorilla/mux
$ dep ensure

ディレクトリ構成

$GOPATH
  - src
    - project_root
      - main.go
      - handler/
        - main.go // ここに各種handlerを書いていく

実装

main.go

package main

import (
    "gothub/handler"

    "fmt"
    "net/http"

    "github.com/gorilla/mux"
    "github.com/labstack/gommon/log"
)

const port = "8080"

func main() {
    router := mux.NewRouter()
    router.HandleFunc("/", handler.Top)
    router.Handle("/", router)
    if err := http.ListenAndServe(fmt.Sprintf(":%s", port), router); err != nil {
        log.Fatal("err: %v", err)
    }
}

handler/main.go

package handler

import (
    "net/http"

    "fmt"

    "github.com/labstack/gommon/log"
)

func Top(w http.ResponseWriter, r *http.Request) {
    log.Debugf("request url: %s", r.URL)
    w.WriteHeader(http.StatusOK)
    fmt.Printf("%v", r.Body)
}

handlerに設定するメソッドには http.Resposewriterhttp.Request を引数に持たせることでhandlerとして登録してrequestを処理できるようになる。

動作させる

$ go run main.go

// 別shellで
$ curl -i http://localhost:8080
HTTP/1.1 200 OK
Date: Sat, 04 Nov 2017 09:27:35 GMT
Content-Length: 0
Content-Type: text/plain; charset=utf-8

ちゃんとroutingが通っていることを確認

まとめ

goを使って簡易的なapiクライアントを作りたかったのですが、わざわざFWを使うほどでもないので自分で超薄いFWを作れたらなくらいのノリで書いてみましたら、結構簡単に作ることが出来ました。
FWを使っててのツラミとかはこのあたりのスライドにも書いてあるので標準の net/http パッケージを基準にapiサーバーとか構築するのに便利だと思いました。

www.slideshare.net

depを使ってみる

golangのオフィシャル謹製パッケージマネージャ「dep

github.com

個人プロジェクトではglideから乗り換えました。

install

公式の手順に則っておけば問題ないです。

Macでは brew でインストール出来ます。

$ brew install dep

ディレクトリ構成

- $GOPATH
  - src/
    - project/

dep$GOPATH/src 以下にプロジェクトのソースを管理するディレクトリを作成しておかないと動作しません。

使い方

$ cd $GOPATH/src/project
$ dep init
// Gopkg.tomlとGopkg.lockが作成されます。

// add
$ dep ensure -add package-src-URL
$ dep ensure

help

$  dep help
dep is a tool for managing dependencies for Go projects

Usage: dep <command>

Commands:

  init     Initialize a new project with manifest and lock files
  status   Report the status of the project's dependencies
  ensure   Ensure a dependency is safely vendored in the project
  prune    Prune the vendor tree of unused packages
  version  Show the dep version information

Examples:
  dep init                               set up a new project
  dep ensure                             install the project's dependencies
  dep ensure -update                     update the locked versions of all dependencies
  dep ensure -add github.com/pkg/errors  add a dependency to the project

Use "dep help [command]" for more information about a command.

//ensureのhelpの場合
$ dep help ensure
Usage: dep ensure [-update | -add] [-no-vendor | -vendor-only] [-dry-run] [<spec>...]

Project spec:

  <import path>[:alt source URL][@<constraint>]


Ensure gets a project into a complete, reproducible, and likely compilable state:

  * All non-stdlib imports are fulfilled
  * All rules in Gopkg.toml are respected
  * Gopkg.lock records precise versions for all dependencies
  * vendor/ is populated according to Gopkg.lock

Ensure has fast techniques to determine that some of these steps may be
unnecessary. If that determination is made, ensure may skip some steps. Flags
may be passed to bypass these checks; -vendor-only will allow an out-of-date
Gopkg.lock to populate vendor/, and -no-vendor will update Gopkg.lock (if
needed), but never touch vendor/.

The effect of passing project spec arguments varies slightly depending on the
combination of flags that are passed.


Examples:

  dep ensure                                 Populate vendor from existing Gopkg.toml and Gopkg.lock
  dep ensure -add github.com/pkg/foo         Introduce a named dependency at its newest version
  dep ensure -add github.com/pkg/foo@^1.0.1  Introduce a named dependency with a particular constraint

For more detailed usage examples, see dep ensure -examples.

Flags:

  -add          add new dependencies, or populate Gopkg.toml with constraints for existing dependencies (default: false)
  -dry-run      only report the changes that would be made (default: false)
  -examples     print detailed usage examples (default: false)
  -no-vendor    update Gopkg.lock (if needed), but do not update vendor/ (default: false)
  -update       update the named dependencies (or all, if none are named) in Gopkg.lock to the latest allowed by Gopkg.toml (default: false)
  -v            enable verbose logging (default: false)
  -vendor-only  populate vendor/ from Gopkg.lock without updating it first (default: false)

こんな感じ。オフィシャル謹製だし、今後はこちらを使っていこう。 refs: mattn.kaoriya.net

goのsortで複数条件でのsortを実現する上で考えたこと

サマリ

特定のModelに対して複数条件でsortしたい場合の実装方法についての考察

実装方法

  • sortしたいfield毎にモデルにsortのinterfaceを実装をする - ➀
  • embedで親のsortパッケージを継承する(sort条件を上書きする) - ➁

➀のとき

sortしたいStructのfield毎にLen,Swap,Lessのinterfaceを実装する。
古典的な実装方法で基本Structのfieldごとにsortで必要となるinterface3つを実装すればよい。

type Person struct{
  Name
  Age
}

type PeopleByName []Person

func (p PeopleByName)Len int{
  return len(p)
}

func (p PeopleByName)Swap(i, j int) {
  p[i], p[j] = p[j], p[i]
}

// Nameでsortする場合
func (p PeopleByName)Less (i, j int) {
  return p[i].Name < p[j].Name
}

func (p People)SortByName(){
  sort.Sort(PeopleByName(p))
}

type PeopleByAge []Person

func (p PeopleByAge)Len int{
  return len(p)
}

func (p PeopleByAge)Swap(i, j int) {
  p[i], p[j] = p[j], p[i]
}

// Ageでsortする場合
func (p PeopleByAge)Less (i, j int)  bool {
  return p[i].Age < p[j].Age
}

func (p People) SortByAge (){
  sort.Sort(PeopleByAge(p))
}

func main (){
  p := []Person{
  Person{
    Name: "Taro",
    Age: 1,
  },
  Person{
    Name: "Jiro",
    Age: 2,
  },
  Person{
    Name: "Saburo",
    Age: 3
  }
  
  // 名前でSort
  p.SortByName() // Jiro, Saburo, Taro
  // 年齢でsort
  p.SortByAge() // 1, 2, 3

}

古典的でわかりやすい反面、使わないinterfaceも定義してしまい、コード全体の見通しが悪い。

②の場合

ベースとなるStructにつき一度sortのinterface3つを実装する。
複数条件でsortする場合は、ベースとなるStructを埋め込んだ別のStructを作って、その新しく条件を追加するために作成したStructに必要なinterfaceのみを追加する。

ベースとなるStructを埋め込んだ、新しいStructではsortのinterfaceを全て実装しなくても、大本のStrcutでsortが実装されていれば、全て実装する必要はない。
必要となるinterfaceのみ実装すれば、sortがcallされる時にsort条件が上書きされる。
※ 継承みたいなイメージ

type Person struct{
  Name
  Age
}

type People []Person

func (p People)Len int{
  return len(p)
}

func (p People)Swap(i, j int) {
  p[i], p[j] = p[j], p[i]
}

// Nameでsortする場合
func (p People)Less (i, j int) bool {
  return p[i].Name < p[j].Name
}

最初にベースとなるPeople StructにNameでsortするための基準となるsortのinterfaceを定義しておきます。
次にAgeでsortするために、Ageでsortする場合はNameでsortするために定義したLessを上書きします。

type PeopleByAge struct{
  People 
}

func (b PeopleByAge) Less (i, j int) bool {
  return b.People[i].Age < b.People[j].Age // ここでPeopleのLessを上書きしている。
}

func main (){
  p := []Person{
  Person{
    Name: "Taro",
    Age: 1,
  },
  Person{
    Name: "Jiro",
    Age: 2,
  },
  Person{
    Name: "Saburo",
    Age: 3
  }
  
  // 名前でSort
  sort.Sort(p)
  // AgeでSort
  sort.Sort(PeopleByAge{p})
}
type PeopleByAge struct{
  People 
}

の箇所でPeople Structを埋め込んでいるので、PeopleByAgeに新しく3つ全てのinterfaceを実装しなくても良くなります。

考察

古典的な方法はわかりやすい、かつ実装しやすいため基本➀で実装するのをベースに置きつつ、埋め込み型の方がよりsort条件を明確にでき、かつコードの記述量もすくないと思うので、使えるならStructに埋め込むパターンの方がいい気がする。

一方でsortについては➁の実装方法はベースのStructに依存することになるので、もしかしたら改修のときなどに予期せぬ影響が出る可能性がある。

refs

https://qiita.com/Jxck_/items/fb829b818aac5b5f54f7

goでprivateメソッドをテストする

サマリ

go でのprivate methodをテストでcallする方法

テスト手順

goでprivateメソッドをテストファイルからcallする場合には、同一package内においてはファイルが異なっても、同じpackage内に存在するprivate methodを呼ぶことができる というgoの仕様を利用します。
goではrailsrspecのようにテストをする場合に、専用のspecディレクトリを用意してもいいですが、private methodのテスト等を考慮した場合に、同一package内にテストファイルも配置することが多いです。

例えば以下のようなpackage構成になります

model
   user.go
   user_test.go

※ テストファイルへの物理的な移動距離が少なくて僕はこのテストの構成は非常に開発者に優しいと思っています。

実際にテストを書いてみます。

今回つかってリポジトリはこちら

github.com

ディレクトリ構成は以下のようにしました。

src
   main.go
   test
       sample.go
       sample_test.go

実際にテストファイルを書いてみます。

// sample.go
package test

import "fmt"

func PublicMethod(){
    fmt.Printf("public method: %s \n", "")
}

func privateMethod(){
    fmt.Printf("private method: %s \n", "")
}
// sample_test.go
package test

import "testing"

func TestPublicMethod(t *testing.T) {
    t.Logf("test public method","")
    PublicMethod()
}

func TestPrivateMethod(t *testing.T){
    t.Logf("test private method","")
    privateMethod()
}

実際にテストを動かしてみます。

$ go test -run ./test
public method:
private method:
PASS
ok      test    0.006s

package test内であればprivateメソッドでもcall出来ます。
こうすればprivateメソッドを簡単にテストすることが出来ます。

まとめ

goのprivateメソッドをcallするところで少しハマったので、備忘録に簡単にまとめました。

(続)goでのhttpの書き方あれこれ

以前書いたこれの続き

ema-hiro.hatenablog.com

以前書いたgoでのhttpの書き方の内容の続編です。
Postの書き方とAutorizationヘッダーを付けたリクエストを構築するときの方法を備忘録としてまとめました。

標準パッケージはこちら

https://golang.org/pkg/net/http/

Goのhttpを使う

  • http.Post(method, url, data)
  • http.PostForm(url, data)
  • Client{} 型を使う

の三通りの書き方があります。

http.Postを使う

jsonFile, err := os.Open("data.json")
if err != nil {
    // error
}

resp, err := http.Post("sample.com", "application/json", jsonFile)

http.PostFormを使う

val := url.Values{}
val.Add("key", "value")

resp, err := http.PostForm("sample.com", val)

Client型を使う

client := http.Clietn{}

val := url.Values{}
val.Add("key", "value")

req, err := http.NewRequest("POST", "sample.com", strings.NewReader(val.Encode()))
if err != nil{
    // request create error
}

resp, err := client.Do(req)

GAEを使う

GAE上でhttpリクエストを使うときは以前記載した内容と同じで urlfetch(appengine.context) を使います。
内容は標準のパッケージのClient型に近い。

Postを使う

// r は *http.Request
ctx := appengine.NewContext(r) // GAEのContextを作成する
client := urlfetch.Client(ctx)

jsonFile, err := os.Open("data.json")
if err != nil {
    // error
}


resp, err := client.Post("POST", "sample.com", jsonFile)

PostFormを使う

// r は *http.Request
ctx := appengine.NewContext(r) // GAEのContextを作成する
client := urlfetch.Client(ctx)


val := url.Values{}
val.Add("key", "value")

resp, err := client.PostForm("sample.com", val)

client.Do型を使う

// r は *http.Request
ctx := appengine.NewContext(r) // GAEのContextを作成する
client := urlfetch.Client(ctx)

val := url.Values{}
val.Add("key", "value")
req, err := http.NewRequest("POST", "sample.com", http.NewReader(val.Encode())
if err != nil {
    // request create error
}

resp, err := client.Do(req)

Header情報を追加する

Header情報を付加するには標準パッケージ、GAE、どちらにしてもClient型を使うことになります。

http.Packageのとき

req, err := http.NewRequest("Post", "sapmple.com", url.Values{})
if err != nil{
     // request create error
}
req.Header.Set("ContentType", "application/json")
req.Header.Set("Authorization", "XXXXXXXX")

// httpパッケージを使う場合
client := http.Client{}
resp, err := client.Do(req)

GAEのとき

req, err := http.NewRequest("Post", "sapmple.com", url.Values{})
if err != nil{
     // request create error
}
req.Header.Set("ContentType", "application/json")
req.Header.Set("Authorization", "XXXXXXXX")

// GAEを使う時
// r は *http.Request
ctx := appengine.NewContext(r)
client := urlfetch.Client(crx)
resp, err := client.Do(req)

Header情報を付与するときは、予め func NewRequest(method, url string, body io.Reader) (*Request, error)http.Request 型を生成しておかなければなりません。
その Request 型に対してHeader情報を付与していくことになります。

Refs

http - The Go Programming Language

qiita.com

cURL as DSL — cURL as DSL 1.0 documentation

AbemaTV Developer Conference に参加してきました。

ちょっと遅刻してしまったけど、21日にAbemaTV Developer Conferenceに参加してきました。 (ほとんど個人のメモです。)

感想

先に感想だけ書いておきます。
※ 僕向けのメモよりスライド探したほうがわかりやすいと思うので(笑)

感想としては、 Abemaのモニタリング、負荷対策の仕組みがマジですごかったこ
AbemaTVのような大規模サービスをどう監視しているのかに、興味があったので、モニタリング関連のセッションは非常に勉強になりました。
特に、セッション中にOSS化されたpromvizはデモ含めてすごくUIがめちゃめちゃイケてました。

github.com

いやほんと超かっこいい。
AbemaTVはPrometheusを使ってたんですね。

developers.cyberagent.co.jp

iOSアプリもデイリーで7~8個PRマージしてるとか、開発もすごい活発に行われている印象を受けました。
AbemaTVといえば何度かダウンしたこともあったけど、その時のことも語られててました。

来月も「72時間ホンネテレビ」というどでかいトラフィックが来るイベントがあるので是非、来年のConferenceでそのあたりについても話が聞きたいなーと思います。

以下自分向けのメモ

iOS開発

  • 1monthで162個のmerge。
  • 営業日換算で7~8個のPRがマージされている。

毎日大量のコードが変更されいる。

  • 開発フローはスクラム
  • 開発とQAの期間の重複があると厳しい
  • QA期間中にフライングする開発も可能
  • 長期間に渡る開発はスプリントをまたぐ

タスクにはストーリーポイントを付けている。 * 大雑把な付け方をしているけれども。

優先度判断については共通言語、共通指標を持っている * 優先度を決める時に、非エンジニアとのコミュニケーションも円滑になる。

レビューはしっかりルール化されている。 * レビューは活発な文化

デザインについて

iOSアプリをAndroidに寄せている * bottom sheetとか * スマホの大画面化に合わせて、全画面モーダルは避けれる傾向にある。 * マテリアルデザインのデザインシステムは非常に優れている。

Web * モバイルページでのアプリDL率向上について * twitterからの流入率は高い。しかし、twitterから来たユーザーのDL率が低い * twitterから流入したユーザーのDLを上げた * ちょっとtwitterからのLPを変更したくらい * エンジニアの工数をほとんど使わずに大きな効果を出せた事例

  • UI変更に抵抗はない
  • UIが急に変わっても、時間が経って文句を言い続けている人はいない
  • ユーザーは慣れる、変更による使いやすさの恩恵も受ける
  • 変化を恐れることは悪

abemaTVにおけるモニタリング

モニタリングシステムの進化 * 最初半年くらいはstackdriver * abemaはほぼすべてのレイヤーでモニタリングしたい * Prometheus + grafana * kubernetsと親和性が高い * PromQLが使いやすい * sqlライク * Exporterを作りやすい * AlertManagerがある

  • stackdriverのモニタリングもPrometheusで監視している。

クラスタ全体のトラフィックの状態がわかりにくい * Prometheusのデータからデータを可視化したい。

speakerdeck.com

Microservices下におけるWebの負荷対策

  • 負荷対策
    • 亀田興毅でwebが死んだ
    • 藤井四段でwebが死んだ

webのシステム構成

  • nginx + node + grpcで構成

webのサーバーリクエストをどうスケーリングさせるのか?

  • LBレベルも通らないキャッシュが必要なのでは?
  • CDNが必要になってくるのではないか?
    • GCPは東京リージョンがあるよ
  • キャッシュミス
    • レスポンシブが合わない
    • PCとスマホで出し分けが存在する
  • フロントエンドエンジニアができることは限りがある。
    • CDNからキャッシュが返ってくればいい。
    • GoogleCloudCDNを活用
    • CDNがキャッシュしたら、nginxのassets配信は必要なくなるのでは?
      • しかし、将来的BFF化を考える、サーバーサイドレンダリングが必要になったタイミングでnginxは必要なのでは?
  • 負荷対策はパフォーマンス改善につながる
    • 負荷対策は運用時の登竜門

MPEG-DASHで行うリニア配信

  • MPEG-DASHとは
    • 会場の方で動画関連の方が使っているのは大体HLS

MPD

  • media presentation description
    • mediaで定義されているメディアに関する情報を記述するxml
    • type
      • dynamic -> 定期的に取りにいく。生放送に使われる
      • static -> 取りに行かない。

abemaTVにおけるリニア配信技術とは?

  • リニア配信とは
    • 予め決められた番組表に則って配信を続ける方法
  • mpeg-dashをどうしてabemaで対応したか?
    • DRM技術に対応したコンテンツ配信を行う必要性
    • DRMはデバイス毎に異なる
  • abemaTVではマニフェストファイルを逐一作る必要がある。
    • ts -> mp4 -> init.m4s/ medeia.m4s の2つを作る。
    • 生放送のCMがどこに入るのかわからない
      • tsが知っている。

配信品質管理

  • トラブルが起きる箇所
    • 配信トラブルをいち早く検知するのは?

AbemaTVの裏側

  • 負荷試験について
    • ヒット企画が多いが、
      • 負荷対策の試練
  • 負荷試験
    • Locus
      • python
      • 分散型ユーザー負荷テストツール
      • GKEと相性がいい
      • ユーザの綿密なシナリオはLocusで記述可能
      • python -> golangに変更したい
    • wrk
      • Lua
      • 大規模な負荷を発生させることが可能
      • abコマンドとかに触ったことがある人には手に馴染みやすい

Referense

developers.cyberagent.co.jp

webpack + typescript

なぜtypescript?

MSが作ってGoogleが採用したので、フロントエンド界隈でtypescriptがしばらく積極的に使われて来そうな気が予感がしたのと、そろそろフロントエンドのキャッチアップしとかないとなーと感じ始めていたので...

typescriptの導入

typescriptをwebpackでコンパイルできるようにします。 https://webpack.js.org/guides/typescript/ を参考にして導入しました。

yarnでinstall

$ yarn add -D typescript ts-loader inline-source-map

ts-loader
refs: https://github.com/TypeStrong/ts-loader

tsの設定

tsconfig.jsonを作成する

$ touch tsconfig.json
$ ls -la
drwxr-xr-x    - user 14 10  0:56 .idea
drwxr-xr-x    - user 14 10  0:56 dist
drwxr-xr-x    - user 14 10  1:28 node_modules
.rw-r--r--  640 user 14 10  1:28 package.json
drwxr-xr-x    - user 14 10  0:54 src
.rw-r--r--    0 user 14 10  1:29 tsconfig.json
.rw-r--r--  857 user 14 10  0:56 webpack.config.js
.rw-r--r-- 142k user 14 10  1:28 yarn.lock

tsconfig.jsonにtypescriptをes5にコンパイルする設定をカリカリ書いていく。
今回は最初というのもあり、webpackのtypescript導入に記載してあるjsonファイルをそのまま使う。

json内でjsxを使っているが今回は使っていない

{
    "compilerOptions": {
        "outDir": "./dist/",
        "noImplicitAny": true,
        "module": "commonjs",
        "sourceMap": true,
        "target": "es5",
        "jsx": "react",
        "allowJs": true
    }
}

webpack.config.js

以下の用にts-loaderを入れてtypescriptをコンパイルできるようにする

const src = __dirname + "/src";
const dist = __dirname + "/dist"

var webpack = require('webpack');
const UglifyJSPlugin = require('uglifyjs-webpack-plugin')

module.exports = {
    context: src,
    entry: "./ts/index.ts",
    devtool:"inline-source-map",
    module: {
        rules:[
            {
                test: /\.html$/,
                loader: "html-loader"
            },
            {
                test:/\.tsx?$/,
                loader: "ts-loader",
                exclude: /node_modules/
            },
        ]
    },
    resolve: {
        extensions:[".js", ".ts", ".tsx"]
    },
    output: {
        path: dist,
        filename: "bundle.js"
    },
    devServer: {
        contentBase: dist,
        port: 3000
    },
    plugins: [
        new UglifyJSPlugin(),
    ]
};

typescriptファイルの作成

$ touch src/ts/index.ts
console.log("test") //のみを記載

コンパイル

ema-hiro.hatenablog.com

で作ったyarnのscriptの再利用。

$ yarn run build
yarn run v1.1.0
$ yarn run --config webpack
ts-loader: Using typescript@2.5.3 and ~/emahiro/ts_sample/tsconfig.json
Hash: bcac9e140803472acb6d
Version: webpack 3.7.1
Time: 1114ms
     Asset       Size  Chunks             Chunk Names
 bundle.js  498 bytes       0  [emitted]  main
index.html  182 bytes          [emitted]
   [0] ./ts/index.ts 21 bytes {0} [built]
Child html-webpack-plugin for "index.html":
     1 asset
       4 modules
✨  Done in 2.27s.

動作確認する

以前作成したものと同様の設定で webpack-dev-server を起動し localhost:3000 にアクセスする

$ yarn run start // ※ リンクにあるブログで作成済みのコマンド
Project is running at http://localhost:3000/
webpack output is served from /
Content not from webpack is served from ~/emahiro/ts_sample/dist

開発者画面 -> consoleを見て test とlogが記載されていればtypescriptはes5にコンパイルされて正常にブラウザで読み込まれている。

所感

webpack + typescriptは想像以上に導入が簡単。
次は簡単なアプリを作ってみたい。

webpackでhtmlファイルも出力する

ema-hiro.hatenablog.com

昨日の上記エントリへの追記

TODO

  • htmlファイルをdist以下に作成していましたが、frontendのファイルはsrc以下に管理します。
  • webpackでコンパイルしたらdist以下にhtmlファイルも出力させます
  • html-webpack-pluginを使います

install

$ yarn add -D html-webpack-plugin
$ yarn add -D html-loader

webpack.config.js

html-webpack-pluginhtml-loader を加えてコンパイルの設定を更新する

const src = __dirname + "/src";
const dist = __dirname + "/dist"

var webpack = require('webpack');
const UglifyJSPlugin = require('uglifyjs-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  context: src,
  entry: "./js/index.js",
  output: {
    path: dist,
    filename: "index.min.js"
  },
  devServer: {
    contentBase: dist,
    port: 3000
  },
  module: {
    loaders: [
      {
        test: /\.js$/,
        exclude:/node_modules/,
        loader: "babel-loader",
        query: {
          presets:[
            ["env", {
              "targets": {
                "node": "current"
              }
            }]
          ]
        }
      },
      {
        test: /\.html$/,
        loader: "html-loader"
      }
    ]
  },
  plugins: [
    new UglifyJSPlugin(),
    new HtmlWebpackPlugin({
      template: "./html/index.html"
    })
  ]
};

ディレクトリの変更

- root
  - src
    - js
      - index.js
    - html
      - index.html
  - dist
    - 出力先

index.htmlを作成する

src/html/index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>test</title>
  </head>
  <body>
    hello world
  </body>
</html>

コンパイルする

$ yarn run build
yarn run v1.1.0
$ yarn run --config webpack
Hash: 1c4cb048a83c8c452b7b
Version: webpack 3.6.0
Time: 577ms
       Asset       Size  Chunks             Chunk Names
index.min.js  814 bytes       0  [emitted]  main
  index.html  195 bytes          [emitted]
   [0] ./js/index.js 400 bytes {0} [built]
   [1] ./js/person.js 171 bytes {0} [built]
Child html-webpack-plugin for "index.html":
     1 asset
       [0] ../node_modules/html-webpack-plugin/lib/loader.js!./html/index.html 168 bytes {0} [built]
✨  Done in 1.71s.

buildpackage.json で新しくscriptsに追加したカスタムビルドコマンド

生成された index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>test</title>
  </head>
  <body>
    hello world
  <script type="text/javascript" src="index.min.js"></script></body>
</html>

コンパイルした index.min.jsscript タグに入ってちゃんと生成されている。

この状態で webpack-dev-server を動かしてちゃんと動作するかを確認する

おまけ

複数のhtmlファイルをコンパイルしたい

refs: https://github.com/jantimon/html-webpack-plugin#generating-multiple-html-files

SPAならindex.htmlだけ作成してそこにSPAの要素を追加していくのもありであるが、複数のhtmlファイルを出力したい場合がある。 そういう場合は html-webplack-plugin はデフォでは index.html しか出力しないが、別のファイルを新しくpluginで指定してfile名とコンパイル対象のtemplateを指定すれば良い。

// 略
plugins: [
  // index
  new HtmlWebpackPlugin({
    template: "./html/index.html"
  }),
  // その他
  new HtmlWebpackPlugin({
    filename: "admin.html",
    template: "./html/admin.html"
  })
]
$ ls dist/
admin.html  index.html

htmlファイルが複数出力されている

所感

webpack一発でフロントエンドの環境が構築されるっていうのはこういうことかってわかってきた。
今回はベタなhtmlをそのままコンパイルしているがテンプレ(ejsファイルとか)をコンパイルすることも可能

いまさらだけどyarn + webpackを試す

仕事の中でjsの開発環境を構築する機会があったので、自前でes6をコンパイルする環境をととえてみた備忘録。
jsは動きが早くて色んなツールが出て来るなか、一定出尽くして落ち着いてきそうな感じをここ最近感じていたので、ようやく少しずつキャッチアップしていこうと思いました。

サマリ

  • yarn + webpack導入
  • es6を動かす
  • es6で書いたコードを動かす

どうしてyarnにしてのか

yarnとは?
facebookが開発したnext npm的なポジションのpackageマネージャー

去年10月にローンチして、徐々に使用事例が多くなってきたので自前でjsのモダンな開発環境を作るにあたって採用してみようと思ったから。

yarnのいいところ

https://qiita.com/0829/items/ec5271c06f8ff0633dd3 あたりを参考にした。

npmが生成した package.json を読み込める用に作られているので、npmとの互換性が保たれているので、正直どちらを使ってもいいと思われるが、npmより高速に動作し、依存関係やバージョン等も厳格に管理することができるため、npmでもできるが、yarnを積極的に使わない理由も弱い。

といった感想を持っている。

導入手順

yarn init

$ mkdir MyProject
$ cd MyProject
$ yarn init
yarn init
yarn init v1.1.0
...
success Saved package.json

対話式でプロジェクトの概要をセットアップしていく。
完了したらpackage.jsonができる

babelの設定

babelを使う場合は、 .babelrc が必要なものかと思ってましたが、webpackの場合は webpack.config.jsコンパイルの定義を書けます。

babelと一緒にes2015(es6)の構文対応できるように、 babel-preset-env をインストールします。

$ yarn add -D babel-loader babel-core babel-preset-env

当初 babel-preset-es2015 を入れてましたが、yarn add している最中に warning babel-preset-es2015@6.24.1: 🙌 Thanks for using Babel: we recommend using babel-preset-env now: please read babeljs.io/env to update! というwarnを受けたので、 babel-preset-env を入れました。

refs: https://babeljs.io/env/
github: https://github.com/babel/babel-preset-env

webpack.config.jsの設定

コンパイル元のファイルはentryに書いて、1つに固めたファイルの出力先がoutput。

というわけで出来上がった webpack.config.js はこちら

const src = __dirname + "/src";
const dist = __dirname + "/dist"

var webpack = require('webpack');

module.exports = {
  context: src,
  entry: "./index.js",
  output: {
    path: dist,
    filename: "index.min.js"
  },
  module: {
    loaders: [
      {
        test: /\.js$/,
        exclude:/node_modules/,
        loader: "babel-loader",
        query: {
          presets:[
            ["env"]
          ]
        }
      }
    ]
  }
};

es6の導入

ディレクトリ構成は以下

MyProject
  src
    index.js
  dist
    index.min.js

このindex.jsにes6の構文で書いて、コンパイル後index.min.jsがちゃんと読み込まれるかを確認する

おまけ

難読化(UglifyJS)

コンパイルしたファイルを難読化する。
webpackには標準でUglifyJSがあるので、これを plugins に追加したが、defaultの難読化プラグインコンパイル時にエラーが発生した

$ yarn run build
yarn run v1.1.0
$ yarn run --config webpack
Hash: 2689b764da6353bc4101
Version: webpack 3.6.0
Time: 341ms
       Asset     Size  Chunks             Chunk Names
index.min.js  3.11 kB       0  [emitted]  main
   [0] ./index.js 371 bytes {0} [built]
   [1] ./person.js 162 bytes {0} [built]

ERROR in index.min.js from UglifyJs
Unexpected token: name (Friend) [index.min.js:79,6]
error Command failed with exit code 2.
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

標準のuglify-jsが動かなかったので、uglifyjs-webpack-pluginを使う
refs: https://www.npmjs.com/package/uglifyjs-webpack-plugin

$ yarn add -D uglifyjs-webpack-plugin
const src = __dirname + "/src";
const dist = __dirname + "/dist"

var webpack = require('webpack');
const UglifyJSPlugin = require('uglifyjs-webpack-plugin') //追加

module.exports = {
  context: src,
  entry: "./index.js",
  output: {
    path: dist,
    filename: "index.min.js"
  },
  module: {
    loaders: [
      {
        test: /\.js$/,
        exclude:/node_modules/,
        loader: "babel-loader",
        query: {
          presets:[
            ["env", {
              "targets": {
                "node": "current"
              }
            }]
          ]
        }
      }
    ]
  },
  plugins: [
    new UglifyJSPlugin() //追加
  ]
};

webpackを動かす

yarn run build
yarn run v1.1.0
$ yarn run --config webpack
Hash: 2689b764da6353bc4101
Version: webpack 3.6.0
Time: 381ms
       Asset       Size  Chunks             Chunk Names
index.min.js  786 bytes       0  [emitted]  main
   [0] ./index.js 371 bytes {0} [built]
   [1] ./person.js 162 bytes {0} [built]
✨  Done in 1.37s.

$ cat dist/index.min.js
!function(e){function t(n){if(r[n])return r[n].exports;var u=r[n]={i:n,l:!1,exports:{}};return e[n].call(u.exports,u,u.exports,t),u.l=!0,u.exports}var r={};t.m=e,t.c=r,t.d=function(e,r,n){t.o(e,r)||Object.defineProperty(e,r,{configurable:!1,enumerable:!0,get:n})},t.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(r,"a",r),r},t.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},t.p="",t(t.s=0)}([function(e,t,r){"use strict";var n=function(e){return e&&e.__esModule?e:{default:e}}(r(1));class u extends n.default{constructor(e){super(name)}callName(){alert(this.name)}}new u("Taro").callName()},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0});class n{constructor(e){this.n=e}}t.default=n}]);⏎

ちゃんと難読化されている。

動かしてみる

webpackにはlocalにサーバーを立てるライブラリ webpack-dev-server があるのでこちらを使う

$ yarn add -D webpack-dev-server
$ yarn run webpack-dev-server //これで動作させることができる。

上記でも問題ないがscriptを作成した

"scritps": {
  "start": "yarn run webpack-dev-server"
  "build":...
}

devserverはデフォルトは8080ポートで動作するが、手元のgoとパッティングするので3000番portに変更。
以下を webpack.config.json に追加する

const src = __dirname + "/src";
const dist = __dirname + "/dist"

var webpack = require('webpack');
const UglifyJSPlugin = require('uglifyjs-webpack-plugin')

module.exports = {
  context: src,
  entry: "./index.js",
  output: {
    path: dist,
    filename: "index.min.js"
  },
  devServer: {
    contentBase: dist,
    port: 3000
  },
  module: {
    loaders: [
      {
        test: /\.js$/,
        exclude:/node_modules/,
        loader: "babel-loader",
        query: {
          presets:[
            ["env", {
              "targets": {
                "node": "current"
              }
            }]
          ]
        }
      },
      // 以下でhtmlの読み込みも追加する
      {
        test: /\.html$/, 
        loader: "file?name=[path][name].[ext]"
      }
    ]
  },
  plugins: [
    new UglifyJSPlugin()
  ]
};

サーバーを起動させる

yarn run start
yarn run v1.1.0
$ yarn run webpack-dev-server
Project is running at http://localhost:3000/
webpack output is served from /
Content not from webpack is served from ~/emahiro/es6_sample/dist
Hash: 093cadee3f8742a9e298
Version: webpack 3.6.0
Time: 2247ms
       Asset    Size  Chunks             Chunk Names
...
webpack: Compiled successfully.

注目すべきは以下

webpack output is served from /
Content not from webpack is served from ~/emahiro/es6_sample/dist

サーブされているのは / だけど、実際にはdistがwebから見たときのcontentのrootになる

dist
  index.min.js

というディレクトリ構造をしている場合、htmlからjsを呼び出すときは ./index.min.js と直書きする。

サーバーが起動した状態で localhost:3000 にアクセスしたら alert が発火するはず。
発火したら正常にes6がコンパイルされて動作していることになる。

所感

今までjsの潮流が早すぎて、正直足突っ込んだら抜けられなくなりそうだったので、避けてきた分野でしたが、webpackを使うことでjsド素人でも簡単にes6をプロジェクトに取り入れることが出来ました。

色々試せそうですが、次はtypescript入れてコンパイルしてみようと思います。

goでのhttpの書き方あれこれ

goの標準

goでhttpの処理を書きたいときはnet/httpパッケージを使う。
強力なパッケージなので、基本これを使うで、やりたいことの殆どはまかなえてしまうと思う

http.Get(url)

package main

import (
    "fmt"
    "io/ioutil"
    "net/http"
)

func main() {
    resp, err := http.Get("http://www.google.co.jp")
    if err != nil {
        fmt.Printf("err: %v", err)
    }
    defer resp.Body.Close()

    b, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        fmt.Printf("ioutil err: %v", err)
    }
    fmt.Printf("html: %v", string(b))
}

http.NewRequest(url)

単純にGetをするだけであれば http.Get(url) で事足りると思われるが、その他のMethodにも対応させるために、httpクライアントを明示的に作ることも可能

package main

import (
    "fmt"
    "io/ioutil"
    "net/http"
)

func main() {
    req, err := http.NewRequest("GET", "http://www.google.co.jp", nil)
    if err != nil {
        fmt.Printf("new request err: %v", err)
    }
    client := http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        fmt.Printf("request response err: %v", err)
    }

    defer resp.Body.Close()

    b, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        fmt.Printf("ioutil err: %v", err)
    }
    fmt.Printf("html: %v", string(b))
}

GAEの標準

GAE/Goでhttpを書きたいときにはちょっと1つ考慮しておかなければいけないことがある。

  • GAE/Goでは、GAEのContextを使わないといけない
  • 通常のリクエストからGAEのContextを作成し、それを使ってhttpのrequest/responseを実装する
package main

import (
    "net/http"

    "io/ioutil"

    "google.golang.org/appengine"
    "google.golang.org/appengine/log"
    "google.golang.org/appengine/urlfetch"
)

func init() {
    http.HandleFunc("/", SampleHandler)
}

func SampleHandler(w http.ResponseWriter, r *http.Request) {
    ctx := appengine.NewContext(r) // GAEのContextを作成する
    httpClient := urlfetch.Client(ctx)
    resp, err := httpClient.Get("http://www.google.co.jp")
    if err != nil {
        log.Debugf(ctx, "html: %v", err)
    }
    defer resp.Body.Close()
    b, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        log.Debugf(ctx, "html: %v", err)
    }
    log.Debugf(ctx, "html: %v", string(b))
}

ハマったところ

  • gaeのプロジェクトの最小構成単位はhandlerを受け付けるgoのファイルとapp.yamlがあること。
  • しかし、この2つのファイルは同じ階層においては行けない
- my-project
    - app.yaml
    - src
        - main.go

というディレクトリ構成にする必要がある。

  • gaeではmain関数でなく init() を使う。
  • main() を使っていた最中にずっと起動後 localhost:8080 に繋いでも 404 page not found が返ってきてしまっていた。
  • gaeのhttpのHandlerには http.ListenAndServe() メソッドは使わない。

The Twelve-Factor App を読む

ちょっと前にはてブで上位に上がっていた以下のスライド

speakerdeck.com

の中に出てきていた「The Twelve-Factor App」を読んだ感想を書く。

原典の邦訳はこちら ▶ https://12factor.net/ja/

今更感があるがちょっと最近基礎が揺らいでいるなという感覚もあったのでざっと通読してみた。

  • 12個の原則はどれも昨今のウェブサービスを作るのであれば当たり前とも言える内容だった。
  • 自分がそういう環境でしか仕事をしたことがないからかもしれないが、既存のチームにJOINするにしても、新しく1からサービスを作るにしても意識、というか当たり前にできなければいけないことだと思う。
  • 内容としては昨今のフルマネージドが流行りつつある状況ではもしかしたらvagrantやポートバインディングなどは自分で管理するものではないかもしれない。
  • しかし、背景にこういう法則があって、というContextを理解しておくことは大事だと思う。

結構忘れていることも多く、API開発の名著も読み直そうと思う。

goで一時的なスクリプトを書く方法

Goで開発しているときに

  • 標準パッケージの動作確認したい
  • ライブラリの動作確認したい
  • 簡単なスクリプト(HTTPやgoroutineなど)を書きたい

と思って調べてみました。

Rubyならirbやpryを使ってコンソールで簡単な動作検証を行えたり、変数の内部見ることができますが、Goは静的な言語でコンパイルすることが必要になるので、デフォルトではそういうちょっとした確認のためのスクリプト書いたりみたいなことができません。

Goで一時的なスクリプト確認したい場合はいつも The Go Playgroundを使っていましたが、

  • 補完が効かない
  • 標準パッケージしか使えない

という2点があって、例えばgithub上のライブラリの検証は実際に手元に落としてきて、Goでsampleファイル書くっていう手順をいつも行っててめんどくさいなーって思ってました。

そこでGoでもREPL的なものが無いかを調べてみたところ

ありました

github.com

作者さんが書いた使い方のエントリー motemen.hatenablog.com

実際に入れてみたところ

$ gore
gore version 0.2.6  :help for help
gore> :import encoding/json
gore> b, err := json.Marshal(nil)
[]uint8{
  0x6e, 0x75, 0x6c, 0x6c,
}
nil

おおおお!これはよい。簡単なスクリプトを試すにはすごく便利です。
:import すると手元に落としてきたライブラリもimportできるので、標準以外のメジャーなライブラリのサンプル書くのもいちいちGoのファイル作らなくていいので楽になります。

ppgocode をあわせて入れとくと出力をきれいに整形してくれたり、補完が効くようになります。
可読性も効率もアップできるのでとても便利なデバック用のライブラリだと思います。

GoonのGet()とGetAll()の違いでハマる

Go+GAEの環境でCloudDataStoreからデータを取得するときに

  • Get()
  • GetMulti()
  • GetAll()

の挙動の違いでハマったので備忘録として挙動をまとめておきます。

Datasotreのライブラリ

datastore

https://godoc.org/google.golang.org/appengine/datastore

googleappengineには標準でdatastoreのクライアントとして datastore が用意されています。 僕も使うようになってこの便利さに気づいたのですが、structでRDBでいうところのレコード、datasotreではentityの構造を定義してしまえば、かなり簡単にCRUDの操作ができます。
※ ここでいうentityの構造とはモデルの構造のことです。

goon

https://godoc.org/github.com/mjibson/goon

goonはdatastoreとほぼ同じようなインターフェースを持っていて、自動キャッシュがついているので、キャッシュのことまで考えた場合に使いやすいライブラリです。

データを取得する

goonを使ってDatastoreからentityを取得する場合以下のようにします。

type entity struct {
    Id      int64 `datastore:"-" goon:"id"`     
    Field_1 string
    Field_2 string
}

func GetInstances(r *http.Request){
    g := goon.NewGoon(r)
    entity := &entity{ID:1}
    g.Get(entity) // ID(key) = 1のentityを取得する
}

goon.NewGoon(r) としている箇所はhttp.Requestから新規でappengineのContextを作成しています。NewGoonメソッドの内部はgoonのソースコード読むと

// NewGoon creates a new Goon object from the given request.
func NewGoon(r *http.Request) *Goon {
    return FromContext(appengine.NewContext(r))
}

ちなみに以下のような書き方も可能

g := goon.NewGoon(r)
entity := &entity{ID:1}
g.Goon().Get(entity) // ID(key) = 1のentityを取得する

とあるので、ここでappengineから新しくContext作っているのが一目瞭然ですね。

遭遇したエラー

Datastoreからデータを取得するときに以下のエラーに遭遇しました。
順を追ってどうしてエラーが発生したのか見ていきます。

  1. goon: cannot find a key for struct
  2. goon: Expected dst to be a pointer to a slice or nil, got instead
  3. invalid entity type

1. goon: cannot find a key for struct

こちらはGet()とGetMultiが ID(key) をKeyにしないとDatastoreからデータを取得できない性質を持っているということを知らなかったために発生しました。

RDBのようにprimaryキーだけでなく別のfieldをフックにしてデータを取得したい場合があると思います。
しかし、DataStoreのGet()とGetMulti()はID(DatastoreでいうところのprimaryKey)をKeyに取ることしかできません。

そのため、ID以外のfieldをKeyにしたい場合には上記2つのメソッドではなく、GetAll()メソッドを使ってクエリを直に指定する必要があります。

query.Filter("someFiled =", hogehoge) とqueryでFilterを指定すれば可能。

2. goon: Expected dst to be a pointer to a slice or nil, got instead

これはGetAll()を使うときに発生しました。 dstは GetAll(query *datastore.Query, dst interface{}) とあるようにGetAll()メソッドの第二引数です。

DataStoreからデータを取得する場合には dst に対して指定した参照型の変数にデータが格納されて入ってきます。
これがそのまま、DataStoreから取得したデータの結果になります。
そのため、dstには slice化されたpointer型の変数 もしくは何も指定指定しない nil しかいれることができません。
上記のサンプルでも entity が参照型になっています。

3. invalid entity type

これも2と要点は同じなんですが、Datastoreから取得したEntityの入れ物となる参照型の変数の構造は取得したいDatastoreのkindのfield構成と一致している必要があるそうです。

言われてみれば当たり前のような気もしますが、参照型、かつ構造が同じでないといけません。

Datastoreのルールで詰まったり、構造で詰まったり、そもそもなぜdstは参照型で渡すのか、色々知るいい機会になりました。

appengineのmemcacheを使う

GAEではmemcache一択

appengineのmemcacheパッケージを使ってみました。

リファレンスはこちら

https://cloud.google.com/appengine/docs/standard/go/memcache/reference

goでキャッシュを扱うときに最初 go-cache を使おうと思っていたのだけれど、GAEでキャッシュを使うときはmemcacheが標準のパッケージでついているのでこれを使うと簡単に実装できることを知る。

go-cache https://github.com/patrickmn/go-cache

実装方法

キャッシュなので、

  • キャッシュを取得する。
  • キャッシュがexpiredを過ぎていれば新しくキャッシュをセットする

の2通り

sampleコード

import (
  "encoding/json"
  "google.golang.org/appengine/memcache"
  "google.golang.org/appengine"
)

var (
  cacheKey := "KEY"
  casheExpTime := 60 // seconds
)

type Data struct{
  ID   int
  Name string
}

func CacheProcess(r *http.Request){
  ctx := appengie.NewContext(r)
  item, err := memcache.Get(ctx, cacheKey)
  if err != nil {
    // cacheを取得するときのエラー
  }
  
  if item != nil {
    // cacheが存在する時
    fmt.Fprintf("memcache key: %v", item.Key)
    fmt.Fprintf("memcache value: %v", item.Value)
    
    // cacheのValueの中身を取り出す。Valueは[]byte型
    data := &Data{}
    err := json.Unmarshal(item.Value, data)
    if err != nil {
      // jsonをunmarschalした時のエラー
    }
    
  } else {
      // cacheが存在しない時
      data := &Data{
        ID:   1,
        Name: "emahiro"
      }
     
      // dataを[]byte型に変換
      byte, err := json.Marshal(data)
      if err != nil{
        // dataをbyteにMarshalするときのエラー
      }
     
      // cacheをセットする
      i := &memcache.Item{
        Key:        cacheKey,
        Value:      byte
        Expiration: time.Duration(cacheExpTime) * time.Seconds // time.Duration(ns) 型
      }
     
      err := memcache.Set(ctx, i)
      if err != nil {
        // cacheをSetする時のエラー
      }
  }
}

Cacheを登録するときにその値を memcache.Item 型に変換することと、itemの Valueプロパティは[]byte型であるというところに注意すればかなり簡単にキャッシュを実装できます。

Goglandでソフトラップを消す

Preference ▶ Editor ▶ Appearance で show right margin をoffにする これでソフトラップがoffになる。

f:id:ema_hiro:20170817011538p:plain

goは変なところでソフトラップがかかって改行されるとコンパイルエラーになるのが嫌なので、ソフトラップをオフにしてます。

参考 http://samuraism.com/products/jetbrains/intellij-idea/quickstart/editor-basics

JetBrains系のソフトウェアなので同じような設定の仕方でいけますね。

[20171108追記]
GogLandがアップデートしたので show hard wrap guide(configured in Code Style options) にUIが変わっていました。