emahiro/b.log

Drastically Repeat Yourself !!!!

githubのSearchAPIで遊んだ話

githubのsearchAPIを簡単にラップしたGUI作りました。

f:id:ema_hiro:20171119031801g:plain

久しぶりにjqueryとか触ったらすごい懐かしい匂いがして色々つまりました。
request処理とかしててハマったところがあるので別でエントリでまとめようと思います。

コードは以下 github.com

refs: はまったところは以下でまとめてみた

ema-hiro.hatenablog.com

Goでファイル読み込みを調べた話

サマリ

  • io.Readerの読み込みついて調べた
  • io.ReaderをWrapして文字列置換
  • io.Readerが一括読み込みでなくstream的な動作で順次読み込みされている

io.Readerについて

refs: io - The Go Programming Language

io.ReaderはデフォルトのReadのWrap。
ioパッケージのReaderはinterfaceとしてReadが設定されている。

type Reader interface {
    Read(p []byte) (n int, err error)
}

文字列置換を実装する

文字列置換を実装する前にgoにおけるinterface埋め込みを使った抽象化で文字列置換を実装します。

interface埋め込みによる抽象化

Readerが標準のReadをinterfaceとして持っているので、このReaderを埋め込むことでReadメソッドに独自の処理を加えてWrapします。

※ goは同一interfaceを定義したメソッドをstructに定義することで実装の詳細をstructに生やしたメソッドに移譲することができる。

cf. io.Reader

type Reader interface {
    Read(p []byte) (n int, err error)
}

goによるinterfaceの埋め込み

type WrapRead interface {
  Read([]byte)(int, error)
}

type WrapReader struct {
   wrapper WrapRead
}

func (w *WrapReader) Read(p []byte) (int, error){
  // WrapReaderでのReadの実装の詳細を記載
} 

以下と書いても同義
※ io.Readerはデフォルトの Read([]byte) (int, error) interfaceのラップなのでわざわざinterfaceで定義し直すのは冗長。

type WrapReader struct {
  reader io.Reader
}

func (w *WrapReader) Read(p []byte) (int, error){
  // WrapReaderでのReadの実装の詳細を記載
}

文字列置換メソッドを実装する

type RewriteWriter struct {
    reader io.Reader
}

func (r *RewriteWriter) Read(p []byte) (int, error) {
    buf := make([]byte, len(p))
    n, err := r.reader.Read(buf) 
    if err != nil && err != io.EOF {
        return n, err
    }

    return copy(p, bytes.Replace(buf, []byte("0"), []byte("1"), -1)), nil
}

デフォルトのReaderを RewriteWriter のstructにreaderという変数で扱えるように埋め込んで置くことで、 RewriteWriter に生やしたReadメソッドでデフォルトのReadに実装の詳細を追加することができる。
置換するので、replaceした内容をもともとのbyteにコピーしています。

io.Readerの動作を確認する。

読み込みを確認するために以下のようなコードを書きました。

handler/main.go

func Top(w http.ResponseWriter, r *http.Request) {
    res, err := http.Get(fmt.Sprintf("http://%v:%v/data", host, port))
    if err != nil {
        fmt.Printf("request get error. err: %v", err)
    }
    body := res.Body
    defer body.Close()
  io.Copy(w, &RewriteWriter{body})
}

func Data(w http.ResponseWriter, r *http.Request) {
    var str string
    for i := 0; i < 10000; i++ {
        str = str + fmt.Sprintf("%v\n", "000")
    }

    w.Write([]byte(str))
}

10000行の文字列を置換するというものです。

動かしてみます。

$ go run main.go
# serverが起動

$ curl http://localhost:8080
111
111
111
111
# 以下同様

10000行程度ならすぐに完了してしまいますが、Readメソッドは読み込むデータを一時的に保存しておくbufferの頭から順々に読み込んでいくような動作をしているようです。
全てのデータを終端記号まで読み込んでから全データを処理するわけではないみたいです。
※ 全てのデータを終端記号まで一括で読み込む場合でから使う場合には ioutil.ReadAllメソッドを使います。

このあたりは 「Goならわかるシステムプログラミング」 の io.Readerの章を確認しながら理解しました。

今回書いたコードは以下

github.com

GAE/GOのversionを上げたらContextが違ってコードが動かなくなってた話

有名な話です。が、いざ自分が体験したので備忘録としてまとめます。

github.com

上記で上げられている netcontextとcontext周りで死ぬ というのに引っかかりました。

課題

go1.6上で以下のようなリクエスト比較するコードを書いてました。 ※ コードはあくまでサンプルです。

url := "https://www.gooogle.com"
values := url.Values{}
req_1, _ := http.NewRequest("POST", url, strings.NewReader(values.Encode()))
req_2, _ := http.NewRequest("POST", url, strings.NewReader(values.Encode()))

if reflect.DeepEqual(req_1, req_2) {
  // requestが同値である。
} else {
  // requestは同値でない
}

req_1req_2 をdeepEqualで比較して、同値性を判別したいという意図でしたが、このコードはgo1.6だと同値だと判定されますが、go1.8だと同値だと判定されません。

理由は最初に書いた netcontextとcontext周りで死ぬ が原因だと思われます。

goのx/net/context パッケージが標準の context に入ったというのは有名です。
refs: Go 1.7 Release Notes - The Go Programming Language

net/context も標準のcontextとして扱われるようになったので、req_1req_2 は別々のリクエスト、すなわち異なるcontextを持っていると判定され、それを reflect.DeepEqual にかけた場合、標準の context 違いがあるので、同値判定されません。

go1.7のcontextについては以下のブログがすごく勉強になりました。

Go1.7のcontextパッケージ | SOTA

同値性の比較

では、requestの比較をしたい場合はどうすればいいかというと、 httputil.DumpRequest を使います。 refs: httputil - The Go Programming Language

url := "https://www.gooogle.com"
values := url.Values{}
req_1, _ := http.NewRequest("POST", url, strings.NewReader(values.Encode()))
req_2, _ := http.NewRequest("POST", url, strings.NewReader(values.Encode()))

// dumpは[]byte型
dump_1, _ := httputil.DumpRequest(req_1, true)
dump_2, _ := httputil.DumpRequest(req_2, true) 

// DeepEqual
if reflect.DeepEqual(dump_1, dump_2){
  // requestの同値判定
}

// bytes.Equal
if bytes.Equal(dump_1, dump_2) {
  // requestの同値判定
}

// stringに変換して文字列比較
if string(dump_1) == string(dump_2) {
  // requestの同値判定
}

DumpRequest することで context ではなくリクエストそのものを比較出来ます。
比較方法は、以前同様 DeepEqual を使ってもいいですし、 []byte 型に変換されるので、それに合わせて bytes packageを使ってもいいですし、文字列に変換して文字列一致をしても同値性を取ることが出来ると思います。

time.IsZero()の挙動でハマった話

サマリ

  • goのtimeパッケージの IsZero() はUnixTime = 0ではない
  • GAEのdatasotreのdefaultの時刻で IsZero() を使ってもtrueを返さない

IsZero()メソッドについて

refs: time package - time - pkg.go.dev

IsZero reports whether t represents the zero time instant, January 1, year 1, 00:00:00 UTC. 

IsZero()は 01-01 00:00:00 UTC の時にtrueを返します。
ここで注意するべきはtrueを返す時刻はunixtimeのスタート時刻 1970-01-01 00:00:00 +0000 UTC を指し示すわけではないということでです。

実際の挙動を見てみます。

def := time.Time{}
fmt.Printf("%v\n", def)
fmt.Printf("%v\n", def.IsZero())
// output
// 0001-01-01 00:00:00 +0000 UTC
// true

time.Time{} は何もない時刻をinstance化する、すなわちtimeパッケージにおける標準時刻をinstance化することですが、これの結果は 0001-01-01 00:00:00 +0000 UTC という時刻がinstance化され、IsZero() はこの時刻のときのみtrueを返します。

udef, _ := time.Parse("2006-01-02 15:04:05 -0700", "1970-01-01 00:00:00 +0000")
fmt.Printf("%v\n", udef)
fmt.Printf("%v\n", udef.IsZero())
// output
// 1970-01-01 00:00:00 +0000 +0000
// false

一方でコンピューターにおける時刻ゼロとはunixtimeの1番最初だと想起できるので、unixtimeのスタートした時刻に対して IsZero() をcallすると、unixtimeのstartの時刻にもかかわらず false を返します。

timeパッケージの中身を見てみると

type Time struct {
    // sec gives the number of seconds elapsed since
    // January 1, year 1 00:00:00 UTC.
    sec int64

    // nsec specifies a non-negative nanosecond
    // offset within the second named by Seconds.
    // It must be in the range [0, 999999999].
    nsec int32

    // loc specifies the Location that should be used to
    // determine the minute, hour, month, day, and year
    // that correspond to this Time.
    // Only the zero Time has a nil Location.
    // In that case it is interpreted to mean UTC.
    loc *Location
}

とあり、そもそもの sec = 0 の時には January 1, year 1 00:00:00 UTC. が初期値設定されています。
IsZero() については

// IsZero reports whether t represents the zero time instant,
// January 1, year 1, 00:00:00 UTC.
func (t Time) IsZero() bool {
    return t.sec == 0 && t.nsec == 0
}

とあるので、そもそもunixtime=0を返さないのはgoのtimeパッケージの仕様のようです。

GAE上での挙動について

さて、ここで困ったのがGAEでDatastore上に time.Time 型で標準時刻をinstance化した時のことです。

以下のようなstructを考えてみます。

type App struct {
  ID         int       `datastore: "ID"         json: "id"`
  CreatedAt  time.Time `datastore: "createdAt"  json: "created_at"`
  UpdatedAt  time.Time `datastore: "updatedAt"  json: "updated_at"`
  ReleasedAt time.Time `datastore: "ReleasedAt" json: "released_at"`
}

この App Entityがcreateされた時に CreatedAtUpdatedAt はそれぞれcreateされた時刻が入りますが リリースされたわけではないので、 ReleasedAt には何も入りません。
つまり、 ReleasedAt のfieldには time.Time{} が入ってくることを期待してました。
しかし実際には 1970-01-01 00:00:00 +0900 JST という日本標準時のでのunixtime = 0の状態が入っていました。

つまり、 ReleasedAt に一度しか値を入れたくない、みたいな要件があったときに

if !app.ReleasedAt.UTC().IsZero() {
    // ReleasedAtにすでに値が入っている時
} else {
    // ReleasedAtに初回に値が入る    
}

上記のような条件分岐を考慮した場合、どんなときでも else 以下に入ってしまいます。
理由は上記で述べた通り、 unixtime のスタートはtimeパッケージで IsZero 判別するときには false を返してしまうからです。

ではどうすればいいかというと、実は unixtimeの最初の状態を作り出した time オブジェクトのunixtimeを取ると 0 になります。

udef, _ := time.Parse("2006-01-02 15:04:05 -0700", "1970-01-01 00:00:00 +0000")
fmt.Printf("%v\n", udef)
fmt.Printf("%v\n", udef.UTC().Unix())
// output
// 1970-01-01 00:00:00 +0000 +0000
// 0

これを利用して上記の条件分岐を以下のように書き換えます。

if app.ReleasedAt.UTC().Unix() != 0 {
    // ReleasedAtにすでに値が入っている時
} else {
    // ReleasedAtに初回に値が入る    
}

app.ReleasedAt.UTC().Unix() とすることで、すでに ReleasedAt に値が入ってきている場合は、 Unix() でunixtimeに変換した時に 0以外 が入ってくる事になります。

まとめ

timeパッケージにおける IsZero() の挙動とGAEのDatastoreでデフォルトの時刻を unixtime = 0 判定を同様に考えてきて、かなりハマりました。
IsZero() がunixtimeのstart時刻を示さないのはどうにも納得が行きませんが、timeパッケージ的にはどうしようもなさそうなので、注意しようと思いました。

追記

このエントリを書いてから 4年以上経って言及されるとは思ってなかったですが、 いい感じに答えたが書いてあって参考にしたいなと思いました。

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

前回書いた記事の中でオレオレroutingを実装する際に標準の net/http パッケージだけだと足りないと書いてましたがこれ、間違いでした。

ema-hiro.hatenablog.com

標準の net/http パッケージだけでオレオレroutingを実装する方法は以下

main.go

package main

import (
    "gothub/handler"

    "fmt"
    "net/http"

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

const port = "8080"

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

handler/handler.go

pakage handler

import (
    "net/http"
)

func Top(w http.ResponseWriter, r *httpRequest){
    // serverの処理
}

router := http.NewServeMux()HTTP Request multiplexerインスタンス化し、routerとして扱う。
http requestをhandleしたいroutingのメソッドには http.ResponseWriterhttp.Request を引数に与える。

routingのライブラリを使うことなく、標準のHTTPパッケージだけでもやりたかった、超薄いAPIを作るということは可能でした。

(golangのhttpパッケージすげぇ強力だなぁ(小並感))

GogLandでtmplファイルをhtmlのシンタックス対象に加える

いつからはわからないですが、GogLand EAPをアップデートしたらtmplファイルがhtmlのシンタックス対象から外れてて、htmlを開いてもxmlと判別されてエラーがうるさくなってしまったので、カスタムファイルとしてtmplファイルのときは、htmlのシンタックスを追加する方法を記載します。

手順

Preferences -> Editor -> FileType でhtmlを選択し、シンタックス適用の登録ファイルにtmplを追加する。

画像は以下の場所 (+) マークを押下して、 tmpl ファイルを追加します。

f:id:ema_hiro:20171107012953p:plain

【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入れてコンパイルしてみようと思います。