nirasan's tech blog

趣味や仕事の覚え書きです。Linux, Perl, PHP, Ruby, Javascript, Android, Cocos2d-x, Unity などに興味があります。

Go 言語のアプリを heroku にデプロイするときは vender を .gitignore にいれるとだめ

  • ビルドエラーになる
  • godep 使わせている意味なくない?
  • デプロイ用のブランチ作って .gitignore から外すとかするのかな?
$ git push heroku master
Counting objects: 280, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (260/260), done.
Writing objects: 100% (280/280), 36.31 KiB | 0 bytes/s, done.
Total 280 (delta 123), reused 0 (delta 0)
remote: Compressing source files... done.
remote: Building source:
remote:
remote: -----> Go app detected
remote: -----> Checking Godeps/Godeps.json file.
remote: -----> Installing go1.6... done
remote: -----> Running: godep go install -tags heroku ./...
remote: my_go_file.go:5:2: cannot find package "any/dep/package" in any of:
... 略 ...
remote: godep: go exit status 1
remote:  !     Push rejected, failed to compile Go app.
remote:
remote:  !     Push failed
remote: Verifying deploy...
remote:
remote: !       Push rejected to myapp.
remote:
To https://git.heroku.com/myapp.git
 ! [remote rejected] master -> master (pre-receive hook declined)
error: failed to push some refs to 'https://git.heroku.com/myapp.git'

Rubyのリファクタリングでイケてないコードをなんとかするやつをやってみた

はじめに

リファクタリングできそうな点

  • initialize で start_date, end_date を渡しているのが不自然。
    • OrdersReport という汎用的なクラス名なので期間以外の条件で絞り込みたくなった時に応用が効かない
    • クラス名が OrdersDailyReport だったり、仕様として集計時には必ず期間が指定されると決まっていたりしたら気にしなくていいかも
  • total_sales_within_date_range で orders の絞り込みと集計を同時にやっている
    • ここも他の条件で絞り込みたくなった時に応用が効かないので絞り込み処理と集計処理を分けたい

リファクタリング結果

変更後

  • 呼び出し方は変わって OrdersReport.new(orders).select_within_date_range(start_date, end_date).total_amount となった
  • 行数が削減された
  • 複数の責務を持つ一つの大きなメソッドから単一の責務を持つ複数の小さなメソッドに分割された
  • 絞り込み処理と集計処理をメソッドチェーンで繋げるようにしたので、新しい絞り込み条件が入ってきても拡張しやすくなった
require 'ostruct'

class OrdersReport
  def initialize(orders)
    @orders = orders
  end

  def select_within_date_range(start_date, end_date)
    select { |o| start_date <= o.placed_at && o.placed_at <= end_date }
  end

  def select(&block)
    self.class.new(@orders.select(&block))
  end

  def total_amount
    @orders.map(&:amount).inject(:+)
  end
end

class Order < OpenStruct
end

変更前

require 'ostruct'

class OrdersReport
  def initialize(orders, start_date, end_date)
    @orders = orders
    @start_date = start_date
    @end_date = end_date
  end

  def total_sales_within_date_range
    orders_within_range = []
    @orders.each do |order|
      if order.placed_at >= @start_date && order.placed_at <= @end_date
        orders_within_range << order
      end
    end

    sum = 0
    orders_within_range.each do |order|
      sum += order.amount
    end
    sum
  end
end

class Order < OpenStruct
end

Go 言語でソリティアを作った

termbox-go を知ったので、息抜きに Go 言語でソリティアを作りました。
https://github.com/nirasan/go-solitaire

f:id:nirasan:20160610081329p:plain

仕事中にやりやすいように ls っぽいモードも搭載してあります。

f:id:nirasan:20160610081347p:plain

mac 用だけサンプルがあります。操作は README で。
https://github.com/nirasan/go-solitaire/blob/master/sample/solitaire?raw=true

Go 言語の列挙型的な定数と定数でのフラグ管理

列挙型的定数

  • Go では const と iota を使って列挙型的な定数を定義する
    • iota は const 宣言開始時に 0 になり、定数を定義する毎にインクリメントする
    • 定数定義は値を指定しないと直前の定数と同じ値になるので iota と組み合わせることで連続した値が定義される
// 定義
type Flag int
const (             // iota = 0 (仮想コード)
  Flag1 Flag = iota // Flag1 = iota; iota++
  Flag2             // Flag2 = iota; iota++
  Flag3             // Flag3 = iota; iota++
)
// 利用例
f := Flag1
if f == Flag1 {
  // Flag1 の場合の処理
}

列挙型的定数を使ったフラグ管理

  • 列挙型的な定数の定義時に一工夫すると定数をフラグ的に管理できるようになる
// 定義
type Flag int
const (
  Flag1 Flag = 1 << iota // 1 << 0 (10進数: 1, 2進数: 00000001)
  Flag2                  // 1 << 1 (10進数: 2, 2進数: 00000010)
  Flag3                  // 1 << 2 (10進数: 4, 2進数: 00000100)
  Flag4                  // 1 << 3 (10進数: 8, 2進数: 00001000)
)
// 利用例
f := Flag1
if f & Flag1 != 0 {
  // Flag1 の場合の処理
  // &(and) 演算子で双方にビットが立っていた場合に 0 以外が返るので Flag1 が有効かどうかの判定ができる
}
if f & (Flag1 | Flag2) != 0 {
  // Flag1 か Flag2 の場合の処理
  // |(or) 演算子で Flag1 かつ Flag2 の値を作ることができるので複数のフラグの判定処理がちょっと楽になる
}

ひとつの定数で複数種類のフラグを管理する

  • 例えば文字の色と書体を同一の方で管理するような場合
type Flag int
const Flag_N_Start = 0
const (
    Flag1 Flag = 1 << (iota + Flag_N_Start) // 1 (00000001)
    Flag2                                   // 2 (00000010)
    Flag3                                   // 4 (00000100)
    Flag_N_Bits = iota                      // 3 (Flag_N で 3bit 使っている)
    Flag_N_Mask = 1 << Flag_N_Bits - 1      // 7 (00000111) (Flag_N 用のビットマスク)
)

const Flag_A_Start = Flag_N_Bits                       // Flag_A の開始ビット
const (
    FlagA Flag = 1 << (iota + Flag_A_Start)              // 1 << (0 + 3) =  8 (00001000)
    FlagB                                                // 1 << (1 + 3) = 16 (00010000)
    FlagC                                                // 1 << (2 + 3) = 32 (00100000)
    Flag_A_Bits = iota                                   // 3 (Flag_A で 3bit 使っている)
    Flag_A_Mask = (1 << Flag_A_Bits - 1) << Flag_A_Start // 56 (00111000) (Flag_A 用のビットマスク) (使用ビット分だけマスクを作って開始ビット分だけ左シフトする)
)

f := (Flag1 | FlagB)
if f & (Flag1 | FlagA) != 0 {
    // Flag1 か Flag2 だった時の処理
}

termbox-go 使い方まとめ

はじめに

https://github.com/kurehajime/pong-command とかみたいな感じでターミナル上で任意の座標に任意の文字を表示できる termbox-go の使い方まとめ。

概要としては termbox の内部でターミナル上の座標に対応した文字のバッファを持っているので、SetCell で座標と文字などを指定して、Flush で表示するという感じ。

使い方

インストール

go get github.com/mattn/go-runewidth // termbox-go が参照している
go get github.com/nsf/termbox-go

初期化

err := termbox.Init()
if err != nil {
        panic(err)
}
defer termbox.Close()

バッファのクリア

文字色と背景色を指定してバッファをクリアする

色の一覧は https://godoc.org/github.com/nsf/termbox-go#Attribute

termbox.Clear(termbox.ColorDefault, termbox.ColorDefault)

文字のセット

https://godoc.org/github.com/nsf/termbox-go#SetCell

termbox.SetCell(1, 2, rune("a"), termbox.ColorDefault, termbox.ColorDefault)

文字の取得

termbox 内部のバッファから文字を取得する。

表示済みの文字の色だけ変える場合などに使用できる。

以下は2行目の3文字目を取得する。

width, _ := termbox.Size()
row := 1
col := 2
cell := termbox.CellBuffer()[(width*row)+col]
var char rune = cell.Ch
var fg termbox.Attribute = cell.Fg
var bg termbox.Attribute = cell.Bg

バッファの表示

termbox.Flush()

キーボード入力の受付

mainloop:
for {
  switch ev := termbox.PollEvent(); ev.Type {
  case termbox.EventKey:
    if ev.Key == termbox.KeyEsc {
      break mainloop // Esc で実行終了
    }
  case termbox.EventResize:
    // 画面のリサイズが発生したので再描画などをする
  }
}

マウス入力の受付

mac の Terminal.app や iTerm2.app では現状バグが有って正常に動かない様子(マウス入力時に Esc など他のキー押下イベントが発生してしまう)

https://github.com/nsf/termbox-go/issues/120

termbox.SetInputMode(termbox.InputEsc | termbox.InputMouse)
mainloop:
for {
  switch ev := termbox.PollEvent(); ev.Type {
  case termbox.EventMouse:
    if ev.Key == termbox.MouseLeft {
      mx := ev.MouseX
      my := ev.MouseY
    }
}

キーボードのアルファベットキーを取得する

  • termbox のデフォルトのキーイベントではアルファベットキーをそのままは受け取れないので github.com/jteeuwen/keyboard/termbox を使う
import keyboard "github.com/jteeuwen/keyboard/termbox"

func main() {
  // termbox の初期化などは省略
  pollEvent()
}

func pollEvent() {
    running := true

    kb := keyboard.New()
    kb.Bind(func() { running = false }, "escape", "q")
    kb.Bind(func() { /* do up */ }, "up", "k")
    kb.Bind(func() { /* do down */ }, "down", "j")
    kb.Bind(func() { /* do left */  }, "left", "h")
    kb.Bind(func() { /* do right */  }, "right", "l")

    for running {
        kb.Poll(termbox.PollEvent())
    }
}

文字色を数字で指定する

// 256色表示(デフォルトは8色)
termbox.SetOutputMode(termbox.Output256)
// 文字色を背景色を数字で指定(内部で-1されているので+1する)
termbox.SetCell(0, 0, "a", termbox.Attribute(100+1), termbox.Attribute(200+1))

参考アプリケーション

Go 言語でブログを作ったメモ

  • Go 言語学習のためにユーザー認証とDB連携のあるブログアプリを作った
  • ソースは https://github.com/nirasan/go-blog
  • Web 周りに Gorilla web toolkit を、ORM に gorp を使用
  • Go 自体シンプルだし、あえてシンプルなライブラリを選んでみたのでわりとはまらずにできた
  • 次は revel とかリッチめのライブラリ試すかこのままシンプルで行くかどうするか

Go 言語で gorilla/mux, gorilla/context, gorilla/sessions を使ったユーザーログイン機能のサンプル

はじめに

Go 言語練習のため gorilla/mux, gorilla/context, gorilla/sessions を使ったユーザーログイン機能を実装してみました。
ユーザーの管理はメモリ上の map 型変数にユーザー名とパスワードを入れているだけなので、ここをDBに変えるのが次の課題のつもりです。

package main

import (
	"net/http"
	"html/template"
	"github.com/gorilla/mux"
	"github.com/gorilla/context"
	"github.com/gorilla/securecookie"
	"github.com/gorilla/sessions"
	"errors"
)

var (
	// セッションストアの初期化
	store *sessions.CookieStore = sessions.NewCookieStore(securecookie.GenerateRandomKey(64))
	// 登録ユーザーをメモリ上で管理
	users map[string]string = make(map[string]string)
)

const (
	SessionName = "session-name"
	ContextSessionKey = "session"
)

func main() {
	r := mux.NewRouter()
	handleFunc(r, "/", rootHandler)
	handleFunc(r, "/register", registerGetHandler).Methods("GET")
	handleFunc(r, "/register", registerPostHandler).Methods("POST")
	handleFunc(r, "/login", loginGetHandler).Methods("GET")
	handleFunc(r, "/login", loginPostHandler).Methods("POST")
	handleFunc(r, "/logout", logoutGetHandler).Methods("GET")
	handleFunc(r, "/logout", logoutPostHandler).Methods("POST")
	http.ListenAndServe(":8080", r)
}

// アプリケーション共通処理を常に呼び出すための糖衣構文
func handleFunc(r *mux.Router, path string, fn http.HandlerFunc) *mux.Route {
	return r.HandleFunc(path, applicationHandler(fn))
}

// アプリケーション共通処理
func applicationHandler(fn http.HandlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		// セッションの取得
		session, err := store.Get(r, SessionName)
		if err != nil {
			// 不正なセッションだった場合は作り直す
			session, err = store.New(r, SessionName)
			checkError(err)
		}
		context.Set(r, ContextSessionKey, session)
		// 個別のハンドラー呼び出し
		fn(w, r)
	}
}

// トップページ
func rootHandler(w http.ResponseWriter, r *http.Request) {
	type page struct {
		Username string
	}
	p := &page{}
	session, err := getSession(r)
	checkError(err)
	if v, ok := session.Values["username"]; ok {
		p.Username = v.(string)
	}
	executeTemplate(w, "template/index.html", p)
}

// 登録ページ
func registerGetHandler(w http.ResponseWriter, r *http.Request) {
	executeTemplate(w, "template/register.html", nil)
}

// 登録処理
func registerPostHandler(w http.ResponseWriter, r *http.Request) {
	// フォームのパース
	r.ParseForm()
	// フォームの値の取得
	username, password := r.Form["username"][0], r.Form["password"][0]
	// 空なら登録ページへ
	if username == "" || password == "" {
		http.Redirect(w, r, "/register", http.StatusFound)
		return
	}
	// ユーザー登録
	users[username] = password
	// 現在のユーザーをセッションで管理
	session, err := getSession(r)
	checkError(err)
	session.Values["username"] = username
	session.Save(r, w)
	// トップページへ
	http.Redirect(w, r, "/", http.StatusSeeOther)
}

// ログインページ
func loginGetHandler(w http.ResponseWriter, r *http.Request) {
	executeTemplate(w, "template/login.html", nil)
}

// ログイン処理
func loginPostHandler(w http.ResponseWriter, r *http.Request) {
	// フォームのパース
	r.ParseForm()
	// フォームの値の取得
	username, password := r.Form["username"][0], r.Form["password"][0]
	// 空ならログイン画面へ
	if username == "" && password == "" {
		http.Redirect(w, r, "/login", http.StatusFound)
		return
	}
	// ユーザーが登録済みでありパスワードが一致したらログイン状態に
	if v, ok := users[username]; ok && v == password {
		session, err := getSession(r)
		checkError(err)
		session.Values["username"] = username
		session.Save(r, w)
		http.Redirect(w, r, "/", http.StatusSeeOther)
	} else {
		http.Redirect(w, r, "/login", http.StatusSeeOther)
	}
}

// ログアウトページ
func logoutGetHandler(w http.ResponseWriter, r *http.Request) {
	executeTemplate(w, "template/logout.html", nil)
}

// ログアウト処理
func logoutPostHandler(w http.ResponseWriter, r *http.Request) {
	session, err := getSession(r)
	checkError(err)
	delete(session.Values, "username")
	session.Save(r, w)
	http.Redirect(w, r, "/", http.StatusSeeOther)
}

// セッションの取得
func getSession(r *http.Request) (*sessions.Session, error) {
	if v := context.Get(r, ContextSessionKey); v != nil {
		return v.(*sessions.Session), nil
	}
	return nil, errors.New("failed to get session")
}

// テンプレートの実行
func executeTemplate(w http.ResponseWriter, name string, data interface{}) {
	t, err := template.ParseFiles(name)
	checkError(err)
	err = t.Execute(w, data)
	checkError(err)
}

// エラーチェック
func checkError(err error) {
	if err != nil {
		panic(err)
	}
}

おわりに

net/http のハンドラーをチェインさせて共通処理を切り出すパターンは「Go言語によるWebアプリケーション開発」読んだらちゃんと書いてあったのでちゃんと読もうと思いました。
学習目的で gorilla/mux で実装してみてて実際勉強になってますが、普通に web アプリ作るならたぶん goji とか WAF を使った方がいい気がしてきてます。