Go言語入門:効果的なGo -A web server-

スポンサーリンク
Go言語入門:効果的なGo -A web server- ノウハウ
Go言語入門:効果的なGo -A web server-
この記事は約35分で読めます。
よっしー
よっしー

こんにちは。よっしーです(^^)

本日は、Go言語を効果的に使うためのガイドラインについて解説しています。

スポンサーリンク

背景

Go言語を学び始めて、より良いコードを書きたいと思い、Go言語の公式ドキュメント「Effective Go」を知りました。これは、いわば「Goらしいコードの書き方指南書」になります。単に動くコードではなく、効率的で保守性の高いコードを書くためのベストプラクティスが詰まっているので、これを読んだ時の内容を備忘として残しました。

Webサーバー

完全なGoプログラムであるWebサーバーで締めくくりましょう。これは実際にはある種のWeb再サーバーです。Googleはchart.apis.google.comでサービスを提供しており、データを自動的にチャートやグラフにフォーマットします。しかし、データをクエリとしてURLに入れる必要があるため、対話的に使用するのは困難です。ここのプログラムは、ある形式のデータに対してより良いインターフェースを提供します:短いテキストが与えられると、チャートサーバーを呼び出してQRコード(テキストをエンコードするボックスのマトリックス)を生成します。その画像は携帯電話のカメラで取得でき、例えばURLとして解釈でき、電話の小さなキーボードにURLを入力する手間を省けます。

以下が完全なプログラムです。説明が続きます。

package main

import (
    "flag"
    "html/template"
    "log"
    "net/http"
)

var addr = flag.String("addr", ":1718", "http service address") // Q=17, R=18

var templ = template.Must(template.New("qr").Parse(templateStr))

func main() {
    flag.Parse()
    http.Handle("/", http.HandlerFunc(QR))
    err := http.ListenAndServe(*addr, nil)
    if err != nil {
        log.Fatal("ListenAndServe:", err)
    }
}

func QR(w http.ResponseWriter, req *http.Request) {
    templ.Execute(w, req.FormValue("s"))
}

const templateStr = `
<html>
<head>
<title>QR Link Generator</title>
</head>
<body>
{{if .}}
<img src="http://chart.apis.google.com/chart?chs=300x300&cht=qr&choe=UTF-8&chl={{.}}" />
<br>
{{.}}
<br>
<br>
{{end}}
<form action="/" name=f method="GET">
    <input maxLength=1024 size=70 name=s value="" title="Text to QR Encode">
    <input type=submit value="Show QR" name=qr>
</form>
</body>
</html>
`

mainまでの部分は理解しやすいはずです。1つのフラグはサーバーのデフォルトHTTPポートを設定します。テンプレート変数templが面白い部分です。これはページを表示するためにサーバーによって実行されるHTMLテンプレートを構築します。これについてはすぐに詳しく説明します。

main関数はフラグを解析し、上で話したメカニズムを使用して、関数QRをサーバーのルートパスにバインドします。そしてhttp.ListenAndServeが呼び出されてサーバーを開始します。サーバーが実行中はブロックします。

QRはフォームデータを含むリクエストを受信し、sという名前のフォーム値内のデータに対してテンプレートを実行するだけです。

テンプレートパッケージhtml/templateは強力です。このプログラムはその機能の一部に触れているだけです。本質的には、templ.Executeに渡されたデータ項目(この場合はフォーム値)から導出された要素を置換することで、HTMLテキストの一部を動的に書き換えます。テンプレートテキスト(templateStr)内で、二重ブレースで区切られた部分はテンプレートアクションを示します。{{if .}}から{{end}}までの部分は、現在のデータ項目の値(.(ドット)と呼ばれる)が空でない場合のみ実行されます。つまり、文字列が空の場合、テンプレートのこの部分は抑制されます。

2つの{{.}}スニペットは、テンプレートに提示されたデータ(クエリ文字列)をWebページに表示することを意味します。HTMLテンプレートパッケージは適切なエスケープを自動的に提供するため、テキストは安全に表示されます。

テンプレート文字列の残りの部分は、ページが読み込まれたときに表示するHTMLです。この説明が速すぎる場合は、より詳細な議論についてはテンプレートパッケージのドキュメントを参照してください。

これで完成です:数行のコードといくつかのデータ駆動HTMLテキストで有用なWebサーバーができました。Goは数行で多くのことを実現するのに十分強力です。

QRコードWebサーバーの概要

このプログラムは、テキストを入力するとQRコードを生成して表示するWebサーバーです。GoogleのChart APIを使用してQRコードを生成しています。

プログラムの構造解説

package main

import (
    "flag"
    "html/template"
    "log"
    "net/http"
)

// コマンドライン引数でポート番号を指定可能
var addr = flag.String("addr", ":1718", "http service address") // Q=17, R=18

// HTMLテンプレートを事前にパース
var templ = template.Must(template.New("qr").Parse(templateStr))

func main() {
    flag.Parse()                                    // フラグ解析
    http.Handle("/", http.HandlerFunc(QR))         // ルートパスにハンドラー登録
    err := http.ListenAndServe(*addr, nil)         // サーバー開始
    if err != nil {
        log.Fatal("ListenAndServe:", err)
    }
}

// HTTPリクエストを処理する関数
func QR(w http.ResponseWriter, req *http.Request) {
    // フォームの"s"パラメータを取得してテンプレートに渡す
    templ.Execute(w, req.FormValue("s"))
}

実行可能な改良版

package main

import (
    "flag"
    "fmt"
    "html/template"
    "log"
    "net/http"
    "net/url"
    "time"
)

var addr = flag.String("addr", ":8080", "http service address")

// テンプレート構造体で複数の値を渡せるようにする
type PageData struct {
    Text      string
    QRCodeURL string
    Timestamp string
    Error     string
}

var templ = template.Must(template.New("qr").Parse(templateStr))

func main() {
    flag.Parse()
    
    fmt.Printf("QRコードサーバーを開始しています... http://localhost%s\n", *addr)
    
    // 静的ファイル配信(CSS、JSなど)
    http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))
    
    // メインハンドラー
    http.HandleFunc("/", QRHandler)
    
    // サーバー開始
    fmt.Printf("サーバーがポート%sで起動しました\n", *addr)
    err := http.ListenAndServe(*addr, nil)
    if err != nil {
        log.Fatal("サーバー開始エラー:", err)
    }
}

func QRHandler(w http.ResponseWriter, req *http.Request) {
    data := PageData{
        Timestamp: time.Now().Format("2006-01-02 15:04:05"),
    }
    
    // フォームからテキストを取得
    text := req.FormValue("s")
    if text != "" {
        // テキストの長さチェック
        if len(text) > 1000 {
            data.Error = "テキストが長すぎます(1000文字以内)"
        } else {
            data.Text = text
            // URLエンコードしてQRコードURLを作成
            encodedText := url.QueryEscape(text)
            data.QRCodeURL = fmt.Sprintf("https://chart.googleapis.com/chart?chs=300x300&cht=qr&choe=UTF-8&chl=%s", encodedText)
        }
    }
    
    // テンプレートを実行
    if err := templ.Execute(w, data); err != nil {
        log.Printf("テンプレート実行エラー: %v", err)
        http.Error(w, "内部サーバーエラー", http.StatusInternalServerError)
    }
}

const templateStr = `
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>QRコードジェネレーター</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            max-width: 800px;
            margin: 0 auto;
            padding: 20px;
            background-color: #f5f5f5;
        }
        .container {
            background-color: white;
            padding: 30px;
            border-radius: 10px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
        }
        h1 {
            color: #333;
            text-align: center;
            margin-bottom: 30px;
        }
        .form-group {
            margin-bottom: 20px;
        }
        label {
            display: block;
            margin-bottom: 5px;
            font-weight: bold;
            color: #555;
        }
        input[type="text"] {
            width: 100%;
            padding: 12px;
            border: 2px solid #ddd;
            border-radius: 5px;
            font-size: 16px;
            box-sizing: border-box;
        }
        input[type="text"]:focus {
            border-color: #4CAF50;
            outline: none;
        }
        button {
            background-color: #4CAF50;
            color: white;
            padding: 12px 24px;
            border: none;
            border-radius: 5px;
            cursor: pointer;
            font-size: 16px;
            width: 100%;
        }
        button:hover {
            background-color: #45a049;
        }
        .result {
            text-align: center;
            margin-top: 30px;
            padding: 20px;
            background-color: #f9f9f9;
            border-radius: 10px;
        }
        .qr-image {
            margin: 20px 0;
        }
        .error {
            color: #d32f2f;
            background-color: #ffebee;
            padding: 10px;
            border-radius: 5px;
            border-left: 4px solid #d32f2f;
        }
        .timestamp {
            font-size: 12px;
            color: #666;
            text-align: center;
            margin-top: 20px;
        }
        .text-display {
            word-break: break-all;
            background-color: #e8f5e8;
            padding: 10px;
            border-radius: 5px;
            margin: 10px 0;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>📱 QRコードジェネレーター</h1>
        
        {{if .Error}}
        <div class="error">
            ⚠️ {{.Error}}
        </div>
        {{end}}
        
        {{if .Text}}
        <div class="result">
            <h2>生成されたQRコード</h2>
            <div class="qr-image">
                <img src="{{.QRCodeURL}}" alt="QRコード" />
            </div>
            <div class="text-display">
                <strong>エンコードされたテキスト:</strong><br>
                {{.Text}}
            </div>
            <p><small>💡 携帯電話のカメラでQRコードをスキャンできます</small></p>
        </div>
        {{end}}
        
        <form action="/" method="GET">
            <div class="form-group">
                <label for="text-input">QRコードにしたいテキストを入力してください:</label>
                <input type="text" 
                       id="text-input"
                       name="s" 
                       value="{{.Text}}" 
                       placeholder="例: https://www.example.com, 電話番号, メッセージなど"
                       maxlength="1000">
            </div>
            <button type="submit">🔄 QRコード生成</button>
        </form>
        
        <div class="timestamp">
            🕒 最終更新: {{.Timestamp}}
        </div>
    </div>
</body>
</html>
`

さらに高機能な版

package main

import (
    "encoding/json"
    "flag"
    "fmt"
    "html/template"
    "log"
    "net/http"
    "net/url"
    "strings"
    "time"
)

var addr = flag.String("addr", ":8080", "http service address")

type QRData struct {
    Text      string    `json:"text"`
    QRCodeURL string    `json:"qr_url"`
    Type      string    `json:"type"`
    Timestamp time.Time `json:"timestamp"`
}

type PageData struct {
    QRData
    Error   string
    History []QRData
}

// 簡単な履歴保存(実際のアプリではデータベースを使用)
var history []QRData

var (
    pageTemplate = template.Must(template.New("page").Parse(pageTemplateStr))
    apiTemplate  = template.Must(template.New("api").Parse(apiTemplateStr))
)

func main() {
    flag.Parse()
    
    fmt.Printf("多機能QRコードサーバーを開始... http://localhost%s\n", *addr)
    
    // ルートハンドラー
    http.HandleFunc("/", homeHandler)
    
    // API エンドポイント
    http.HandleFunc("/api/qr", apiHandler)
    http.HandleFunc("/api/history", historyHandler)
    
    // 履歴ページ
    http.HandleFunc("/history", historyPageHandler)
    
    fmt.Printf("利用可能なエンドポイント:\n")
    fmt.Printf("  http://localhost%s/          - メインページ\n", *addr)
    fmt.Printf("  http://localhost%s/api/qr    - JSON API\n", *addr)
    fmt.Printf("  http://localhost%s/history   - 履歴ページ\n", *addr)
    
    log.Fatal(http.ListenAndServe(*addr, nil))
}

func detectTextType(text string) string {
    text = strings.ToLower(text)
    if strings.HasPrefix(text, "http://") || strings.HasPrefix(text, "https://") {
        return "URL"
    }
    if strings.Contains(text, "@") && strings.Contains(text, ".") {
        return "Email"
    }
    if len(text) >= 10 && strings.ContainsAny(text, "0123456789-+()") {
        return "Phone"
    }
    return "Text"
}

func createQRData(text string) QRData {
    encodedText := url.QueryEscape(text)
    qrURL := fmt.Sprintf("https://chart.googleapis.com/chart?chs=300x300&cht=qr&choe=UTF-8&chl=%s", encodedText)
    
    return QRData{
        Text:      text,
        QRCodeURL: qrURL,
        Type:      detectTextType(text),
        Timestamp: time.Now(),
    }
}

func homeHandler(w http.ResponseWriter, req *http.Request) {
    data := PageData{
        History: getRecentHistory(5),
    }
    
    text := strings.TrimSpace(req.FormValue("s"))
    if text != "" {
        if len(text) > 1000 {
            data.Error = "テキストが長すぎます(1000文字以内にしてください)"
        } else {
            data.QRData = createQRData(text)
            // 履歴に追加
            history = append(history, data.QRData)
            if len(history) > 100 { // 最新100件のみ保持
                history = history[1:]
            }
        }
    }
    
    if err := pageTemplate.Execute(w, data); err != nil {
        log.Printf("Template error: %v", err)
        http.Error(w, "Internal Server Error", http.StatusInternalServerError)
    }
}

func apiHandler(w http.ResponseWriter, req *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    
    text := strings.TrimSpace(req.FormValue("text"))
    if text == "" {
        w.WriteHeader(http.StatusBadRequest)
        json.NewEncoder(w).Encode(map[string]string{
            "error": "text parameter is required",
        })
        return
    }
    
    if len(text) > 1000 {
        w.WriteHeader(http.StatusBadRequest)
        json.NewEncoder(w).Encode(map[string]string{
            "error": "text too long (max 1000 characters)",
        })
        return
    }
    
    qrData := createQRData(text)
    history = append(history, qrData)
    
    json.NewEncoder(w).Encode(qrData)
}

func historyHandler(w http.ResponseWriter, req *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(map[string]interface{}{
        "history": getRecentHistory(20),
        "total":   len(history),
    })
}

func historyPageHandler(w http.ResponseWriter, req *http.Request) {
    data := struct {
        History []QRData
    }{
        History: getRecentHistory(20),
    }
    
    if err := apiTemplate.Execute(w, data); err != nil {
        log.Printf("Template error: %v", err)
        http.Error(w, "Internal Server Error", http.StatusInternalServerError)
    }
}

func getRecentHistory(count int) []QRData {
    if len(history) == 0 {
        return []QRData{}
    }
    
    start := len(history) - count
    if start < 0 {
        start = 0
    }
    
    // 最新のものを先頭に
    recent := make([]QRData, len(history[start:]))
    copy(recent, history[start:])
    
    // 逆順にする
    for i, j := 0, len(recent)-1; i < j; i, j = i+1, j-1 {
        recent[i], recent[j] = recent[j], recent[i]
    }
    
    return recent
}

const pageTemplateStr = `
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>多機能QRコードジェネレーター</title>
    <style>
        body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; margin: 0; padding: 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; }
        .container { max-width: 1000px; margin: 0 auto; background: white; border-radius: 15px; box-shadow: 0 10px 30px rgba(0,0,0,0.2); overflow: hidden; }
        .header { background: linear-gradient(45deg, #667eea, #764ba2); color: white; padding: 30px; text-align: center; }
        .content { padding: 30px; }
        .form-group { margin-bottom: 20px; }
        label { display: block; margin-bottom: 8px; font-weight: 600; color: #333; }
        input[type="text"] { width: 100%; padding: 15px; border: 2px solid #e1e5e9; border-radius: 8px; font-size: 16px; box-sizing: border-box; transition: border-color 0.3s; }
        input[type="text"]:focus { border-color: #667eea; outline: none; }
        button { background: linear-gradient(45deg, #667eea, #764ba2); color: white; padding: 15px 30px; border: none; border-radius: 8px; cursor: pointer; font-size: 16px; width: 100%; font-weight: 600; transition: transform 0.2s; }
        button:hover { transform: translateY(-2px); }
        .result { text-align: center; margin: 30px 0; padding: 30px; background: #f8f9fa; border-radius: 10px; }
        .error { color: #dc3545; background: #f8d7da; padding: 15px; border-radius: 8px; border-left: 4px solid #dc3545; margin-bottom: 20px; }
        .type-badge { display: inline-block; padding: 4px 12px; border-radius: 20px; font-size: 12px; font-weight: 600; margin-left: 10px; }
        .type-url { background: #28a745; color: white; }
        .type-email { background: #17a2b8; color: white; }
        .type-phone { background: #ffc107; color: black; }
        .type-text { background: #6c757d; color: white; }
        .history { margin-top: 40px; }
        .history-item { display: flex; align-items: center; padding: 10px; border-bottom: 1px solid #e9ecef; }
        .history-item:hover { background: #f8f9fa; }
        .history-text { flex: 1; font-size: 14px; }
        .history-time { font-size: 12px; color: #6c757d; }
        .nav { text-align: center; margin-top: 20px; }
        .nav a { color: #667eea; text-decoration: none; margin: 0 15px; font-weight: 500; }
    </style>
</head>
<body>
    <div class="container">
        <div class="header">
            <h1>🚀 多機能QRコードジェネレーター</h1>
            <p>テキスト、URL、メール、電話番号をQRコードに変換</p>
        </div>
        
        <div class="content">
            {{if .Error}}
            <div class="error">⚠️ {{.Error}}</div>
            {{end}}
            
            {{if .Text}}
            <div class="result">
                <h2>生成されたQRコード
                    <span class="type-badge type-{{.Type | lower}}">{{.Type}}</span>
                </h2>
                <img src="{{.QRCodeURL}}" alt="QRコード" style="border-radius: 10px; box-shadow: 0 5px 15px rgba(0,0,0,0.1);" />
                <div style="margin-top: 15px; padding: 15px; background: white; border-radius: 8px; word-break: break-all;">
                    {{.Text}}
                </div>
                <p style="color: #6c757d; margin-top: 15px;">📱 スマートフォンのカメラでスキャンしてください</p>
            </div>
            {{end}}
            
            <form action="/" method="GET">
                <div class="form-group">
                    <label for="text-input">QRコードにするテキストを入力:</label>
                    <input type="text" id="text-input" name="s" value="{{.Text}}" 
                           placeholder="URL、メールアドレス、電話番号、またはテキスト" maxlength="1000">
                </div>
                <button type="submit">🔄 QRコード生成</button>
            </form>
            
            {{if .History}}
            <div class="history">
                <h3>📋 最近の履歴</h3>
                {{range .History}}
                <div class="history-item">
                    <div class="history-text">
                        {{.Text}}
                        <span class="type-badge type-{{.Type | lower}}">{{.Type}}</span>
                    </div>
                    <div class="history-time">{{.Timestamp.Format "15:04"}}</div>
                </div>
                {{end}}
            </div>
            {{end}}
            
            <div class="nav">
                <a href="/history">📜 全履歴を見る</a>
                <a href="/api/qr?text=Hello%20World">🔧 API テスト</a>
            </div>
        </div>
    </div>
</body>
</html>
`

const apiTemplateStr = `
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>QRコード履歴</title>
    <style>
        body { font-family: Arial, sans-serif; margin: 40px; background: #f5f5f5; }
        .container { max-width: 800px; margin: 0 auto; background: white; padding: 30px; border-radius: 10px; }
        h1 { color: #333; border-bottom: 2px solid #667eea; padding-bottom: 10px; }
        .history-item { padding: 15px; border: 1px solid #e9ecef; margin: 10px 0; border-radius: 5px; }
        .back-link { display: inline-block; margin-bottom: 20px; color: #667eea; text-decoration: none; }
    </style>
</head>
<body>
    <div class="container">
        <a href="/" class="back-link">← メインページに戻る</a>
        <h1>📜 QRコード生成履歴</h1>
        {{range .History}}
        <div class="history-item">
            <strong>{{.Type}}</strong> - {{.Timestamp.Format "2006-01-02 15:04:05"}}<br>
            {{.Text}}
        </div>
        {{else}}
        <p>履歴はまだありません。</p>
        {{end}}
    </div>
</body>
</html>
`

重要なポイント

1. GoでのシンプルなWebサーバー

  • 最小構成: わずか数十行でWebサーバーを構築
  • 標準ライブラリ: 外部依存なしでHTTPサーバーを作成
  • テンプレートエンジン: HTMLの動的生成

2. HTTPハンドリングの基本

http.HandleFunc("/", handler)        // ルート登録
http.ListenAndServe(":8080", nil)   // サーバー開始

3. テンプレートシステム

  • 動的コンテンツ: {{.}}でデータを埋め込み
  • 条件分岐: {{if .}}...{{end}}
  • 自動エスケープ: XSS攻撃を自動防止

4. フォーム処理

text := req.FormValue("s")  // フォームデータ取得

5. 実用的な機能

  • エラーハンドリング: 適切なエラー処理
  • 入力検証: 文字数制限、型判定
  • 履歴機能: 過去の生成記録
  • API提供: JSON形式でのデータ提供

6. セキュリティ考慮

  • XSSプロテクション: HTMLテンプレートの自動エスケープ
  • 入力制限: 最大文字数の制限
  • 適切なHTTPステータス: エラー時の適切なレスポンス

このように、Goの標準ライブラリだけで、実用的で高機能なWebアプリケーションを簡潔に構築できます。Goの「シンプルさ」と「パワフルさ」を同時に体現した例といえるでしょう。

おわりに 

本日は、Go言語を効果的に使うためのガイドラインについて解説しました。

よっしー
よっしー

何か質問や相談があれば、コメントをお願いします。また、エンジニア案件の相談にも随時対応していますので、お気軽にお問い合わせください。

それでは、また明日お会いしましょう(^^)

コメント

タイトルとURLをコピーしました