Go httpサーバー＆リソースをワンバイナリにする

Go言語で 標準package のみを使用して、
httpサーバーを作成するのをやってみたいと思います。

ただ httpサーバー を立てるだけだと
すぐできてしまいそうなので、今回は `go:embed` を使ってみたいと思います。

## go:embed とは

httpサーバーを立てて何かページを返すようなものを作ろうと思っています。
あとリソースは、外部アクセスさせずにローカルで完結するようにします。

そのためには、リソースをローカルで持たなければいけません。

例えば、画像・CSS・JavaScript などのファイルとか。。。

実行体(exe)と同じところにファイルを置いて、
exeから読み込んで返してもらってもよいのですが
ファイルを入れ忘れるとかがあると面倒なので・・・ワンバイナリにしたい！

そんな時に使えるのが `go:embed` です。
リソースも実行体に含めてワンバイナリにしてくれます。

Go 1.16 からリリースされたので、それ以上で使えます！

使い方もそんなに難しくないので、さっそくやってみます。

## httpサーバーを立てる

まずは、httpサーバーを作ります。

```go:server.go
package main

import (
	"fmt"
	"log"
	"net/http"
)

func main() {
	http.HandleFunc("/", indexPage)

	err := http.ListenAndServe(":3000", nil)
	if err != nil {
		log.Fatal(err)
	}
}

func indexPage(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "Hello, World.")
}
```

あとは `go build` して 出来上がったexe をコマンドプロンプトから実行する。
※最初は`go mod`も作成してください

実行したあとに、ブラウザで `http://localhost:3000` にアクセスしてみましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/238600/2bc6ae47-90d3-09f9-889c-e8de531a995c.png)

文字が表示されました！

ちなみに実行を止めるときは、
コマンドプロンプト上で Ctrl + C を押すと実行を停止できます。

## ページを作成する

ちょっと もの寂しいので、ページを作って返してみます。

Bootstrap4 でなんとなく作ります。

https://getbootstrap.com/docs/4.6/getting-started/download/

`Compiled CSS and JS` の `Download` からファイルを一式(zip)落としてきます。

あと jQuery も必要なので、落としてきます。

https://jquery.com/download/

`Downloading jQuery` の `Download the compressed, production jQuery 3.6.0` あたりからファイル(js)を落としてきます。

落としたファイル類は、解凍して ローカルの`./static` に入れます。

あと`./templates` に ページのテンプレート を作って入れます。

```html:index.tmpl
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>{{ .Title }}</title>
  <link rel="stylesheet" href="/static/css/bootstrap.min.css">
  <script src="/static/js/jquery-3.6.0.min.js"></script>
  <script src="/static/js/bootstrap.bundle.min.js"></script>
</head>
<body>

<div class="container mt-2">
  <div class="card">
    <div class="card-header">
      Profile
    </div>
    <div class="card-body">
      <h5 class="card-title">hiro</h5>
      <p class="card-text">
        見習いエンジニア Go / Perl など勉強中
      </p>
    </div>
  </div>
  </div>
</body>
</html>
```

先ほど作成したGoのソースコードをベースに少し変更します。

テンプレートを読み取って、ページを返すようにします。
あとリソースもローカルから返すように変更します。

```go:server.go
package main

import (
	"log"
	"net/http"
	"text/template"
)

func main() {
	http.HandleFunc("/", indexPage)

	// ローカルからファイルを返す
	http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))

	err := http.ListenAndServe(":3000", nil)
	if err != nil {
		log.Fatal(err)
	}
}

func indexPage(w http.ResponseWriter, r *http.Request) {
	// テンプレートを読み取ってページを作成する
	t, err := template.ParseFiles("templates/index.tmpl")
	if err != nil {
		log.Fatalln(err)
	}

	err = t.Execute(w, struct {
		Title string
	}{
		Title: "プロフィール",
	})
	if err != nil {
		log.Println(err)
	}
}
```

最終的にはこんな構成になるはず。

```text:ファイル構成
./
│  go.mod
│  server.go
│  
├─static
│  ├─css
│  │      bootstrap-grid.css
│  │      bootstrap-grid.css.map
│  │      bootstrap-grid.min.css
│  │      bootstrap-grid.min.css.map
│  │      bootstrap-reboot.css
│  │      bootstrap-reboot.css.map
│  │      bootstrap-reboot.min.css
│  │      bootstrap-reboot.min.css.map
│  │      bootstrap.css
│  │      bootstrap.css.map
│  │      bootstrap.min.css
│  │      bootstrap.min.css.map
│  │      
│  └─js
│          bootstrap.bundle.js
│          bootstrap.bundle.js.map
│          bootstrap.bundle.min.js
│          bootstrap.bundle.min.js.map
│          bootstrap.js
│          bootstrap.js.map
│          bootstrap.min.js
│          bootstrap.min.js.map
│          jquery-3.6.0.min.js
│          
└─templates
        index.tmpl
```

`go build` して アクセスしてみましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/238600/6d3cfce4-5137-2138-4636-0c27c4adb548.png)

ページが表示できました！

この状態でも、
exeファイル と `static`フォルダ と `templates`フォルダ を持っていけば動くのですが
`go:embed`でワンバイナリにしてみましょう。

## go:embed でファイルを埋め込む

`go:embed` で指定してあげて、少し変更するだけ 簡単に使用できます！

```go:server.go
package main

import (
	"embed"
	"log"
	"net/http"
	"text/template"
)

//go:embed static
var static embed.FS

//go:embed templates
var templates embed.FS

func main() {
	http.HandleFunc("/", indexPage)

	// http.FS に変更するだけ
	http.Handle("/static/", http.FileServer(http.FS(static)))

	err := http.ListenAndServe(":3000", nil)
	if err != nil {
		log.Fatal(err)
	}
}

func indexPage(w http.ResponseWriter, r *http.Request) {
	// ParseFS に変更するだけ
	t, err := template.ParseFS(templates, "templates/index.tmpl")
	if err != nil {
		log.Fatalln(err)
	}

	err = t.Execute(w, struct {
		Title string
	}{
		Title: "プロフィール",
	})
	if err != nil {
		log.Println(err)
	}
}
```

`//go:embed <ディレクトリ or ファイル>`で指定してあげる。
※`//`の後ろは半角スペースあけない！
そしてその直下に変数を定義する。

これだけで`FileSystem`な変数ができちゃいます。

あとは `go build`すれば、ワンバイナリになるので
リソースとなるファイル類がなくても、実行体だけを持って行って実行するだけ。

テンプレート側はソースコード内で指定する必要がありますが、
static側はファイルを置いて、ページから読み込むだけという感じで使えますね！

## まとめ

今回初めて `go:embed` を使用してみましたが、
簡単に使えて便利です！

今回紹介したもの以外にも
いろいろな機会に使用できると思うので、みなさん使ってみてくださいね！