Go で静的ファイルを配信する:http.FileServer の使い方

Go の標準ライブラリには http.FileServer というハンドラが用意されており、HTML・CSS・JavaScript・画像などの静的ファイルを簡単に配信できます。フロントエンドのビルド成果物を Go サーバーから配信したい場合に重宝します。

基本的な使い方

http.FileServerhttp.Dir でディレクトリを指定するだけで、そのディレクトリ内のファイルを配信できます。

package main

import (
	"fmt"
	"net/http"
)

func main() {
	fs := http.FileServer(http.Dir("./static"))
	http.Handle("/", fs)

	fmt.Println("サーバー起動: http://localhost:8080")
	http.ListenAndServe(":8080", nil)
}

./static ディレクトリに index.html を置いておけば、http://localhost:8080/ でそのファイルが表示されます。./static/css/style.css なら http://localhost:8080/css/style.css でアクセスできるという具合です。

ファイルの MIME タイプは拡張子から自動判定されます。.html なら text/html.css なら text/css.js なら application/javascript が Content-Type として設定されます。

パスプレフィックスを付ける

静的ファイルを /static/ 配下で配信したい場合は http.StripPrefix と組み合わせます。

fs := http.FileServer(http.Dir("./public"))
http.Handle("/static/", http.StripPrefix("/static/", fs))

http.StripPrefix は URL パスからプレフィックスを取り除く役割を果たします。/static/css/style.css というリクエストが来ると、プレフィックス /static/ が除去され、./public/css/style.css というファイルが探索されます。

リクエスト: /static/css/style.css

StripPrefix が /static/ を除去

FileServer が ./public/css/style.css を配信

StripPrefix を忘れると、FileServer は ./public/static/css/style.css を探しに行ってしまい、404 が返ります。プレフィックスとディレクトリのマッピングは慎重に設定しましょう。

API と静的ファイルを共存させる

実際のアプリケーションでは、API エンドポイントと静的ファイル配信を同じサーバーで動かすことが多いです。

func main() {
	mux := http.NewServeMux()

	// API エンドポイント
	mux.HandleFunc("/api/hello", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		w.Write([]byte(`{"message":"Hello!"}`))
	})

	// 静的ファイル
	fs := http.FileServer(http.Dir("./static"))
	mux.Handle("/", fs)

	fmt.Println("サーバー起動: http://localhost:8080")
	http.ListenAndServe(":8080", mux)
}

API のルートを先に登録し、/ のハンドラを最後に登録するのがポイントです。Go の ServeMux は長いパスを先にマッチさせるため、/api/hello/ より優先されます。

ディレクトリリスティングを無効にする

http.FileServer はデフォルトでディレクトリの一覧表示が有効になっています。index.html がないディレクトリにアクセスすると、ファイル一覧が表示されてしまいます。

セキュリティ上の理由から、これを無効にしたい場合は http.FileSystem を自作します。

type noListingFS struct {
	fs http.FileSystem
}

func (nfs noListingFS) Open(name string) (http.File, error) {
	f, err := nfs.fs.Open(name)
	if err != nil {
		return nil, err
	}

	info, err := f.Stat()
	if err != nil {
		f.Close()
		return nil, err
	}

	if info.IsDir() {
		index := name + "/index.html"
		if _, err := nfs.fs.Open(index); err != nil {
			f.Close()
			return nil, err
		}
	}

	return f, nil
}

この noListingFS をラップして使います。

fs := http.FileServer(noListingFS{http.Dir("./static")})
http.Handle("/", fs)

ディレクトリにアクセスされたとき、index.html が存在しなければエラーを返す仕組みです。結果として 404 が返り、ファイル一覧は表示されません。

デフォルトの FileServer

ディレクトリ一覧が見えてしまう。内部構造が外部に漏れるリスクがある。

noListingFS でラップ

index.html がないディレクトリは 404 を返す。ファイル構成を隠蔽できる。

embed で静的ファイルをバイナリに埋め込む

Go 1.16 以降では embed パッケージを使って、静的ファイルをバイナリに埋め込めます。デプロイ時にファイルをコピーする手間が省け、単一バイナリで配信できるのが魅力です。

import "embed"

//go:embed static/*
var staticFiles embed.FS

func main() {
	fs := http.FileServer(http.FS(staticFiles))
	http.Handle("/", fs)
	http.ListenAndServe(":8080", nil)
}

//go:embed ディレクティブでファイルやディレクトリを指定すると、コンパイル時にバイナリに含まれます。http.FSembed.FShttp.FileSystem に変換すれば、あとは通常の FileServer と同じように使えます。

小規模な管理画面や SPA(Single Page Application)のフロントエンドを Go のバイナリに同梱したい場面で特に便利な手法です。