Golang构建小型在线投票与评分系统
投票与评分是Web应用中常见的互动功能。无论是简单的每日话题投票,还是带有星级评分的产品评价,都可以用Go语言快速实现。本文将带你从零开始,使用Go标准库构建一个小型在线投票与评分系统,支持创建主题、提交投票以及查看结果。
项目概述
我们将实现一个单文件Web应用,包含以下功能:
展示投票主题列表
进入单个主题并提交投票
实时查看投票结果(支持百分比和票数)
简单的评分机制(选项附带分数)
所有数据存储在内存中,无需数据库
技术栈仅使用Go net/http、html/template 以及标准库的JSON序列化,便于理解核心原理。
准备工作
确保本地已安装Go(1.16+)。创建项目文件夹 voting-system,并初始化模块:
mkdir voting-system cd voting-system go mod init voting-system
项目目录结构如下:
voting-system/ ├── main.go └── templates/ ├── index.html ├── topic.html └── results.html
数据模型与存储
首先定义投票涉及的核心结构体。一个投票主题(Topic)包含标题、描述、选项列表以及创建时间。每个选项(Option)拥有唯一ID、名称和分数(用于评分场景)。
package main
import (
"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 // optionID -> count
CreatedAt time.Time
}
type VoteRecord struct {
TopicID int
OptionID int
}为简化并发访问,将数据存储在一个线程安全的结构体中:
type Store struct {
mu sync.RWMutex
topics map[int]*Topic
nextID int
}
func NewStore() *Store {
return &Store{
topics: make(map[int]*Topic),
nextID: 1,
}
}添加几个辅助方法:AddTopic、GetTopic、Vote。为了防止重复投票,这里通过客户端IP简单限制,实际场景可结合Cookie或用户登录。
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
}路由与处理器
使用标准库的多路复用器 http.NewServeMux 注册路由。处理器函数直接读取模板并操作Store。
主入口:
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)
}
}投票处理器 处理POST请求,记录投票并重定向到结果页:
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)
}
}所有处理器都用到一个renderTemplate辅助函数,负责解析和执行模板:
func renderTemplate(w http.ResponseWriter, name string, data interface{}) {
tmpl, err := template.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)
}
}