完成コードと動作確認
読了目安 約3分
完成した main.go の全体と、登録からログアウトまでを通しで確かめる手順です。
- 登録とログイン 本人を確認
- 投稿 DBへ保存
- ログアウト セッションを破棄
この章の目次
完成した main.go の全体(約 200 行)を載せます。 index.html は前章のもの、register.html と login.html は 3 章のままです。
package main
import (
"crypto/rand"
"database/sql"
"encoding/hex"
"errors"
"html/template"
"log"
"net/http"
"sync"
"time"
"github.com/go-sql-driver/mysql"
"golang.org/x/crypto/bcrypt"
)
var (
db *sql.DB
templates = template.Must(template.ParseGlob("templates/*.html"))
sessions = map[string]int64{} // セッション ID → ユーザー ID
sessionsMu sync.Mutex
)
type Post struct {
ID int64
UserName string
Body string
CreatedAt time.Time
}
func main() {
var err error
db, err = sql.Open("mysql", "root:neziki@tcp(127.0.0.1:3306)/neziki_board?parseTime=true")
if err != nil {
log.Fatal(err)
}
if err := db.Ping(); err != nil {
log.Fatal(err)
}
mux := http.NewServeMux()
mux.HandleFunc("GET /{$}", handleIndex)
mux.HandleFunc("GET /register", handleRegisterForm)
mux.HandleFunc("POST /register", handleRegister)
mux.HandleFunc("GET /login", handleLoginForm)
mux.HandleFunc("POST /login", handleLogin)
mux.HandleFunc("POST /logout", handleLogout)
mux.HandleFunc("POST /posts", handleCreatePost)
log.Println("http://localhost:8080 で待ち受けます")
log.Fatal(http.ListenAndServe(":8080", mux))
}
func serverError(w http.ResponseWriter, err error) {
log.Println(err)
http.Error(w, "サーバー内部でエラーが起きました", http.StatusInternalServerError)
}
func newSessionID() string {
b := make([]byte, 32)
if _, err := rand.Read(b); err != nil {
panic(err)
}
return hex.EncodeToString(b)
}
func currentUserID(r *http.Request) (int64, bool) {
c, err := r.Cookie("session_id")
if err != nil {
return 0, false
}
sessionsMu.Lock()
defer sessionsMu.Unlock()
userID, ok := sessions[c.Value]
return userID, ok
}
func handleIndex(w http.ResponseWriter, r *http.Request) {
rows, err := db.Query(`
SELECT posts.id, users.name, posts.body, posts.created_at
FROM posts
JOIN users ON posts.user_id = users.id
ORDER BY posts.id DESC`)
if err != nil {
serverError(w, err)
return
}
defer rows.Close()
var posts []Post
for rows.Next() {
var p Post
if err := rows.Scan(&p.ID, &p.UserName, &p.Body, &p.CreatedAt); err != nil {
serverError(w, err)
return
}
posts = append(posts, p)
}
if err := rows.Err(); err != nil {
serverError(w, err)
return
}
userName := ""
userID, loggedIn := currentUserID(r)
if loggedIn {
if err := db.QueryRow("SELECT name FROM users WHERE id = ?", userID).Scan(&userName); err != nil {
serverError(w, err)
return
}
}
data := map[string]any{
"Posts": posts,
"LoggedIn": loggedIn,
"UserName": userName,
}
if err := templates.ExecuteTemplate(w, "index.html", data); err != nil {
log.Println(err)
}
}
func handleRegisterForm(w http.ResponseWriter, r *http.Request) {
if err := templates.ExecuteTemplate(w, "register.html", nil); err != nil {
log.Println(err)
}
}
func handleRegister(w http.ResponseWriter, r *http.Request) {
name := r.FormValue("name")
password := r.FormValue("password")
if name == "" || password == "" {
http.Error(w, "ユーザー名とパスワードを入力してください", http.StatusBadRequest)
return
}
if len(password) > 72 {
http.Error(w, "パスワードは72バイト以内にしてください", http.StatusBadRequest)
return
}
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
serverError(w, err)
return
}
_, err = db.Exec("INSERT INTO users (name, password_hash) VALUES (?, ?)", name, string(hash))
if err != nil {
var mysqlErr *mysql.MySQLError
if errors.As(err, &mysqlErr) && mysqlErr.Number == 1062 {
http.Error(w, "そのユーザー名はすでに使われています", http.StatusConflict)
return
}
serverError(w, err)
return
}
http.Redirect(w, r, "/login", http.StatusSeeOther)
}
func handleLoginForm(w http.ResponseWriter, r *http.Request) {
if err := templates.ExecuteTemplate(w, "login.html", nil); err != nil {
log.Println(err)
}
}
func handleLogin(w http.ResponseWriter, r *http.Request) {
name := r.FormValue("name")
password := r.FormValue("password")
var userID int64
var passwordHash string
err := db.QueryRow("SELECT id, password_hash FROM users WHERE name = ?", name).Scan(&userID, &passwordHash)
if errors.Is(err, sql.ErrNoRows) {
http.Error(w, "ユーザー名かパスワードが違います", http.StatusUnauthorized)
return
}
if err != nil {
serverError(w, err)
return
}
if err := bcrypt.CompareHashAndPassword([]byte(passwordHash), []byte(password)); err != nil {
http.Error(w, "ユーザー名かパスワードが違います", http.StatusUnauthorized)
return
}
sessionID := newSessionID()
sessionsMu.Lock()
sessions[sessionID] = userID
sessionsMu.Unlock()
http.SetCookie(w, &http.Cookie{
Name: "session_id",
Value: sessionID,
Path: "/",
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
})
http.Redirect(w, r, "/", http.StatusSeeOther)
}
func handleLogout(w http.ResponseWriter, r *http.Request) {
if c, err := r.Cookie("session_id"); err == nil {
sessionsMu.Lock()
delete(sessions, c.Value)
sessionsMu.Unlock()
}
http.SetCookie(w, &http.Cookie{
Name: "session_id",
Value: "",
Path: "/",
MaxAge: -1,
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
})
http.Redirect(w, r, "/", http.StatusSeeOther)
}
func handleCreatePost(w http.ResponseWriter, r *http.Request) {
userID, ok := currentUserID(r)
if !ok {
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
body := r.FormValue("body")
if body == "" {
http.Redirect(w, r, "/", http.StatusSeeOther)
return
}
if _, err := db.Exec("INSERT INTO posts (user_id, body) VALUES (?, ?)", userID, body); err != nil {
serverError(w, err)
return
}
http.Redirect(w, r, "/", http.StatusSeeOther)
}通しの動作確認
サーバーを起動して、機能をひととおり触って確かめます。
go run main.go2026/07/21 10:00:00 http://localhost:8080 で待ち受けますブラウザで次の順に確かめてください。
- 未ログインで http://localhost:8080 を開くと、投稿フォームの代わりにログインと登録への案内が出る
- /register で登録すると、ログイン画面へ移動する
- ログインすると、投稿フォームとログアウトボタンが現れる
- 投稿すると、一覧のいちばん上に自分の投稿が出る
- ログアウトすると、未ログインの案内表示に戻る
うまく動かないとき
起動時によく見るエラーと原因です。
dial tcp 127.0.0.1:3306: connect: connection refused:MySQL のコンテナが動いていません。docker start neziki-mysqlで起動してから再実行してください。panic: html/template: pattern matches no files: templates/*.html:main.go のあるディレクトリ以外から起動しています。templates ディレクトリが見える場所でgo run main.goを実行してください。listen tcp :8080: bind: address already in use:前に起動したサーバーが残っています。起動したままのターミナルを探して Ctrl+C で止めてください。