一から勉強させてください

最下級エンジニアが日々の学びをアウトプットしていくだけのブログです

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)
}
  1. templatesを定義してこの中で必要なテンプレートファイルをパースしておく。別ファイルに定義したテンプレートを読み込むにはtemplate.ParseFilesを使う。template.Mustはバリデーションチェックで、指定したテンプレートのエラーがnilじゃない場合にpanicを呼ぶっぽい。templatestype Templateのポインタ。

  2. renderTemplateメソッドを定義して、その中でtemplates.ExecuteTemplateを呼んで該当するテンプレートを表示。エラーがあった場合はlog.Fatallnを呼ぶ。ここに処理が入った場合はexit(1)で異常終了する。

  3. http.HandleFuncfunc HandleFunc(pattern string, handler func(ResponseWriter, *Request))となっていて、パターンとハンドラメソッドを受け取るようになっているので/のリクエストでIndexHandlerを実行するようにする。

  4. http.ListenAndServe8888ポートでサーバ起動。

ひとまずこんな感じでgo run app.goしたらlocalhost:8888index.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)
}
  1. まずUploadHandlerで受け付けるのはPOSTだけなのでGETなどが来た場合は405 Method Not Allowedエラーを返す。

  2. multipart/form-dataなファイルのアップロード処理をするためにはまずr.ParseMultipartFormを呼ぶ必要がある。これのインターフェースはfunc (r *Request) ParseMultipartForm(maxMemory int64) errorとなっていて、アップロードするファイルはmaxMemoryのサイズのメモリに保存される。もしファイルのサイズがmaxMemoryを超えた場合、残った部分はシステムのテンポラリファイルに保存される。

  3. エラーハンドリングはとりあえずエラーが起きたらhttp.StatusInternalServerErrorを返すようにする。

  4. r.FormFileで、2 で用意したファイルハンドルを取得することができる。今回は HTML の Form のほうでuploadという名前でデータを送信しているのでその名前で取得する。また、defer file.Close()を呼んでおくことでUploadHandlerの処理を抜ける前に必ずファイルのクローズ処理が行われる事が保証できる。defer便利。

  5. os.Create("/tmp/test.jpg")/tmp以下にtest.jpgというファイルを作ります(雑につくったので JPG しか対応していません)。このos.Createの他にos.OpenFileを使っている例も見受けられたけど、Createは内部でOpenFileを使っていたのでやってることは同じっぽい。これもdefer f.Close()でちゃんと閉じる。

  6. io.Copyでフォームからアップロードされたデータが/tmp/test.jpgとして保存される。

  7. /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)
}
  1. まずos.Openで先ほどの画像を取り出す。Openも内部でOpenFileを使っている。こちらもクローズ処理を忘れないようにする。

  2. image.Decodeで取り出したファイルをデコードする。

  3. writeImageWithTemplateというメソッドを定義。こちらに取り出した Image を渡して、いい感じにテンプレートに出力するよう試みる。

  4. new(bytes.Buffer)で処理を行うためのメモリ領域を確保する。そしてjpeg.Encodeにそれと*imgを渡してエンコード処理。

  5. 4 でできあがったものを base64 形式にしてstringに変換。それをテンプレートに渡す。data := map[string]interface{}{"Title": tmpl, "Image": str}は Key がstringValueinterface{}にしているけど、これは Swift でAnyObjectを渡すみたいなノリなんだろうなと思っている。

これで画像が埋め込まれた形でテンプレートがちゃんと表示できた!

まとめ

Go は標準パッケージが充実しているなーと思った。今まで触ってきた言語と結構違っていてなかなか慣れないけど、楽しい。

次はゴルーチンとかチャネルとか絡めたものを作りたいなー

参考