
こんにちは。よっしーです(^^)
本日は、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言語を効果的に使うためのガイドラインについて解説しました。

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