今川館

都内勤務の地味OLです

はじめてのwebアプリ④ アップロードファイルの扱い方

この記事のソースコードこちら

ファイルを読み込んで文字数を数える機能を追加する

文字数カウントするwebアプリも入力テキストを処理するだけなら出来上がったので、今度はファイルをアップロードしてカウントする機能を追加することにした。

(画面イメージ)
f:id:imagawa_yakata:20161220110636p:plain

ファイルアップロードの結果として

  • 文字数(不正な文字は除外した数)
  • バイト数
  • 不正なバイト数

これらを画面に表示する。

ここまでは比較的悩むことなく簡単に進めてこられたが、アップロードファイルを扱う必要が生じて、途端に進みが遅くなった。
色々ハマって一応完成したので、学んだことを以下記す。

ファイルアップロードに対応

リクエストメソッドはPOST, enctype="multipart/form-data"を指定

取り敢えず画面からファイルをアップロードしなければならないので、

<form action="/count/file" method="POST" enctype="multipart/form-data">
  <input type="file" name="file">
  <input type="submit">
</form>

このように、リクエストメソッドはPOST、enctypeは"multipart/form-data"にしておく。

http.Request.FormFileからアップロードされたファイルを取得

次に、サーバー側の処理でアップロードされたファイルを取得、利用する方法だが、リクエストがFormFileというメソッドを持っているのでこれを使うことにした。

FormFileはキーを指定するとファイルを返してくれるが、その際、データをhttp.defaultMaxMemoryまで自動的にメモリに読み込んで、残りを一時ファイルに保存する。

FormFileの該当箇所
https://golang.org/src/net/http/request.go?s=34318:34403#L1118

func (r *Request) FormFile(key string) (multipart.File, *multipart.FileHeader, error) {
	if r.MultipartForm == multipartByReader {
		return nil, nil, errors.New("http: multipart handled by MultipartReader")
	}
	if r.MultipartForm == nil {
		err := r.ParseMultipartForm(defaultMaxMemory)
		if err != nil {
			return nil, nil, err
		}
	}
	(後略)
ファイルの後始末

FormFileの処理がわかったのは良いとして、一時ファイルが作られたときは後で削除する必要があるが、それはどう対処するのだろうか。
これには、RequestにMutipartForm.RemoveAll()というお誂え向きのメソッドがあった。
ソースを見ると、一時ファイルがあれば消す、と賢く処理してくれるようだ。

RemoveAllの該当箇所
https://golang.org/src/mime/multipart/formdata.go?s=2602:2634#L102

func (f *Form) RemoveAll() error {
	var err error
	for _, fhs := range f.File {
		for _, fh := range fhs {
			if fh.tmpfile != "" {
				e := os.Remove(fh.tmpfile)
				if e != nil && err == nil {
					err = e
				}
			}
		}
	}
	return err
}


そこで、deferに無名関数を指定し、

  1. file.Close() → アップロードファイルを閉じる
  2. r.MultipartForm.RemoveAll() → 一時ファイルが作成されていれば、削除

を呼ぶことにした(以下)。

file, _, err := r.FormFile("file")
if err != nil {
	http.Error(w, fmt.Sprintf("%v", err), http.StatusBadRequest)
}

defer func() {
	file.Close()
	r.MultipartForm.RemoveAll()
}()

ファイルから正常なruneの個数を数えたい

今までは文字列の中の文字数やバイト数を数えれば良いので簡単だったのだが、今度はファイルを見なければならない。
せっかくファイルを処理するのだから、全データを[]byteの変数に取り出してカウントするといった無駄なことはしたくないものだ。

最初、unicode/utf8を使って何とかしようと試みたがうまくいかず、色々調べた結果、実はbufioのReaderがReadRuneというメソッドを持っているのでこれを使えば良いことが判った。

ReadRuneの該当箇所
https://golang.org/src/bufio/bufio.go?s=6027:6084#L250

func (b *Reader) ReadRune() (r rune, size int, err error) {
	for b.r+utf8.UTFMax > b.w && !utf8.FullRune(b.buf[b.r:b.w]) && b.err == nil && b.w-b.r < len(b.buf) {
		b.fill() // b.w-b.r < len(buf) => buffer is not full
	}
(後略)

このように、正常なUTF-8の最大バイト数UTFMax(=4)まで読み取って正常なUnicodeコードポイント(rune)であるか調べて文字を返却してくれる。

なお、utf8にはruneを扱う上で必要な定数がちゃんと定義されているので覚えておくと良いかもしれない。

https://golang.org/pkg/unicode/utf8/#pkg-constants

const (
        RuneError = '\uFFFD'     // the "error" Rune or "Unicode replacement character"
        RuneSelf  = 0x80         // characters below Runeself are represented as themselves in a single byte.
        MaxRune   = '\U0010FFFF' // Maximum valid Unicode code point.
        UTFMax    = 4            // maximum number of bytes of a UTF-8 encoded Unicode character.
)

サンプルコード

最終的に、プログラムは以下のようになった。

type Context map[string]interface{}

func FileWordCountHandler(w http.ResponseWriter, r *http.Request) {
	if r.Method != "POST" {
		http.Error(w, "POSTでアクセスしてください", http.StatusMethodNotAllowed)
		return
	}

	file, _, err := r.FormFile("file")
	if err != nil {
		http.Error(w, fmt.Sprintf("%v", err), http.StatusBadRequest)
	}

	defer func() {
		file.Close()
		r.MultipartForm.RemoveAll()
	}()

	in := bufio.NewReader(file)
	count := 0
	bc := 0
	invalid := 0

	for {
		r, size, err := in.ReadRune()
		if err == io.EOF {
			break
		}
		if r == utf8.RuneError {
			invalid += size
		} else {
			bc += size
			// 正常な文字だけカウントする。
			count++
		}
	}

	wc, _ := htmlTemplate.New("file_wc").Parse(`
	<html>
		<head>
			<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
			<title>文字数カウント結果</title>
			<style type="text/css">
				.err { border: solid 1px red; }
			</style>
		</head>

		<body>
			<h1>文字数カウント結果</h1>

			文字数は: {{.count}}<br>
			バイト数は: {{.bc}}<br>
			不正なバイト数は: {{.invalid}}<br>

			でした。<br><br>

			文字を入力してください。
			<form action="/count" method="GET">
			<input type="text" name="text" size="32">
			<input type="submit">
			</form>

			ファイルを調べたい場合はこちら。
			<form action="/count/file" method="POST" enctype="multipart/form-data">
			<input type="file" name="file">
			<input type="submit">
			</form>

		</body>
	</html>`)

	data := Context{
		"count":   count,
		"bc":      bc,
		"invalid": invalid,
	}
	if err := wc.Execute(w, data); err != nil {
		log.Panic(err)
	}
}

はじめてのwebアプリ③ JSONで結果を出力する

この記事のソースコードこちら

JSONで出力する機能を追加する

はじめてのwebアプリのバグを直したところで、今度はJSONで結果を返す機能も追加してみる。

/countにformat=jsonで問い合わせが来たら、結果をJSONで返却する。

ハンドラ関数を分岐

まずハンドラ関数を分岐する。

func WordCountHandler(w http.ResponseWriter, r *http.Request) {
	if r.FormValue("format") == "json" {
		JSONWordCountHandler(w, r)
	} else {
		HTMLWordCountHandler(w, r)
	}
}

func JSONWordCountHandler(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json")
	// ...以下略
}

func HTMLWordCountHandler(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json")
	// ...以下略
}

json.Marshalは渡された構造体の公開フィールドしか出力しないので注意

Goのオブジェクトをjsonのテキストに出力する場合はencoding/jsonモジュールのMarshalを使う。

但し、利用には注意点がある。

小文字で始まるキーでJSONを出力したければタグを指定する

Marshalには構造体を渡すのだが、ここで渡す構造体の中で公開フィールドしか出力してくれない。
つまり、先頭が大文字で始まる名前のフィールドだけが出力対象となる。

最初、わたしは構造体を以下の通り定義したため、結果が"{}"、空のmapで出力されてしまった。

[ダメな例]

type WordCount struct {
	text  string
	count int
}

result, err := json.Marshal(WordCount{text, count})
if err != nil {
	log.Panic(err)
}
w.Write(result)

[ブラウザで問い合わせると…]

f:id:imagawa_yakata:20161219140820p:plain

↑この通り、空のmapが出力されて、NG。

[フィールドの名前を大文字で始めると]

type WordCount struct {
	Text  string
	Count int
}

result, err := json.Marshal(WordCount{text, count})

[ブラウザで問い合わせると…]

f:id:imagawa_yakata:20161219141030p:plain

↑今度はちゃんとデータが出力された、、が、キーが大文字で始まるのはちょっと・・

Goには「タグ」という仕組みがあって、構造体のフィールド宣言の後ろに文字列を書いておくとものによってはよきにはからってくれる。

json.Marshalはタグを見てくれるので、小文字のキーで出力するよう指定する。

[タグ付きでMarshal]

type WordCount struct {
	Text  string `json:"text"`
	Count int    `json:"count"`
}

result, err := json.Marshal(WordCount{text, count})

[ブラウザで問い合わせると…]

f:id:imagawa_yakata:20161219141334p:plain

↑今度は小文字でキーが出力され、期待通りの結果となった。

文字数とバイト数のふたつを返すよう変更

ついでに文字数の他にバイト数も返却するよう変更する。
さっきのMarshalに渡す構造体をちょっといじるだけ。

text := r.FormValue("text")
count := utf8.RuneCountInString(text)
bc := len(text)

// json.Marshalは構造体の公開フィールドしか出力してくれないので注意。
// 小文字でJSONのキーを出力したければタグを指定する。
type WordCount struct {
	Text      string `json:"text"`
	Count     int    `json:"count"`
	ByteCount int    `json:"byte_count"`
}

result, err := json.Marshal(WordCount{text, count, bc})
if err != nil {
	log.Panic(err)
}
w.Write(result)

出力結果にバイト数が追加されるようになった。

f:id:imagawa_yakata:20161219142357p:plain

はじめてのwebアプリ② unicode/utf8で文字数を数える

この記事のソースコードこちら

バグを直す

文字数カウントのwebアプリは、以下のバグがあるので直す。

  1. 文字数ではなくバイト数を算出している
  2. HTMLのエスケープをしていない

このため、

<font color="red">𩸽</font>

上記文字列を入力すると、スクリーンショットの通り、fontタグが適用されて文字が赤字表記になり、なおかつ本当は26文字と数えて欲しいにもかかわらず29文字と数えてしまう。

f:id:imagawa_yakata:20161219115328p:plain

文字数ではなくバイト数を算出している

これは前に学んだunicode/utf8のRuneCountInStringで数を調べるよう改める。

[before]

text := r.FormValue("text")
count := len(text)

[after]

// import "unicode/utf8"

text := r.FormValue("text")
count := utf8.RuneCountInString(text)

HTMLのエスケープをかける

次に、HTMLのエスケープをかけるため、html/templateのHTMLEscapeStringを使うことにした。

[before]

content := fmt.Sprintf(`...HTML本文`, text, count, css)

[after]

// import htmlTemplate "html/template"

content := fmt.Sprintf(`...HTML本文`, htmlTemplate.HTMLEscapeString(text), count, css)

text/templateモジュールでHTMLを出力するよう変更

html/templateをよく見たら、HTMLEscapeStringを使わなくても、テンプレート機能を利用すれば出力の式({{.名前}})で自動エスケープしてくれるようだ。

なので、以下の通り改めた。

// import htmlTemplate "html/template"
// import "log"

wc, _ := htmlTemplate.New("wc").Parse(`
<html>
	<head>
		<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
		<title>文字数カウント結果</title>
		<style type="text/css">
			.err { border: solid 1px red; }
		</style>
	</head>

	<body>
		<h1>文字数カウント結果</h1>

		入力文字: {{.text}}<br>
		文字数は: {{.count}}<br>

		でした。<br><br>

		文字を入力してください。
		<form action="/count" method="GET">
		<input type="text" name="text" size="32" class="{{.css}}">
		<input type="submit">
		</form>
	</body>
</html>`)

type Context map[string]interface{}
data := Context{"text": text, "count": count, "css": css}
if err := wc.Execute(w, data); err != nil {
	log.Panic(err)
}

これでHTMLのタグはエスケープされ、文字数も正しく26を数えてくれるようになった。

f:id:imagawa_yakata:20161219115336p:plain

はじめてのwebアプリ① net/httpのHandleFunc

この記事のソースコードこちら

文字数カウンターを作る

Goも基本的な構文をだいたい覚えたので簡単なwebアプリを作ってみる。

net/httpパッケージの使い方を覚える目的で、テキストを入力して文字数を数えて返す画面を作ることにする。

ソースコードここに上げておく。

Routeの追加: http.HandleFunc

httpにはRouteの追加方法としてHandleとHandleFuncが用意されている。

今回はHandleFuncを使ってトップページとワードカウントのページのRouteを追加してみる。

コードのイメージとしては、以下。

func init() {
	// URLパターンに正規表現は渡せない。
	http.HandleFunc("/count", WordCountHandler)

	// 順番に注意。"/"を先頭に指定すると他のPathがマッチしない。
	http.HandleFunc("/", TopPageHandler)
}

Handle, HandleFuncの第1引数に正規表現は渡せない

どうもnet/httpパッケージのHandleやHandleFuncは単なる文字列の前方一致で処理する模様。
ドキュメントでは引数の名前が"pattern"と書いてあるので正規表現を受け付けてくれるのかと思ったが、違うようだ。
(ただ、最も基本的なAPIは余計なことをしないに限るので、これで良いと思う)

正規表現で処理したい場合はHandleの引数に"/"など広くマッチするPathを指定し、http.Requestから取得可能なURL.Pathを調べて目的の処理に取り次ぐ。

以下、例

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

func Index(w http.ResponseWriter, r *http.Request) {
	if r.URL.Path == "/count" {
		WordCountHandler(w, r)
	} else {
		TopPageHandler(w, r)
	}
}

この辺のコントローラー層を便利にやってくれるフレームワークは色々あるようだが、取り敢えず使わず基本に忠実に行くことにする。

レスポンスボディの書き込み

わたしはHandleFuncの引数に渡されるResponseWriterのWriteを使った(以下)、

func WordCountHandler(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "text/html")

	// 1. テンプレートエンジンを使って処理するよう変更
	// 2. JSONで返却する機能を追加
	// 3. ファイルを読み取って文字数を数える機能を追加
	// 4. 文字数とバイト数を数えるよう変更
	// 5. ユニットテストを追加
	// 6. 最初のプログラムだと文字数ではなくバイト数を返してしまうので直す
	// 7. logをファイルに出力するよう変更

	text := r.FormValue("text")
	count := len(text)

	css := ""
	if count == 0 {
		css = "err"
	}

	content := fmt.Sprintf(`
	<html>
		<head>
			<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
			<title>文字数カウント結果</title>
			<style type="text/css">
				.err { border: solid 1px red; }
			</style>
		</head>

		<body>
			<h1>文字数カウント結果</h1>

			入力文字: %v<br>
			文字数は: %v<br>

			でした。<br><br>

			文字を入力してください。
			<form action="/count" method="GET">
			<input type="text" name="text" size="32" class="%v">
			<input type="submit">
			</form>
		</body>
	</html>`, text, count, css)

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

しかし、Goのドキュメントの例を見ると、fmt.Fprintfを使っていたので、簡単な文字列出力だったらこっちの方が確かに手軽だなぁなどと納得。

https://golang.org/pkg/net/http/#pkg-overview

http.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
})

Pythonでサロゲートペア -- ほっけの逆襲

Pythonサロゲートペアを処理するとどうなるか

http://qiita.com/YusukeHirao/items/2f0fb8d5bbb981101be0

既に他の記事で上記記事に登場する「𩸽」(ほっけ)というサロゲートペアの文字をGoで扱う話しを書いたが、Pythonで処理するとどうなるのか試してみた。

Python2は寛容

Python2は3に比べて寛容である。printに渡しても取り敢えずエラーにならず表示される。

>>> print u'\uD867\uDE3D'
𩸽
>>> x = u'\uD867\uDE3D'
>>> y = u'𩸽'

但し、表示される文字は同じだが、文字列比較では同値ではないと評価される

>>> x == y
False

更に不思議なことに、こちらはordが結果を返すのに

>>> ord(y)
171581

こちらはエラーになってしまう
>>> ord(x)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: ord() expected a character, but string of length 2 found


http://docs.python.jp/2/library/functions.html#ord

ord(c)(原文)

長さ 1 の与えられた文字列に対し、その文字列が unicode オブジェクトならば Unicode コードポイントを表す整数を、 8 ビット文字列ならばそのバイトの値を返します。たとえば、 ord('a') は整数 97 を返し、 ord(u'\u2020') は 8224 を返します。この値は 8 ビット文字列に対する chr() の逆であり、 unicode オブジェクトに対する unichr() の逆です。引数が unicodePython が UCS2 Unicode 対応版ならば、その文字のコードポイントは両端を含めて [0..65535] の範囲に入っていなければなりません。この範囲から外れると文字列の長さが 2 になり、 TypeError が送出されることになります。

サロゲートペアはlenで長さを調べると、「2」が返却される。
unicode型をlenに渡しているので「文字数」を取得するはずが、「2」を返す点が問題。
 str型をlenに渡すとバイト数を返すのでここでの問題対象外。

>>> len(x)
2
>>> len(y)
1

参考:

>>> len(u'あ')  # unicode型: 文字数
1
>>> len('あ')  # str型: バイト数
3

Unicode番号から文字を生成する」

当然、chrに渡すと範囲外なのでエラー。

>>> chr(0x29E3D)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: chr() arg not in range(256

unichrは正しく値を返してくれる。

>>> unichr(0x29E3D)
u'\U00029e3d'
>>> print unichr(0x29E3D)
𩸽

しかも、他の文字を試すとUbuntu16のVMでは化けて表示されるが、Macだとエラーになった。

★Ubuntu16.04のVM

f:id:imagawa_yakata:20161216105738p:plain

★El CapitalのMac

f:id:imagawa_yakata:20161216105742p:plain

El CapitanのMacでダメなのは、Pythonのビルドの条件のせいらしい。(以下)

Pythonでサロゲートペアの範囲の数値実体参照を文字に戻す - 西尾泰和のはてなダイアリー

Python3だと扱いが難しくなる

Python3ではそもそもu'\uD867\uDE3D'をprintに渡すとエラーになる。

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
UnicodeEncodeError: 'utf-8' codec can't encode character '\ud867' in position 0: surrogates not allowed

Python3は、最初からサロゲートペアを許さないポリシーのようだ。
そのため、'surrogatepass', 'suroogateescape'といったエラーオプションを指定して何とか解決する。

具体的には以下stackoverflowの回答の通り、UTF-16サロゲートペアを許可した状態で一度byteに変換し、strに戻すと大丈夫らしい。

How to work with surrogate pairs in Python? - Stack Overflow

>>> "\ud867\ude3d".encode('utf-16', 'surrogatepass').decode('utf-16')
'𩸽'
|<

ちなみに、Unicodeコードポイントでリテラル表記してあげれば大丈夫である。

>|text|
>>> print('\U00029e3d')
𩸽

サロゲートペアの入った文字列を処理する場合の注意点

サロゲートペア

「𩸽のひらきを居酒屋で注文して、1時間経つがまだ来ない。𠈻な客が店員を引き止めてなじるからだ。」

ほっけの ひらきを いざかやで ちゅうもんして、いちじかん たつが まだこない。
ぞくな きゃくが てんいんを ひきとめて なじる からだ。

年末の東京らしい風景を彷彿とさせるこの一文にはサロゲートペアの文字がふたつ含まれている。

ひとつは「𩸽」という文字
もうひとつは「𠈻」という文字である。

JavaScriptでのサロゲートペア文字列のメモ - Qiita
この記事に詳しく書かれているが、サロゲートペアは通常の文字列処理が通用しない厄介者である。

Goの文字列の扱い

Goでは文字をruneという型で処理する決まりらしい。
いつもの『プログラミング言語Go』の記述を引用すると、

Unicodeは世界のすべての書記体系のすべての文字、アクセントや他の発音区別記号、タブやキャリッジリターンなどの制御コード、多数の難解な文字を集めており、各文字にUnicodeコードポイント(Unicode code point)あるいはGo用語ではルーン(rune)と呼ばれる規格番号を割り当てています。

つまり、文字をruneとして取り出すと、番号で管理するそうだ。

実際、「ほっけ」と「ぞく」の文字をruneのリテラルで表すと、

package main

import "fmt"

func main() {
	var hokke, zok rune

	hokke, zok = '\U00029E3D', '\U0002023B'
	fmt.Printf("%c, %c", hokke, zok) // 𩸽, 𠈻
}

このように、16進数の番号を書く。

stringをrangeに渡すと[]runeに置き換えてくれる

Goでは文字列をrangeに渡すとちゃんとruneのスライスに置き換えてくれる。
このため、「一文字ずつ」ループを当たり前のようにできる。

exp := "𩸽のひらきを居酒屋で注文して、1時間経つがまだ来ない。𠈻な客が店員を引き止めてなじるからだ。"

for _, x := range exp {
	fmt.Printf("%c", x)
}
fmt.Println()

これで変数expに書いた通りの文章がコンソールに出力される。

もしこれをbyteのスライスにかえてしまうと都合が悪い。

exp := "𩸽のひらきを居酒屋で注文して、1時間経つがまだ来ない。𠈻な客が店員を引き止めてなじるからだ。" // 46文字

for _, x := range []byte(exp) {
	fmt.Printf("%c", x)
}

こっちを動かすと、画像のようにめちゃくちゃな出力になってしまう。

f:id:imagawa_yakata:20161216144357p:plain

utf8.DecodeRuneを使うとrange同様、安全に文字を取り出せる。

// import "unicode/utf8"

exp := "𩸽のひらきを居酒屋で注文して、1時間経つがまだ来ない。𠈻な客が店員を引き止めてなじるからだ。"
var xs []byte

xs = []byte(exp)
for len(xs) > 0 {
	r, size := utf8.DecodeRune(xs)
	fmt.Printf("%c", r)
	xs = xs[size:]
}
fmt.Println()

lenで文字数を数えたいか、バイト数を数えたいか

冒頭の文は全部で46文字あるが、単純に文字列をlenで調べてしまうとバイト数を返す。
これは、Pythonでlen(unicode)は文字数、len(str)はバイト数を返す話と似ている。

正しく文字数を取得したい場合は[]runeにキャストしてlenに渡すか、utf8.RuneCountInStringを使う。

exp := "𩸽のひらきを居酒屋で注文して、1時間経つがまだ来ない。𠈻な客が店員を引き止めてなじるからだ。" // 46文字

fmt.Println(exp)
fmt.Println(
	len(exp),                    // 138
	utf8.RuneCountInString(exp), // 46
	len([]rune(exp)))            // 46

サンプルコード

package main

import (
	"fmt"
	"unicode/utf8"
)

func main() {
	exp := "𩸽のひらきを居酒屋で注文して、1時間経つがまだ来ない。𠈻な客が店員を引き止めてなじるからだ。" // 46文字

	fmt.Println(exp)
	fmt.Println(
		len(exp),                    // 138
		utf8.RuneCountInString(exp), // 46
		len([]rune(exp)))            // 46

	for _, x := range exp {
		fmt.Printf("%c", x)
	}
	fmt.Println() // ここで改行を挟まないと何故か1回めのループの「𩸽」の文字が化けて表示される。

	var xs []byte

	xs = []byte(exp)
	for len(xs) > 0 {
		r, size := utf8.DecodeRune(xs)
		fmt.Printf("%c", r)
		xs = xs[size:]
	}
	fmt.Println()

	// DecodeLastRuneだと逆から反復
	xs = []byte(exp)
	for len(xs) > 0 {
		r, size := utf8.DecodeLastRune(xs)
		fmt.Printf("%c", r)
		xs = xs[:len(xs)-size]
	}
}

go installを使え -- QCon London 2016

2016年版Goのベストプラクティス

日本語訳(POSTD)
6年間におけるGoのベストプラクティス | プログラミング | POSTD

原著
Go best practices, six years in

こんな記事を見つけて読んでいた。

内容は15項目にもわたるプラクティスの紹介なのだが、今回、特に興味を持ったのが以下だった。

1. $GOPATH/binを$PATHに追加しましょう。インストールしたバイナリが扱いやすくなります。
2. リポジトリfooが主にバイナリ用なら、ライブラリコードはサブディレクトリlib/に置き、fooパッケージと名付けましょう。
3. リポジトリが主にライブラリ用なら、バイナリはcmd/配下の個別のサブディレクトリに置きましょう。
15. go buildよりもgo installを使いましょう。

GOPATH/binにPATHを通し、go installでビルドする

まず、15番の「$GOPATH/binを$PATHに追加 & go buildよりもgo installを使いましょう」という話に興味を持った。

今まで「go build」を実行するとバイナリが生成されることは知っていたが、「go install」というコマンドの存在を知らなかった。
「go install」を実行すると$GOPATH/binの下にバイナリが配置されるので、PATHを通しておけばいつでも実行できて便利ですよ、という意図らしい。

リポジトリ構成はgo getに対応を心がける

以下、バイナリとライブラリを作る場合でリポジトリ構成を変えた方が良いらしい。

[引用] バイナリを作る場合

github.com/peterbourgon/foo/
    main.go      // package main
    main_test.go // package main
    lib/
        foo.go      // package foo
        foo_test.go // package foo

[引用] ライブラリを作る場合

github.com/peterbourgon/foo
    foo.go      // package foo
    foo_test.go // package foo
    cmd/
        foo/
            main.go      // package main
            main_test.go // package main

この構成案は既に過去の記事で調べていたが、出典はこのカンファレンスだったのかという感想。

go getに対応したリポジトリ構成に、go installを加えることでシンプルに開発環境を構築することができそうだ。

go installを使えば、bin/の下にバイナリが配置されるので、go buildで生成したバイナリを誤ってコミットしてしまうミスも減ると思った。