Closed5
goのhttp.FileServerでフロントエンドをシンプルに表現する

最終コード
- apiはjsonを返すだけ、htmlには依存しない
-
go run main.go
するとhtmlへアクセス出来る - go buildすることで配布も容易
ディレクトリ構成
.
├── .air.toml
├── main.go
├── static
│ ├── app.js
│ ├── index.html
│ └── styles.css
コード
コード群
main.go
main.go
package main
import (
"embed"
"encoding/json"
"io/fs"
"log"
"net/http"
)
//go:embed static/*
var staticFiles embed.FS
func main() {
// APIエンドポイント - シンプルなデータのみを返す
http.HandleFunc("/api/data", func(w http.ResponseWriter, r *http.Request) {
// CORSヘッダーを設定
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Content-Type", "application/json")
// データ配列のみを返す
items := []string{"項目1", "項目2", "項目3", "テスト"}
json.NewEncoder(w).Encode(items)
})
// 埋め込まれた静的ファイルを提供
// 注: staticFiles は "static" ディレクトリを含むので、Sub を使って中身だけ取得
staticContent, err := fs.Sub(staticFiles, "static")
if err != nil {
log.Fatal("静的ファイルのサブツリー取得に失敗:", err)
}
// 静的ファイルをルートパスで提供
http.Handle("/", http.FileServer(http.FS(staticContent)))
// サーバー起動
port := ":8080"
log.Printf("サーバーを開始: http://localhost%s", port)
log.Printf("静的ファイルはバイナリに埋め込まれています")
log.Fatal(http.ListenAndServe(port, nil))
}
html
index.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Goバックエンド + 静的フロントエンド</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="container">
<h1>Goバックエンド + 静的フロントエンド</h1>
<p>シンプルな構成でウェブアプリケーションを構築</p>
<div class="card">
<h2>APIデータ表示</h2>
<div id="api-message" class="message-box">データ取得中...</div>
<ul id="api-items" class="items-list"></ul>
<button id="fetch-button">APIからデータを取得</button>
</div>
</div>
<script src="app.js"></script>
</body>
</html>
css
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Helvetica Neue', Arial, sans-serif;
line-height: 1.6;
color: #333;
background-color: #f5f5f5;
}
.container {
max-width: 800px;
margin: 2rem auto;
padding: 1rem;
}
h1 {
margin-bottom: 1rem;
color: #2c3e50;
}
.card {
background-color: white;
border-radius: 8px;
padding: 1.5rem;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
margin: 2rem 0;
}
.message-box {
margin: 1rem 0;
padding: 1rem;
background-color: #f0f7ff;
border-left: 4px solid #3498db;
border-radius: 4px;
}
.items-list {
margin: 1rem 0;
padding-left: 1.5rem;
}
.items-list li {
margin-bottom: 0.5rem;
}
button {
background-color: #3498db;
color: white;
border: none;
padding: 0.75rem 1.5rem;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
transition: background-color 0.2s;
}
button:hover {
background-color: #2980b9;
}
js
document.addEventListener('DOMContentLoaded', () => {
const apiMessageElement = document.getElementById('api-message');
const apiItemsElement = document.getElementById('api-items');
const fetchButton = document.getElementById('fetch-button');
// メッセージをフロントエンドで定義
const messageText = "フロントエンドで定義したメッセージ";
// ボタンクリック時のデータ取得処理
fetchButton.addEventListener('click', fetchDataFromApi);
// APIからデータを取得する関数
async function fetchDataFromApi() {
apiMessageElement.textContent = 'データ取得中...';
apiItemsElement.innerHTML = '';
try {
const response = await fetch('/api/data');
if (!response.ok) {
throw new Error(`APIエラー: ${response.status}`);
}
// データは直接配列で返ってくる
const items = await response.json();
// メッセージを表示(フロントエンドで定義したもの)
apiMessageElement.textContent = messageText;
// 項目リストを表示
if (items && items.length > 0) {
items.forEach(item => {
const li = document.createElement('li');
li.textContent = item;
apiItemsElement.appendChild(li);
});
} else {
apiItemsElement.innerHTML = '<li>表示する項目がありません</li>';
}
} catch (error) {
apiMessageElement.textContent = `エラーが発生しました: ${error.message}`;
console.error('APIデータ取得エラー:', error);
}
}
// 初回表示時に自動的にデータを取得
fetchDataFromApi();
});

モチベーション
- goで作ったAPIにフロントエンドを作成したいが、以下を気にしている
- reactなどのフレームワークを使うほどではないため、シンプルにhtml/css/javascriptだけで済ましたい
-
go run main.go
したらフロントエンドも立ち上がって欲しい - 将来フロントエンドを別に作るかも知れないので、html/templateなどに依存するのは避けたい

http.fileserverを使う
func main() {
// APIエンドポイント - 単純なデータのみを返す
http.HandleFunc("/api/data", func(w http.ResponseWriter, r *http.Request) {
// CORSヘッダーを設定
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Content-Type", "application/json")
// データ配列のみを返す(メッセージなし)
items := []string{"項目1", "項目2", "項目3", "項目4", "項目5"}
json.NewEncoder(w).Encode(items)
})
// 静的ファイルの提供
fs := http.FileServer(http.Dir("./static"))
http.Handle("/", fs)
// サーバー起動
port := ":8080"
log.Printf("サーバーを開始: http://localhost%s", port)
log.Fatal(http.ListenAndServe(port, nil))
}
こうすることでstaticディレクトリの静的ファイルへアクセス可能
airを入れることでホットリロードも可能
.
├── main.go
├── static # ここへ静的ファイルを入れる
│ ├── app.js
│ ├── index.html
│ └── styles.css

go buildは静的ファイルを含めない
らしいです
前述のコードで試したところ
- staticディレクトリが実行ファイルと同じ階層にある
- 正常動作
- 〃にない
- 404で動作しない
となる。ローカルのstaticディレクトリを見に行ってしまうためである
fs := http.FileServer(http.Dir("./static"))
http.Handle("/", fs)

embedを使う
ことで静的ファイルも含められるとのこと
コードをこんな感じにする
main.go
package main
import (
"embed"
"encoding/json"
"io/fs"
"log"
"net/http"
)
//go:embed static/*
var staticFiles embed.FS
func main() {
// APIエンドポイント - シンプルなデータのみを返す
http.HandleFunc("/api/data", func(w http.ResponseWriter, r *http.Request) {
// CORSヘッダーを設定
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Content-Type", "application/json")
// データ配列のみを返す
items := []string{"項目1", "項目2", "項目3", "テスト"}
json.NewEncoder(w).Encode(items)
})
// 埋め込まれた静的ファイルを提供
// 注: staticFiles は "static" ディレクトリを含むので、Sub を使って中身だけ取得
staticContent, err := fs.Sub(staticFiles, "static")
if err != nil {
log.Fatal("静的ファイルのサブツリー取得に失敗:", err)
}
// 静的ファイルをルートパスで提供
http.Handle("/", http.FileServer(http.FS(staticContent)))
// サーバー起動
port := ":8080"
log.Printf("サーバーを開始: http://localhost%s", port)
log.Printf("静的ファイルはバイナリに埋め込まれています")
log.Fatal(http.ListenAndServe(port, nil))
}
と解消した。これで実行ファイルやらコンテナイメージやらで配布しやすくなる
このスクラップは4ヶ月前にクローズされました