HTML模板
模板存储在 templates/ 目录下。注意在代码示例中,HTML标签已全部转义,以便在文章中正确显示。
index.html – 主题列表页:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>投票系统</title>
<style>
body { font-family: sans-serif; max-width: 600px; margin: 0 auto; padding: 1rem; }
.topic-card { border: 1px solid #ccc; padding: 1rem; margin-bottom: 1rem; border-radius: 4px; }
</style>
</head>
<body>
<h1>可参与的投票与评分</h1>
{{range .}}
<div class="topic-card">
<h2><a href="/topic/{{.ID}}">{{.Title}}</a></h2>
<p>{{.Description}}</p>
<a href="/results/{{.ID}}">查看结果</a>
</div>
{{end}}
</body>
</html>topic.html – 投票表单页:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>{{.Title}} – 投票</title>
<style>
body { font-family: sans-serif; max-width: 600px; margin: 0 auto; padding: 1rem; }
.option { margin-bottom: 0.5rem; }
button { margin-top: 1rem; padding: 0.5rem 1.5rem; }
</style>
</head>
<body>
<h1>{{.Title}}</h1>
<p>{{.Description}}</p>
<form method="POST" action="/vote">
<input type="hidden" name="topic" value="{{.ID}}">
{{range .Options}}
<div class="option">
<input type="radio" id="opt{{.ID}}" name="option" value="{{.ID}}">
<label for="opt{{.ID}}">{{.Name}}</label> (评分 {{.Score}})
</div>
{{end}}
<button type="submit">提交投票</button>
</form>
<p><a href="/results/{{.ID}}">查看当前结果</a></p>
<p><a href="/">返回首页</a></p>
</body>
</html>results.html – 结果显示页:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>{{.Topic.Title}} – 结果</title>
<style>
body { font-family: sans-serif; max-width: 600px; margin: 0 auto; padding: 1rem; }
.bar-wrapper { background: #eee; height: 24px; border-radius: 4px; margin: 0.5rem 0; }
.bar { background: #4CAF50; height: 100%; border-radius: 4px; }
.result-item { margin-bottom: 1rem; }
.votes { color: #666; }
</style>
</head>
<body>
<h1>{{.Topic.Title}} – 投票结果</h1>
<p>总票数:{{.Total}}</p>
{{range .Results}}
<div class="result-item">
<strong>{{.Name}}</strong> <span class="votes">({{.Votes}} 票)</span>
<div class="bar-wrapper">
<div class="bar" style="width:{{printf "%.1f" .Percentage}}%"></div>
</div>
<span>{{printf "%.1f" .Percentage}}%</span>
</div>
{{end}}
<p><a href="/topic/{{.Topic.ID}}">继续投票</a> | <a href="/">返回首页</a></p>
</body>
</html>注意模板中的 {{printf "%.1f" .Percentage}} 用于格式化小数。Go模板默认不支持printf,需要注册自定义函数或使用其他方式。为简单起见,我们可以在传递数据前将百分比格式化为字符串,或者注册template.FuncMap。下面给出修改建议。
模板函数的改进
在renderTemplate中注册printf函数:
func renderTemplate(w http.ResponseWriter, name string, data interface{}) {
funcMap := template.FuncMap{
"printf": fmt.Sprintf,
}
tmpl, err := template.New(name).Funcs(funcMap).ParseFiles("templates/" + name)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
err = tmpl.Execute(w, data)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}完整的 main.go
整合所有代码,并添加必要的导入包:
package main
import (
"fmt"
"html/template"
"net/http"
"strconv"
"sync"
"time"
)
type Option struct {
ID int
Name string
Score int
}
type Topic struct {
ID int
Title string
Description string
Options []Option
Votes map[int]int
CreatedAt time.Time
}
type Store struct {
mu sync.RWMutex
topics map[int]*Topic
nextID int
}
func NewStore() *Store {
return &Store{
topics: make(map[int]*Topic),
nextID: 1,
}
}
func (s *Store) AddTopic(title, desc string, options []Option) int {
s.mu.Lock()
defer s.mu.Unlock()
id := s.nextID
s.nextID++
t := &Topic{
ID: id,
Title: title,
Description: desc,
Options: options,
Votes: make(map[int]int),
CreatedAt: time.Now(),
}
s.topics[id] = t
return id
}
func (s *Store) GetTopic(id int) *Topic {
s.mu.RLock()
defer s.mu.RUnlock()
return s.topics[id]
}
func (s *Store) Vote(topicID, optionID int) bool {
s.mu.Lock()
defer s.mu.Unlock()
t, ok := s.topics[topicID]
if !ok {
return false
}
found := false
for _, opt := range t.Options {
if opt.ID == optionID {
found = true
break
}
}
if !found {
return false
}
t.Votes[optionID]++
return true
}
func main() {
store := NewStore()
store.AddTopic("你最喜欢的编程语言?", "选择一项", []Option{
{ID: 1, Name: "Go", Score: 5},
{ID: 2, Name: "Python", Score: 4},
{ID: 3, Name: "JavaScript", Score: 3},
})
store.AddTopic("给这部电影打分", "满分10分", []Option{
{ID: 1, Name: "1-3分", Score: 2},
{ID: 2, Name: "4-6分", Score: 6},
{ID: 3, Name: "7-8分", Score: 8},
{ID: 4, Name: "9-10分", Score: 10},
})
mux := http.NewServeMux()
mux.HandleFunc("/", indexHandler(store))
mux.HandleFunc("/topic/", topicHandler(store))
mux.HandleFunc("/vote", voteHandler(store))
mux.HandleFunc("/results/", resultsHandler(store))
http.ListenAndServe(":8080", mux)
}
func indexHandler(s *Store) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
s.mu.RLock()
topics := make([]*Topic, 0, len(s.topics))
for _, t := range s.topics {
topics = append(topics, t)
}
s.mu.RUnlock()
renderTemplate(w, "index.html", topics)
}
}
func topicHandler(s *Store) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
idStr := r.URL.Path[len("/topic/"):]
id, err := strconv.Atoi(idStr)
if err != nil {
http.NotFound(w, r)
return
}
topic := s.GetTopic(id)
if topic == nil {
http.NotFound(w, r)
return
}
renderTemplate(w, "topic.html", topic)
}
}
func voteHandler(s *Store) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
topicID, _ := strconv.Atoi(r.FormValue("topic"))
optionID, _ := strconv.Atoi(r.FormValue("option"))
if s.Vote(topicID, optionID) {
http.Redirect(w, r, fmt.Sprintf("/results/%d", topicID), http.StatusSeeOther)
} else {
http.Error(w, "无效的投票", http.StatusBadRequest)
}
}
}
func resultsHandler(s *Store) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
idStr := r.URL.Path[len("/results/"):]
id, _ := strconv.Atoi(idStr)
topic := s.GetTopic(id)
if topic == nil {
http.NotFound(w, r)
return
}
s.mu.RLock()
total := 0
for _, v := range topic.Votes {
total += v
}
type OptionResult struct {
Option
Votes int
Percentage float64
}
var results []OptionResult
for _, opt := range topic.Options {
votes := topic.Votes[opt.ID]
pct := 0.0
if total > 0 {
pct = float64(votes) / float64(total) * 100
}
results = append(results, OptionResult{
Option: opt,
Votes: votes,
Percentage: pct,
})
}
s.mu.RUnlock()
data := struct {
Topic *Topic
Results []OptionResult
Total int
}{
Topic: topic,
Results: results,
Total: total,
}
renderTemplate(w, "results.html", data)
}
}
func renderTemplate(w http.ResponseWriter, name string, data interface{}) {
funcMap := template.FuncMap{
"printf": fmt.Sprintf,
}
tmpl, err := template.New(name).Funcs(funcMap).ParseFiles("templates/" + name)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
err = tmpl.Execute(w, data)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}运行与测试
将上述代码保存为 main.go,并确保 templates/ 目录下有三个HTML模板文件。然后在终端运行:
go run main.go
打开浏览器访问 http://127.0.0.1:8080,即可看到投票主题列表。进入一个主题,选择选项后提交,系统会跳转到结果页面并用进度条展示百分比。同一主题可以多次投票(当前未做重复限制),方便观察数据变化。
扩展与改进
这个小系统可以继续增强:
防止重复投票:结合
context传递客户端标识,或使用Cookie/Session。持久化存储:将数据写入JSON文件或SQLite,保证重启后数据不丢失。
评分计算:根据选项分数和票数加权平均,展示综合评分。
RESTful API:提供JSON接口供前端框架调用,前后端分离。
实现这些扩展时,核心的并发安全存储和模板渲染逻辑可以复用。
总结
本文展示了使用Go标准库构建在线投票与评分系统的完整过程。从数据结构设计、处理器编写到HTML模板渲染,所有组件都清晰可控。Go的高并发特性和简洁的语法使得这类Web小工具的开发效率非常高。你可以以此为基础,快速定制出符合实际需求的企业投票或在线评分应用。