开发一个简单的邮件管理系统,核心是把邮件的元数据与收发关系持久化到数据库,同时用后端服务完成发送逻辑。MySQL负责存储用户和邮件记录,Go语言凭借标准库与高效的并发模型,可以很自然地承担HTTP接口与SMTP投递任务。下面我们从数据库设计开始,逐步用Go把整套流程跑通。

一、数据库表结构设计
在MySQL中,我们需要至少两张表:用户表用来标识发件人和收件人,邮件表保存标题、内容、状态及关联的用户ID。使用InnoDB引擎和外键约束,可以保证删除用户时关联邮件受到约束或级联处理。
字符集建议统一使用utf8mb4,避免表情符或特殊汉字写入时报错。时间字段用DATETIME并配合Go端的时区设置,防止出现八小时偏差。下面是建表语句:
CREATE TABLE `user` ( `id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, `username` VARCHAR(50) NOT NULL, `email` VARCHAR(100) NOT NULL, `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `mail` ( `id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, `sender_id` INT UNSIGNED NOT NULL, `receiver_id` INT UNSIGNED NOT NULL, `subject` VARCHAR(200) NOT NULL, `body` TEXT, `status` TINYINT NOT NULL DEFAULT 0 COMMENT '0草稿 1已发送 2失败', `sent_at` DATETIME, FOREIGN KEY (`sender_id`) REFERENCES `user`(`id`), FOREIGN KEY (`receiver_id`) REFERENCES `user`(`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
上面的设计中,status字段用整型标记邮件生命周期,比直接用字符串更省空间也方便索引。如果后续要支持群发,可以把receiver_id拆成单独的关系表,但简单系统里单收件人模型已经够用。
二、Go连接MySQL与基础封装
Go标准库的database/sql提供了通用的数据库操作接口,配合go-sql-driver/mysql驱动即可连库。关键是设置连接池参数,避免高并发时连接耗尽或闲置断开。
我们在init函数中打开数据库,并通过SetMaxOpenConns与SetMaxIdleConns控制资源。结构体Mail和User对应表字段,用sql.Scanner或手动扫描填充。以下代码展示连接与查询示例:
package main
import (
"database/sql"
"fmt"
"time"
_ "github.com/go-sql-driver/mysql"
)
type User struct {
ID uint
Username string
Email string
}
var db *sql.DB
func initDB() error {
dsn := "root:password@tcp(127.0.0.1:3306)/maildb?charset=utf8mb4&parseTime=true&loc=Local"
var err error
db, err = sql.Open("mysql", dsn)
if err != nil {
return err
}
db.SetMaxOpenConns(20)
db.SetMaxIdleConns(10)
db.SetConnMaxLifetime(time.Minute * 30)
return db.Ping()
}
func getUser(id uint) (User, error) {
var u User
row := db.QueryRow("SELECT id,username,email FROM user WHERE id=?", id)
err := row.Scan(&u.ID, &u.Username, &u.Email)
return u, err
}
func main() {
if err := initDB(); err != nil {
fmt.Println("数据库连接失败:", err)
return
}
u, err := getUser(1)
if err != nil {
fmt.Println("查询用户出错:", err)
return
}
fmt.Printf("用户:%s 邮箱:%sn", u.Username, u.Email)
}
注意DSN里的parseTime=true让驱动把DATETIME映射为time.Time,loc=Local保持时区一致。很多初学者漏掉这两项,导致扫描时间字段直接报错。
另外,实际项目中不要把数据库密码硬编码,可通过环境变量或配置文件注入。连接池参数也应根据机器规格调整,云环境小实例设太大反而容易被数据库侧拒绝。
三、实现邮件发送与状态更新
Go的net/smtp包能直接通过SMTP服务器投递邮件。我们把发送逻辑封装成函数,发送成功后将mail表的status改为1并填写sent_at,失败则标记2便于重试。
使用事务处理状态更新,能保证邮件记录和状态变更要么同时生效要么回滚。下面的例子演示了从取收件人地址到发信再写库的过程:
package main
import (
"database/sql"
"fmt"
"net/smtp"
"strings"
)
func sendMail(sender User, receiver User, subject, body string) error {
tx, err := db.Begin()
if err != nil {
return err
}
// 插入邮件记录
res, err := tx.Exec("INSERT INTO mail(sender_id,receiver_id,subject,body,status) VALUES(?,?,?,?,0)",
sender.ID, receiver.ID, subject, body)
if err != nil {
tx.Rollback()
return err
}
mailID, _ := res.LastInsertId()
// 构造SMTP消息
msg := strings.Builder{}
msg.WriteString("From: " + sender.Email + "rn")
msg.WriteString("To: " + receiver.Email + "rn")
msg.WriteString("Subject: " + subject + "rnrn")
msg.WriteString(body)
err = smtp.SendMail("smtp.ipipp.com:25",
smtp.PlainAuth("", sender.Email, "smtppass", "smtp.ipipp.com"),
sender.Email, []string{receiver.Email}, []byte(msg.String()))
if err != nil {
tx.Exec("UPDATE mail SET status=2 WHERE id=?", mailID)
tx.Commit()
return err
}
_, err = tx.Exec("UPDATE mail SET status=1,sent_at=NOW() WHERE id=?", mailID)
if err != nil {
tx.Rollback()
return err
}
return tx.Commit()
}
func demoSend() {
s, _ := getUser(1)
r, _ := getUser(2)
if err := sendMail(s, r, "测试主题", "这是一封来自Go系统的测试邮件"); err != nil {
fmt.Println("发送失败:", err)
}
}
上述代码在事务里先落库再发信,即便SMTP超时也能在表里留下失败状态。生产环境可另起定时任务扫描status=2的记录做指数退避重试。
SMTP认证信息同样建议配置化。若邮件量较大,可以引入消息队列,把sendMail改成往队列投任务,由独立worker调用SMTP,降低接口响应时间。
四、提供简易HTTP接口
为了让系统可被前端或其他服务调用,用Go的net/http写两个接口:一个发信POST,一个查收件箱GET。借助结构体绑定和简单路由即可运转。
下面示例用标准库实现,不引入第三方框架,保持依赖最小。接收JSON后用前面封装的函数处理,并返回简单结果:
package main
import (
"encoding/json"
"net/http"
)
type SendReq struct {
SenderID uint `json:"sender_id"`
ReceiverID uint `json:"receiver_id"`
Subject string `json:"subject"`
Body string `json:"body"`
}
func sendHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "仅支持POST", 405)
return
}
var req SendReq
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "参数错误", 400)
return
}
s, err := getUser(req.SenderID)
if err != nil {
http.Error(w, "发件人不存在", 404)
return
}
rc, err := getUser(req.ReceiverID)
if err != nil {
http.Error(w, "收件人不存在", 404)
return
}
if err := sendMail(s, rc, req.Subject, req.Body); err != nil {
json.NewEncoder(w).Encode(map[string]string{"result": "fail", "msg": err.Error()})
return
}
json.NewEncoder(w).Encode(map[string]string{"result": "ok"})
}
func main() {
initDB()
http.HandleFunc("/send", sendHandler)
http.ListenAndServe(":8080", nil)
}
这个接口层非常薄,所有业务校验和持久化都下沉到了前面的函数。若需要鉴权,可在sendHandler开头校验token,或套一层中间件。
整体看,MySQL做存储、Go做业务与投递,两者组合开发邮件管理系统非常直接。后续可加登录态、附件上传、全文检索,但核心骨架就是上述表结构加连接池与SMTP封装。