Skip to content

Web 画面を実装

html/templateを使って html コンテンツを返却する Go アプリを作成します。

ディレクトリ構成

.
├── assets
│   ├── static
│      ├── index.js
│      └── style.css
│   └── templates
│       └── index.html
├── cmd
│   └── main.go
├── go.mod
└── internal
    └── handler
        ├── auth_handler.go
        └── router
            ├── auth_router.go
            └── base.go

サンプルコード

擬似的なログイン画面を実装します。

設計

  • ログイン画面表示時に、ボタン名を go から html に渡す
  • ボタン入力時に js から go の関数を呼び出し、API をコール
  • 結果を alert 表示

実装

assets/

  • templates/index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <link rel="stylesheet" href="static/style.css" type="text/css" />
    <link rel="icon" href="static/favicon.ico" />
    <title>First Go web</title>
  </head>

  <body>
    <div class="name-wrapper">
      <input
        class="name-textbox"
        type="text"
        placeholder="ユーザ名"
        id="name"
      />
    </div>
    <div class="password-wrapper">
      <input
        class="password-textbox"
        type="password"
        placeholder="パスワード"
        id="password"
      />
    </div>
    <div class="submit-button-wrapper">
      <button class="submit-button" onclick="handleOnclickButton()">
        {{ .ButtonText }}
      </button>
    </div>

    <script src="static/index.js"></script>
  </body>
</html>
  • static/index.js
function handleOnclickButton() {
  const name = document.getElementById("name").value;
  const password = document.getElementById("password").value;
  fetch("/api/v1/login", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      name: name,
      password: password,
    }),
  })
    .then((response) => response.json())
    .then((data) => {
      alert(data.message);
    })
    .catch((error) => {
      console.log(error);
    });
}
  • static/style.css
body {
  padding: 30px 60px 30px 60px;
  color: #d6d6d6;
  background-color: #000333;
  text-align: center;
}

.name-wrapper {
  padding: 30px 30px 5px 30px;
}

.name-textbox {
  width: 250px;
  height: 35px;
}

.password-wrapper {
  padding: 30px 30px 2px 30px;
}

.password-textbox {
  width: 250px;
  height: 35px;
}

.submit-button-wrapper {
  padding: 30px 30px 2px 30px;
}

.submit-button {
  width: 260px;
  height: 35px;
  background-color: orange;
}

.submit-button:hover {
  cursor: pointer;
}

internal/handler/

  • auth_handler.go
package handler

import (
    "encoding/json"
    "html/template"
    "net/http"
)

// 認証機能のhandlerです。
type AuthHandler interface {

    // 初期表示処理を行います。
    InitView(w http.ResponseWriter, r *http.Request)

    // ログイン処理を行います。
    Login(w http.ResponseWriter, r *http.Request)
}

type authHandler struct{}

func NewAuthHandler() AuthHandler {
    return &authHandler{}
}

func (h *authHandler) InitView(w http.ResponseWriter, r *http.Request) {

    tmpl, err := template.ParseFiles("assets/templates/index.html")
    if err != nil {
        http.Error(w, "Template parsing error", http.StatusInternalServerError)
        return
    }

    // 画面表示用の構造体を作成
    initView := struct{ ButtonText string }{ButtonText: "ログイン"}

    err = tmpl.Execute(w, initView)
    if err != nil {
        http.Error(w, "Template execution error", http.StatusInternalServerError)
    }
}

func (h *authHandler) Login(w http.ResponseWriter, r *http.Request) {

    var req struct {
        Name     string `json:"name"`     // ユーザ名
        Password string `json:"password"` // パスワード
    }
    decoder := json.NewDecoder(r.Body)
    if err := decoder.Decode(&req); err != nil {
        http.Error(w, "Bad request", http.StatusBadRequest)
        return
    }

    if req.Name == "" || req.Password == "" {
        w.Header().Set("Content-Type", "application/json")
        w.WriteHeader(http.StatusUnprocessableEntity)
        json.NewEncoder(w).Encode(struct {
            Message string `json:"message"` // メッセージ
        }{
            Message: "Input your credentials",
        })
        return
    }

    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusOK)
    json.NewEncoder(w).Encode(struct {
        Message string `json:"message"` // メッセージ
    }{
        Message: "Hello, " + req.Name + "!",
    })
}
  • router/base.go
package router

import (
    "net/http"
)

// routerのインターフェースです。
type Router interface {

    // ルーティング情報をセットします。
    SetRouting(m *http.ServeMux)
}

// APIのベースURI
const basePath string = "/api/v1"

// ルーティングを設定します。
func Routing() *http.ServeMux {

    // 各handlerに紐づくルーティングを設定
    m := http.NewServeMux()

    // static配下をルーティング
    m.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("assets/static"))))

    // auth
    NewAuthRouter().SetRouting(m)

    return m
}
  • router/auth_router.go
package router

import (
    "easyapp/internal/handler"
    "net/http"
)

type authRouter struct{}

func NewAuthRouter() Router {
    return &authRouter{}
}

func (r *authRouter) SetRouting(m *http.ServeMux) {

    h := handler.NewAuthHandler()

    // カスタムルータ
    m.HandleFunc(basePath+"/login", func(w http.ResponseWriter, r *http.Request) {
        switch r.Method {
        case http.MethodPost:
            h.Login(w, r)
        default:
            http.Error(w, "Forbidden", http.StatusForbidden)
        }
    })
    m.HandleFunc("/login", h.InitView)
}

cmd/

  • main.go
package main

import (
    "easyapp/internal/handler/router"
    "fmt"
    "log"
    "net/http"
)

func main() {

    fmt.Println("Server started at http://localhost:8080")
    log.Fatal(http.ListenAndServe(":8080", router.Routing()))
}

アプリ起動後、http://localhost:8080/login にアクセスするとログイン画面が表示されます。

静的コンテンツをバイナリに含める

上記のコードではgo buildで作成したバイナリファイルに html などのコンテンツは含まれません。これらもバイナリに含める場合は下記のようにコードを修正します:

  • assets/assets.go を下記で新規作成
package assets

import "embed"

//go:embed static/*
var Static embed.FS // static埋め込み宣言

//go:embed templates/*
var Templates embed.FS // templates埋め込み宣言
  • router/base.go について、埋め込んだ static を使うよう宣言
    staticFiles, err := fs.Sub(assets.Static, "static")
    if err != nil {
        log.Fatalf("Failed to create sub filesystem: %v", err)
    }
    m.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(staticFiles))))
  • handler/auth_handler.go について、埋め込んだ templates を使うよう宣言
    tmpl, err := template.ParseFS(assets.Templates, "templates/index.html")
    if err != nil {
        http.Error(w, "Template parsing error", http.StatusInternalServerError)
        return
    }