Golangで画像をアップロードして表示するだけのアプリをつくってみた
完全に出遅れた感満載だけど、最近 Golang の勉強を始めました。
ちょうど昔、node.js の勉強で画像をアップロードして表示するだけのアプリをつくったことがあったので、今回はそれの Golang 版をつくってみました。
成果物はこちら。
初期表示
とりあえず/
にアクセスしたら画像をアップロードするためのフォームと送信ボタンを表示できるようにしてみる。
Golang は標準でテンプレート機能をサポートしているっぽかったのでそれを使ってみる。
templates/index.html
を用意して以下のようにする。
<!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <title>{{ .Title |html }}</title> </head> <body> <form action="/upload" enctype="multipart/form-data" method="post"> <input type="file" name="upload" id="upload" multiple="multiple"> <input type="submit" value="Upload file" /> </form> </body> </html>
完全にただの HTML だったので、テンプレ感を出すために{{ .Title }}
の部分にタイトルの情報を渡して表示するようにしてみた。たぶんこんなの必要ない。
次にapp.go
を用意して、次のようにしてみる。(一部抜粋)
var templates = template.Must(template.ParseFiles("templates/index.html")) func IndexHandler(w http.ResponseWriter, r *http.Request) { data := map[string]interface{}{"Title": "index"} renderTemplate(w, "index", data) } func renderTemplate(w http.ResponseWriter, tmpl string, data interface{}) { if err := templates.ExecuteTemplate(w, tmpl+".html", data); err != nil { log.Fatalln("Unable to execute template.") } } func main() { http.HandleFunc("/", IndexHandler) http.ListenAndServe(":8888", nil) }
templates
を定義してこの中で必要なテンプレートファイルをパースしておく。別ファイルに定義したテンプレートを読み込むにはtemplate.ParseFiles
を使う。template.Must
はバリデーションチェックで、指定したテンプレートのエラーがnil
じゃない場合にpanic
を呼ぶっぽい。templates
はtype Template
のポインタ。renderTemplate
メソッドを定義して、その中でtemplates.ExecuteTemplate
を呼んで該当するテンプレートを表示。エラーがあった場合はlog.Fatalln
を呼ぶ。ここに処理が入った場合はexit(1)
で異常終了する。http.HandleFunc
はfunc HandleFunc(pattern string, handler func(ResponseWriter, *Request))
となっていて、パターンとハンドラメソッドを受け取るようになっているので/
のリクエストでIndexHandler
を実行するようにする。http.ListenAndServe
で8888
ポートでサーバ起動。
ひとまずこんな感じでgo run app.go
したらlocalhost:8888
にindex.html
の内容が表示できた。
画像をアップロード
次に画像をアップロードする処理をUploadHandler
で書いていく。
func UploadHandler(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { http.Error(w, "Allowed POST method only", http.StatusMethodNotAllowed) return } err := r.ParseMultipartForm(32 << 20) // maxMemory if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } file, _, err := r.FormFile("upload") if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } defer file.Close() f, err := os.Create("/tmp/test.jpg") if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } defer f.Close() io.Copy(f, file) http.Redirect(w, r, "/show", http.StatusFound) } func main() { http.HandleFunc("/", IndexHandler) http.HandleFunc("/upload", UploadHandler) http.ListenAndServe(":8888", nil) }
まず
UploadHandler
で受け付けるのはPOST
だけなのでGET
などが来た場合は405 Method Not Allowed
エラーを返す。multipart/form-data
なファイルのアップロード処理をするためにはまずr.ParseMultipartForm
を呼ぶ必要がある。これのインターフェースはfunc (r *Request) ParseMultipartForm(maxMemory int64) error
となっていて、アップロードするファイルはmaxMemory
のサイズのメモリに保存される。もしファイルのサイズがmaxMemory
を超えた場合、残った部分はシステムのテンポラリファイルに保存される。エラーハンドリングはとりあえずエラーが起きたら
http.StatusInternalServerError
を返すようにする。r.FormFile
で、2 で用意したファイルハンドルを取得することができる。今回は HTML の Form のほうでupload
という名前でデータを送信しているのでその名前で取得する。また、defer file.Close()
を呼んでおくことでUploadHandler
の処理を抜ける前に必ずファイルのクローズ処理が行われる事が保証できる。defer
便利。os.Create("/tmp/test.jpg")
で/tmp
以下にtest.jpg
というファイルを作ります(雑につくったので JPG しか対応していません)。このos.Create
の他にos.OpenFile
を使っている例も見受けられたけど、Create
は内部でOpenFile
を使っていたのでやってることは同じっぽい。これもdefer f.Close()
でちゃんと閉じる。io.Copy
でフォームからアップロードされたデータが/tmp/test.jpg
として保存される。/show
にリダイレクトして、/tmp/test.jpg
の内容を表示する
アップロードした画像を表示
アップロードして保存した画像を表示する。
func ShowHandler(w http.ResponseWriter, r *http.Request) { file, err := os.Open("/tmp/test.jpg") defer file.Close() if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } img, _, err := image.Decode(file) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } writeImageWithTemplate(w, "show", &img) } func writeImageWithTemplate(w http.ResponseWriter, tmpl string, img *image.Image) { buffer := new(bytes.Buffer) if err := jpeg.Encode(buffer, *img, nil); err != nil { log.Fatalln("Unable to encode image.") } str := base64.StdEncoding.EncodeToString(buffer.Bytes()) data := map[string]interface{}{"Title": tmpl, "Image": str} renderTemplate(w, tmpl, data) } func main() { http.HandleFunc("/", IndexHandler) http.HandleFunc("/upload", UploadHandler) http.HandleFunc("/show", ShowHandler) http.ListenAndServe(":8888", nil) }
まず
os.Open
で先ほどの画像を取り出す。Open
も内部でOpenFile
を使っている。こちらもクローズ処理を忘れないようにする。image.Decode
で取り出したファイルをデコードする。writeImageWithTemplate
というメソッドを定義。こちらに取り出した Image を渡して、いい感じにテンプレートに出力するよう試みる。new(bytes.Buffer)
で処理を行うためのメモリ領域を確保する。そしてjpeg.Encode
にそれと*img
を渡してエンコード処理。4 でできあがったものを base64 形式にして
string
に変換。それをテンプレートに渡す。data := map[string]interface{}{"Title": tmpl, "Image": str}
は Key がstring
、Value がinterface{}
にしているけど、これは Swift でAnyObject
を渡すみたいなノリなんだろうなと思っている。
これで画像が埋め込まれた形でテンプレートがちゃんと表示できた!
まとめ
Go は標準パッケージが充実しているなーと思った。今まで触ってきた言語と結構違っていてなかなか慣れないけど、楽しい。
次はゴルーチンとかチャネルとか絡めたものを作りたいなー