手把手教你用Go语言写服务器项目——从小白到进阶的完整实战指南
第一章:为什么选择Go来写服务器?
说实话,刚开始学编程那会儿,我也被各种语言搞得晕头转向。Java太重型,Python写并发又总是让人担心性能,C++那种手动管理内存的感觉更是让人窒息。直到我遇见了Go——这门语言像是专门为写服务器而生的。
Go最迷人的地方在于它的简洁。没有复杂的继承体系,没有繁琐的注解,没有令人头疼的头文件管理。你看这段代码:
package main
import "fmt"
func main() {
fmt.Println("Hello, 服务器!")
}
就这?对,就这。但就是这种简单,让Go在服务器开发领域大放异彩。
更关键的是Go的并发模型。Go的goroutine轻如鸿毛,一个goroutine只占用2KB左右的栈空间,你可以轻松启动上万个并发任务。配合channel,写并发代码就像搭积木一样自然。
// 一个简单的并发例子
package main
import (
"fmt"
"sync"
)
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done()
fmt.Printf("-worker %d 开始工作\n", id)
// 模拟一些工作
for i := 0; i < 3; i++ {
fmt.Printf(" worker %d 处理中...\n", id)
}
fmt.Printf(" worker %d 完成\n", id)
}
func main() {
var wg sync.WaitGroup
numWorkers := 5
// 启动5个并发worker
for i := 0; i < numWorkers; i++ {
wg.Add(1)
go worker(i, &wg)
}
// 等待所有worker完成
wg.Wait()
fmt.Println("所有worker已完成")
}
第二章:环境搭建——别在这一步卡住
2.1 安装Go
去 golang.org 下载对应你系统的安装包。安装完成后,打开终端(Mac/Linux)或命令行(Windows),输入:
go version
如果显示 go version go1.x.x 之类的信息,说明安装成功了。
2.2 配置Go Modules
Go Modules是Go的包管理系统,就像npm之于Node.js,pip之于Python。从Go 1.13开始,Go默认使用Modules。
在你的工作目录下(建议找个专门的文件夹,比如 ~/go-workspace),初始化一个新项目:
mkdir myserver
cd myserver
go mod init myserver
这会在目录下创建一个 go.mod 文件,它记录了你的项目依赖。
2.3 选择编辑器
推荐VS Code + Go插件,这是目前Go开发最流行的组合。安装好Go插件后,它会帮你自动配置lint、format、智能提示等功能。
第三章:第一个HTTP服务器——用最原始的方式
Go标准库的 net/http 包已经足够强大,我们先不用任何框架,从零手写一个服务器。
3.1 Hello World服务器
package main
import (
"fmt"
"net/http"
)
// 处理函数,每个请求都会调用这个函数
func helloHandler(w http.ResponseWriter, r *http.Request) {
// w是响应对象,r是请求对象
fmt.Fprintf(w, "Hello, 欢迎来到我的第一个Go服务器!")
}
// 处理健康检查的函数
func healthHandler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, `{"status": "ok"}`)
}
func main() {
// 注册路由
http.HandleFunc("/hello", helloHandler)
http.HandleFunc("/health", healthHandler)
// 启动服务器,监听8080端口
fmt.Println("服务器启动在 http://localhost:8080")
fmt.Println("试试访问 http://localhost:8080/hello")
fmt.Println("试试访问 http://localhost:8080/health")
err := http.ListenAndServe(":8080", nil)
if err != nil {
fmt.Printf("服务器启动失败: %v\n", err)
}
}
运行方式:
go run main.go
然后打开浏览器访问 http://localhost:8080/hello,你会看到熟悉的问候。
关键点解释:
http.HandleFunc用于注册路由处理器http.ResponseWriter是响应的载体,你可以用它写入状态码、headers和body*http.Request包含了请求的所有信息,比如路径、方法、headers、body等http.ListenAndServe会阻塞当前goroutine,处理所有到达的请求
3.2 处理GET请求和参数
package main
import (
"encoding/json"
"net/http"
)
// 响应结构体
type Response struct {
Code int `json:"code"`
Message string `json:"message"`
Data any `json:"data,omitempty"`
}
// 处理带参数的请求
func getUserHandler(w http.ResponseWriter, r *http.Request) {
// 从URL获取参数
userId := r.URL.Query().Get("id")
if userId == "" {
sendError(w, http.StatusBadRequest, "请提供用户ID")
return
}
// 模拟从数据库查询用户
user := map[string]string{
"id": userId,
"name": "张三",
"email": "zhangsan@example.com",
}
sendJSON(w, http.StatusOK, user)
}
func sendJSON(w http.ResponseWriter, statusCode int, data any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode)
json.NewEncoder(w).Encode(data)
}
func sendError(w http.ResponseWriter, statusCode int, message string) {
sendJSON(w, statusCode, Response{
Code: statusCode,
Message: message,
})
}
func main() {
http.HandleFunc("/user", getUserHandler)
http.ListenAndServe(":8080", nil)
}
测试:
curl "http://localhost:8080/user?id=1001"
第四章:RESTful API设计——写一个用户管理系统
光会返回字符串可不够,真正的项目需要完整的CRUD操作。我们来写一个支持增删改查的用户API。
4.1 数据结构定义
package model
import "time"
// User 用户结构体
type User struct {
ID uint `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
Age int `json:"age"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// CreateUserRequest 创建用户请求体
type CreateUserRequest struct {
Username string `json:"username" binding:"required,min=2,max=20"`
Email string `json:"email" binding:"required,email"`
Age int `json:"age" binding:"required,min=1,max=150"`
}
// UpdateUserRequest 更新用户请求体
type UpdateUserRequest struct {
Username string `json:"username" binding:"omitempty,min=2,max=20"`
Email string `json:"email" binding:"omitempty,email"`
Age int `json:"age" binding:"omitempty,min=1,max=150"`
}
4.2 内存数据库实现
虽然真实项目会连接MySQL或PostgreSQL,但为了演示清晰,我们先用内存存储:
package service
import (
"sync"
"time"
"myserver/model"
)
// UserService 用户服务
type UserService struct {
mu sync.RWMutex // 读写锁,保护并发访问
users map[uint]*model.User
nextID uint
}
// NewUserService 创建用户服务实例
func NewUserService() *UserService {
return &UserService{
users: make(map[uint]*model.User),
nextID: 1,
}
}
// GetAllUsers 获取所有用户
func (s *UserService) GetAllUsers() []*model.User {
s.mu.RLock()
defer s.mu.RUnlock()
users := make([]*model.User, 0, len(s.users))
for _, user := range s.users {
users = append(users, user)
}
return users
}
// GetUser 根据ID获取用户
func (s *UserService) GetUser(id uint) (*model.User, bool) {
s.mu.RLock()
defer s.mu.RUnlock()
user, ok := s.users[id]
return user, ok
}
// CreateUser 创建新用户
func (s *UserService) CreateUser(req *model.CreateUserRequest) (*model.User, error) {
s.mu.Lock()
defer s.mu.Unlock()
// 检查用户名是否已存在
for _, user := range s.users {
if user.Username == req.Username {
return nil, fmt.Errorf("用户名 %s 已存在", req.Username)
}
if user.Email == req.Email {
return nil, fmt.Errorf("邮箱 %s 已被注册", req.Email)
}
}
now := time.Now()
user := &model.User{
ID: s.nextID,
Username: req.Username,
Email: req.Email,
Age: req.Age,
CreatedAt: now,
UpdatedAt: now,
}
s.nextID++
s.users[user.ID] = user
return user, nil
}
// UpdateUser 更新用户信息
func (s *UserService) UpdateUser(id uint, req *model.UpdateUserRequest) (*model.User, error) {
s.mu.Lock()
defer s.mu.Unlock()
user, ok := s.users[id]
if !ok {
return nil, fmt.Errorf("用户不存在,ID: %d", id)
}
// 更新字段
if req.Username != "" {
user.Username = req.Username
}
if req.Email != "" {
user.Email = req.Email
}
if req.Age != 0 {
user.Age = req.Age
}
user.UpdatedAt = time.Now()
return user, nil
}
// DeleteUser 删除用户
func (s *UserService) DeleteUser(id uint) error {
s.mu.Lock()
defer s.mu.Unlock()
if _, ok := s.users[id]; !ok {
return fmt.Errorf("用户不存在,ID: %d", id)
}
delete(s.users, id)
return nil
}
注意:这里用了 sync.RWMutex 来保护并发访问。读锁(RLock)允许多个读者同时访问,写锁(Lock)只允许一个写者独占访问。这在并发场景下非常重要。
4.3 HTTP Handler实现
package handler
import (
"encoding/json"
"net/http"
"strconv"
"myserver/model"
"myserver/service"
)
// UserHandler 用户处理器
type UserHandler struct {
userService *service.UserService
}
// NewUserHandler 创建处理器
func NewUserHandler(svc *service.UserService) *UserHandler {
return &UserHandler{userService: svc}
}
// listUsers 获取用户列表
func (h *UserHandler) listUsers(w http.ResponseWriter, r *http.Request) {
users := h.userService.GetAllUsers()
respondJSON(w, http.StatusOK, users)
}
// getUser 获取单个用户
func (h *UserHandler) getUser(w http.ResponseWriter, r *http.Request) {
idStr := r.URL.Query().Get("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil || id == 0 {
respondError(w, http.StatusBadRequest, "无效的用户ID")
return
}
user, ok := h.userService.GetUser(uint(id))
if !ok {
respondError(w, http.StatusNotFound, "用户不存在")
return
}
respondJSON(w, http.StatusOK, user)
}
// createUser 创建用户
func (h *UserHandler) createUser(w http.ResponseWriter, r *http.Request) {
var req model.CreateUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondError(w, http.StatusBadRequest, "请求体解析失败: "+err.Error())
return
}
// 简单校验
if req.Username == "" || req.Email == "" {
respondError(w, http.StatusBadRequest, "用户名和邮箱不能为空")
return
}
user, err := h.userService.CreateUser(&req)
if err != nil {
respondError(w, http.StatusConflict, err.Error())
return
}
respondJSON(w, http.StatusCreated, user)
}
// updateUser 更新用户
func (h *UserHandler) updateUser(w http.ResponseWriter, r *http.Request) {
idStr := r.URL.Query().Get("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil || id == 0 {
respondError(w, http.StatusBadRequest, "无效的用户ID")
return
}
var req model.UpdateUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondError(w, http.StatusBadRequest, "请求体解析失败")
return
}
user, err := h.userService.UpdateUser(uint(id), &req)
if err != nil {
respondError(w, http.StatusNotFound, err.Error())
return
}
respondJSON(w, http.StatusOK, user)
}
// deleteUser 删除用户
func (h *UserHandler) deleteUser(w http.ResponseWriter, r *http.Request) {
idStr := r.URL.Query().Get("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil || id == 0 {
respondError(w, http.StatusBadRequest, "无效的用户ID")
return
}
if err := h.userService.DeleteUser(uint(id)); err != nil {
respondError(w, http.StatusNotFound, err.Error())
return
}
respondJSON(w, http.StatusOK, map[string]string{"message": "删除成功"})
}
// 辅助函数
func respondJSON(w http.ResponseWriter, statusCode int, data any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode)
json.NewEncoder(w).Encode(data)
}
func respondError(w http.ResponseWriter, statusCode int, message string) {
respondJSON(w, statusCode, map[string]string{"error": message})
}
4.4 路由注册和服务器启动
package main
import (
"fmt"
"log"
"net/http"
"myserver/handler"
"myserver/service"
)
func main() {
// 初始化服务层
userService := service.NewUserService()
// 初始化处理器
userHandler := handler.NewUserHandler(userService)
// 路由配置
mux := http.NewServeMux()
// 用户相关路由
mux.HandleFunc("/api/users", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
userHandler.listUsers(w, r)
case http.MethodPost:
userHandler.createUser(w, r)
default:
http.Error(w, "方法不允许", http.StatusMethodNotAllowed)
}
})
mux.HandleFunc("/api/users/", func(w http.ResponseWriter, r *http.Request) {
// 从路径中提取ID,例如 /api/users/1
// 这里简化处理,实际项目中可能用路由参数
// 由于标准库不支持路径参数,我们用查询参数代替
_ = w
_ = r
switch r.Method {
case http.MethodGet:
userHandler.getUser(w, r)
case http.MethodPut:
userHandler.updateUser(w, r)
case http.MethodDelete:
userHandler.deleteUser(w, r)
default:
http.Error(w, "方法不允许", http.StatusMethodNotAllowed)
}
})
// 健康检查
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"status": "ok"}`))
})
fmt.Println("==========================================")
fmt.Println(" 用户管理API服务器已启动")
fmt.Println(" 监听地址: http://localhost:8080")
fmt.Println("==========================================")
fmt.Println("可用接口:")
fmt.Println(" GET /api/users - 获取所有用户")
fmt.Println(" POST /api/users - 创建用户")
fmt.Println(" GET /api/users?id=1 - 获取单个用户")
fmt.Println(" PUT /api/users?id=1 - 更新用户")
fmt.Println(" DELETE /api/users?id=1 - 删除用户")
fmt.Println(" GET /health - 健康检查")
fmt.Println("==========================================")
log.Fatal(http.ListenAndServe(":8080", mux))
}
4.5 测试你的API
用 curl 测试:
# 创建用户
curl -X POST http://localhost:8080/api/users \
-H "Content-Type: application/json" \
-d '{"username":"张三","email":"zhangsan@example.com","age":25}'
# 获取所有用户
curl http://localhost:8080/api/users
# 获取单个用户
curl "http://localhost:8080/api/users?id=1"
# 更新用户
curl -X PUT "http://localhost:8080/api/users?id=1" \
-H "Content-Type: application/json" \
-d '{"username":"张三丰","age":26}'
# 删除用户
curl -X DELETE "http://localhost:8080/api/users?id=1"
第五章:引入Gin框架——让开发更高效
标准库够用,但在实际项目中,我们通常会选用成熟框架。Gin是Go生态中最流行的Web框架,它轻量、快速、功能丰富。
5.1 安装Gin
go get github.com/gin-gonic/gin
5.2 用Gin重构之前的项目
package main
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"myserver/model"
"myserver/service"
)
// GinHandler 基于Gin的用户处理器
type GinHandler struct {
userService *service.UserService
}
func NewGinHandler(svc *service.UserService) *GinHandler {
return &GinHandler{userService: svc}
}
// ListUsers 获取用户列表
func (h *GinHandler) ListUsers(c *gin.Context) {
users := h.userService.GetAllUsers()
c.JSON(http.StatusOK, gin.H{
"code": 0,
"message": "success",
"data": users,
})
}
// GetUser 获取单个用户
func (h *GinHandler) GetUser(c *gin.Context) {
idStr := c.Query("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil || id == 0 {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "无效的用户ID"})
return
}
user, ok := h.userService.GetUser(uint(id))
if !ok {
c.JSON(http.StatusNotFound, gin.H{"code": 404, "message": "用户不存在"})
return
}
c.JSON(http.StatusOK, gin.H{"code": 0, "data": user})
}
// CreateUser 创建用户
func (h *GinHandler) CreateUser(c *gin.Context) {
var req model.CreateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "请求参数错误: " + err.Error()})
return
}
user, err := h.userService.CreateUser(&req)
if err != nil {
c.JSON(http.StatusConflict, gin.H{"code": 409, "message": err.Error()})
return
}
c.JSON(http.StatusCreated, gin.H{"code": 0, "data": user})
}
// UpdateUser 更新用户
func (h *GinHandler) UpdateUser(c *gin.Context) {
idStr := c.Query("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil || id == 0 {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "无效的用户ID"})
return
}
var req model.UpdateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "请求参数错误"})
return
}
user, err := h.userService.UpdateUser(uint(id), &req)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"code": 404, "message": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"code": 0, "data": user})
}
// DeleteUser 删除用户
func (h *GinHandler) DeleteUser(c *gin.Context) {
idStr := c.Query("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil || id == 0 {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "无效的用户ID"})
return
}
if err := h.userService.DeleteUser(uint(id)); err != nil {
c.JSON(http.StatusNotFound, gin.H{"code": 404, "message": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "删除成功"})
}
func main() {
userService := service.NewUserService()
h := NewGinHandler(userService)
r := gin.Default()
// 定义路由组
api := r.Group("/api")
{
users := api.Group("/users")
{
users.GET("", h.ListUsers)
users.POST("", h.CreateUser)
users.GET("/:id", h.GetUser) // 注意:这里用路径参数
users.PUT("/:id", h.UpdateUser)
users.DELETE("/:id", h.DeleteUser)
}
}
// 健康检查
r.GET("/health", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"status": "ok"})
})
r.Run(":8080") // 监听并启动服务
}
Gin的优势很明显:
- 中间件支持:内置日志、恢复等中间件
- 路由参数:用
:id语法轻松获取路径参数 - 自动绑定:
ShouldBindJSON自动将JSON绑定到结构体 - 优雅的结构:路由分组让代码更清晰
第六章:连接MySQL数据库
内存存储只能用于演示,真实项目需要持久化存储。我们来连接MySQL。
6.1 安装依赖
go get github.com/go-sql-driver/mysql
go get github.com/jmoiron/sqlx
6.2 数据库封装
package db
import (
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql"
"github.com/jmoiron/sqlx"
)
var DB *sqlx.DB
// Init 初始化数据库连接
func Init(dsn string) error {
var err error
DB, err = sqlx.Connect("mysql", dsn)
if err != nil {
return fmt.Errorf("连接数据库失败: %w", err)
}
// 设置连接池参数
DB.SetMaxOpenConns(50) // 最大连接数
DB.SetMaxIdleConns(10) // 最大空闲连接数
DB.SetConnMaxLifetime(300) // 连接最大生命周期(秒)
// 测试连接
if err := DB.Ping(); err != nil {
return fmt.Errorf("数据库连接测试失败: %w", err)
}
fmt.Println("数据库连接成功")
return nil
}
// Close 关闭数据库连接
func Close() {
if DB != nil {
DB.Close()
}
}
// CreateUser 插入用户
func CreateUser(username, email string, age int) (uint, error) {
var id uint
err := DB.Get(&id,
"INSERT INTO users (username, email, age) VALUES (?, ?, ?)",
username, email, age,
)
return id, err
}
// GetUserByID 根据ID查询用户
func GetUserByID(id uint) (*User, error) {
var user User
err := DB.Get(&user, "SELECT * FROM users WHERE id = ?", id)
if err != nil {
return nil, err
}
return &user, nil
}
// GetAllUsers 查询所有用户
func GetAllUsers() ([]*User, error) {
var users []*User
err := DB.Select(&users, "SELECT * FROM users ORDER BY id ASC")
if err != nil {
return nil, err
}
return users, nil
}
// UpdateUser 更新用户
func UpdateUser(id uint, username, email string, age int) error {
_, err := DB.Exec(
"UPDATE users SET username=?, email=?, age=? WHERE id=?",
username, email, age, id,
)
return err
}
// DeleteUser 删除用户
func DeleteUser(id uint) error {
_, err := DB.Exec("DELETE FROM users WHERE id=?", id)
return err
}
6.3 数据库表结构
CREATE DATABASE IF NOT EXISTS myserver DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE myserver;
CREATE TABLE IF NOT EXISTS users (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(20) NOT NULL UNIQUE,
email VARCHAR(100) NOT NULL UNIQUE,
age TINYINT UNSIGNED NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_username (username),
INDEX idx_email (email)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
第七章:中间件——给服务器加上超能力
中间件是Web框架中非常强大的概念,它可以在请求处理前后执行一些通用逻辑,比如日志记录、权限验证、CORS处理等。
7.1 自定义日志中间件
package middleware
import (
"log"
"time"
"github.com/gin-gonic/gin"
)
// Logger 请求日志中间件
func Logger() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
// 执行请求
c.Next()
// 记录日志
log.Printf(
"[%s] %s %s - 状态码: %d - 耗时: %v",
c.Request.Method,
c.Request.URL.Path,
c.ClientIP(),
c.Writer.Status(),
time.Since(start),
)
}
}
使用方式:
r := gin.New()
r.Use(middleware.Logger()) // 全局使用
7.2 CORS中间件——解决跨域问题
前后端分离项目中,跨域是必须解决的问题:
package middleware
import (
"github.com/gin-gonic/gin"
)
// CORS 跨域中间件
func CORS(allowedOrigins ...string) gin.HandlerFunc {
return func(c *gin.Context) {
origin := c.Request.Header.Get("Origin")
// 检查origin是否在白名单中
allowOrigin := ""
for _, o := range allowedOrigins {
if o == origin || o == "*" {
allowOrigin = o
break
}
}
if allowOrigin == "" {
allowOrigin = "*"
}
c.Header("Access-Control-Allow-Origin", allowOrigin)
c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization")
c.Header("Access-Control-Allow-Credentials", "true")
// 预检请求直接返回
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(204)
return
}
c.Next()
}
}
7.3 鉴权中间件
package middleware
import (
"net/http"
"strings"
"github.com/gin-gonic/gin"
)
// Auth 鉴权中间件
func Auth() gin.HandlerFunc {
return func(c *gin.Context) {
// 从Header获取Token
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "缺少认证信息"})
c.Abort()
return
}
// 简单验证(实际项目应该验证JWT等)
token := strings.TrimPrefix(authHeader, "Bearer ")
if token == "" || len(token) < 10 {
c.JSON(http.StatusUnauthorized, gin.H{"error": "无效的Token"})
c.Abort()
return
}
// 验证通过,将用户信息存入Context
// 实际项目中这里应该解析JWT,获取用户ID等信息
c.Set("userID", "12345")
c.Next()
}
}
第八章:错误处理——让服务器更健壮
Go的错误处理哲学是”错误是值”,它不鼓励异常机制,而是让你显式处理每一个错误。
8.1 统一错误处理
package errors
import (
"fmt"
"net/http"
"github.com/gin-gonic/gin"
)
// AppError 应用错误
type AppError struct {
Code int `json:"code"`
Message string `json:"message"`
}
// NotFound 404错误
func NotFound(message string) *AppError {
return &AppError{Code: 404, Message: message}
}
// BadRequest 400错误
func BadRequest(message string) *AppError {
return &AppError{Code: 400, Message: message}
}
// Unauthorized 401错误
func Unauthorized(message string) *AppError {
return &AppError{Code: 401, Message: message}
}
// Conflict 409错误
func Conflict(message string) *AppError {
return &AppError{Code: 409, Message: message}
}
// InternalServerError 500错误
func InternalServerError(message string) *AppError {
return &AppError{Code: 500, Message: message}
}
// SendError 发送错误响应
func SendError(c *gin.Context, err *AppError) {
c.JSON(err.Code, gin.H{
"code": err.Code,
"message": err.Message,
})
}
// WrapError 包装错误
func WrapError(err error, message string) error {
if err == nil {
return nil
}
return fmt.Errorf("%s: %w", message, err)
}
8.2 全局错误恢复中间件
package middleware
import (
"log"
"github.com/gin-gonic/gin"
"myserver/errors"
)
// Recovery 全局错误恢复
func Recovery() gin.HandlerFunc {
return func(c *gin.Context) {
defer func() {
if err := recover(); err != nil {
log.Printf("panic recovered: %v", err)
errors.SendError(c, errors.InternalServerError("服务器内部错误"))
c.Abort()
}
}()
c.Next()
}
}
第九章:配置文件管理——告别硬编码
随着项目变大,硬编码配置会让维护变得痛苦。我们来引入配置文件管理。
9.1 使用Viper管理配置
go get github.com/spf13/viper
package config
import (
"fmt"
"github.com/spf13/viper"
)
// Config 配置结构
type Config struct {
Server ServerConfig
DB DBConfig
Redis RedisConfig
}
type ServerConfig struct {
Port int
Mode string // debug, release, test
}
type DBConfig struct {
Host int
Port int
User string
Password string
Database string
}
type RedisConfig struct {
Addr string
Password string
DB int
}
// Load 加载配置
func Load(configPath string) (*Config, error) {
viper.SetConfigFile(configPath)
viper.SetConfigType("yaml")
if err := viper.ReadInConfig(); err != nil {
return nil, fmt.Errorf("读取配置文件失败: %w", err)
}
var cfg Config
if err := viper.Unmarshal(&cfg); err != nil {
return nil, fmt.Errorf("解析配置失败: %w", err)
}
return &cfg, nil
}
// GetDSN 获取数据库连接字符串
func (d *DBConfig) GetDSN() string {
return fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local",
d.User, d.Password, d.Host, d.Port, d.Database,
)
}
配置文件 config.yaml:
server:
port: 8080
mode: debug
db:
host: localhost
port: 3306
user: root
password: 123456
database: myserver
redis:
addr: localhost:6379
password: ""
db: 0
第十章:项目结构——让代码井井有条
当项目变得复杂,清晰的结构就至关重要了。来看看一个典型的Go Web项目结构:
myserver/
├── cmd/
│ └── server/
│ └── main.go # 程序入口
├── internal/
│ ├── handler/ # HTTP处理器
│ │ └── user.go
│ ├── service/ # 业务逻辑层
│ │ └── user.go
│ ├── model/ # 数据模型
│ │ └── user.go
│ ├── middleware/ # 中间件
│ │ ├── logger.go
│ │ ├── cors.go
│ │ └── auth.go
│ ├── repository/ # 数据访问层
│ │ └── user.go
│ └── config/ # 配置
│ └── config.go
├── pkg/
│ └── errors/ # 公共错误处理
│ └── errors.go
├── migrations/ # 数据库迁移
│ └── 001_create_users.sql
├── configs/ # 配置文件
│ ├── config.yaml
│ └── config.prod.yaml
├── go.mod
├── go.sum
└── README.md
为什么用 internal 目录? Go编译器会阻止外部包导入 internal 目录下的包,这保证了某些代码不会被外部项目使用,是Go官方推荐的项目组织方式。
第十一章:从内存到数据库——完整项目重构
现在我们把前面的知识整合起来,写一个完整的、连接真实数据库的项目:
// cmd/server/main.go
package main
import (
"log"
"myserver/internal/config"
"myserver/internal/db"
"myserver/internal/middleware"
"myserver/internal/handler"
"myserver/internal/service"
"github.com/gin-gonic/gin"
)
func main() {
// 加载配置
cfg, err := config.Load("configs/config.yaml")
if err != nil {
log.Fatalf("加载配置失败: %v", err)
}
// 初始化数据库
if err := db.Init(cfg.DB.GetDSN()); err != nil {
log.Fatalf("初始化数据库失败: %v", err)
}
defer db.Close()
// 初始化服务层
userService := service.NewUserService()
// 初始化处理器
userHandler := handler.NewUserHandler(userService)
// 创建Gin引擎
gin.SetMode(cfg.Server.Mode)
r := gin.New()
// 注册中间件
r.Use(gin.Recovery()) // 全局恢复
r.Use(middleware.Logger()) // 日志
r.Use(middleware.CORS()) // CORS
// 注册路由
api := r.Group("/api/v1")
{
users := api.Group("/users")
{
users.GET("", userHandler.List)
users.POST("", userHandler.Create)
users.GET("/:id", userHandler.Get)
users.PUT("/:id", userHandler.Update)
users.DELETE("/:id", userHandler.Delete)
}
}
// 健康检查
r.GET("/health", func(c *gin.Context) {
c.JSON(200, gin.H{"status": "ok"})
})
log.Printf("服务器启动在 http://localhost:%d", cfg.Server.Port)
log.Fatal(r.Run(fmt.Sprintf(":%d", cfg.Server.Port)))
}
// internal/repository/user.go
package repository
import (
"myserver/internal/model"
"myserver/internal/db"
)
type UserRepository struct{}
func NewUserRepository() *UserRepository {
return &UserRepository{}
}
func (r *UserRepository) Create(user *model.User) error {
query := `INSERT INTO users (username, email, age) VALUES (?, ?, ?)`
result, err := db.DB.Exec(query, user.Username, user.Email, user.Age)
if err != nil {
return err
}
id, err := result.LastInsertId()
if err != nil {
return err
}
user.ID = uint(id)
user.CreatedAt = time.Now()
user.UpdatedAt = time.Now()
return nil
}
func (r *UserRepository) FindByID(id uint) (*model.User, error) {
var user model.User
err := db.DB.Get(&user, "SELECT * FROM users WHERE id = ?", id)
if err != nil {
return nil, err
}
return &user, nil
}
func (r *UserRepository) FindAll() ([]*model.User, error) {
var users []*model.User
err := db.DB.Select(&users, "SELECT * FROM users ORDER BY id ASC")
return users, err
}
func (r *UserRepository) Update(id uint, user *model.User) error {
_, err := db.DB.Exec(
`UPDATE users SET username=?, email=?, age=? WHERE id=?`,
user.Username, user.Email, user.Age, id,
)
return err
}
func (r *UserRepository) Delete(id uint) error {
_, err := db.DB.Exec("DELETE FROM users WHERE id=?", id)
return err
}
第十二章:性能优化——让服务器飞起来
12.1 连接池调优
// 在 db 初始化时设置
db.SetMaxOpenConns(100) // 最大连接数,根据并发量调整
db.SetMaxIdleConns(20) // 最小空闲连接数
db.SetConnMaxLifetime(300) // 连接最大存活时间(秒)
db.SetConnMaxIdleTime(120) // 连接最大空闲时间(秒)
12.2 使用缓存
对于频繁查询的数据,可以加上Redis缓存:
package cache
import (
"context"
"encoding/json"
"github.com/go-redis/redis/v8"
"myserver/internal/model"
)
var rdb *redis.Client
func Init(addr, password string, db int) {
rdb = redis.NewClient(&redis.Options{
Addr: addr,
Password: password,
DB: db,
})
}
func GetUser(ctx context.Context, id uint) (*model.User, error) {
key := "user:" + strconv.Itoa(int(id))
// 尝试从缓存获取
data, err := rdb.Get(ctx, key).Bytes()
if err == nil {
var user model.User
if err := json.Unmarshal(data, &user); err == nil {
return &user, nil
}
}
// 缓存未命中,从数据库获取
// ... 查询数据库逻辑 ...
// 写入缓存,设置5分钟过期
jsonData, _ := json.Marshal(user)
rdb.Set(ctx, key, jsonData, 5*time.Minute)
return user, nil
}
12.3 压测你的服务器
用 Go 的 vegeta 工具进行压测:
# 安装
go get -u github.com/tsenart/vegeta/v12/cmd/vegeta
# 压测
echo "GET http://localhost:8080/api/v1/users" | vegeta attack -duration=10s -rate=100 | vegeta report
第十三章:部署——让服务器跑在生产环境
13.1 编译发布版本
# 交叉编译(在Mac上编译Linux版本)
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o server ./cmd/server
# 查看编译后的文件大小
ls -lh server
13.2 Docker部署
# 多阶段构建,减小镜像体积
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o server ./cmd/server
# 生产镜像
FROM alpine:3.18
WORKDIR /app
COPY --from=builder /app/server .
COPY --from=builder /app/configs ./configs
EXPOSE 8080
CMD ["./server"]
# 构建Docker镜像
docker build -t myserver:latest .
# 运行
docker run -p 8080:8080 -v $(pwd)/configs:/app/configs myserver:latest
13.3 Systemd服务管理
# /etc/systemd/system/myserver.service
[Unit]
Description=My Server Application
After=network.target
[Service]
Type=simple
User=www-data
WorkingDirectory=/opt/myserver
ExecStart=/opt/myserver/server
Restart=always
RestartSec=5
Environment=PATH=/usr/local/go/bin:/usr/bin:/bin
[Install]
WantedBy=multi-user.target
# 启动服务
sudo systemctl start myserver
sudo systemctl enable myserver # 开机自启
sudo systemctl status myserver # 查看状态
第十四章:实战案例——完整用户管理API
最后,我们来写一个完整的、可直接运行的用户管理系统。把所有知识整合在一起:
// 完整项目代码结构
// main.go - 入口
// internal/ - 内部包
// handler/ - 路由处理
// service/ - 业务逻辑
// repository/ - 数据访问
// model/ - 数据模型
// middleware/ - 中间件
// config/ - 配置
// pkg/errors/ - 错误处理
这个项目包含了从入门到进阶的全部知识点:HTTP基础、RESTful设计、数据库操作、中间件、错误处理、配置管理、缓存、部署。
写到这里,相信你已经对Go服务器开发有了全面的认识。Go的学习曲线比较平缓,但深入下去也有许多值得探索的地方——比如RPC框架gRPC、消息队列、分布式系统等。
记住,最好的学习方式就是动手写代码。不要只看教程,动手创建一个项目,遇到问题和错误,然后解决它们。每一次踩坑都是成长的机会。
Go社区非常友好,官方文档也很优秀。当你遇到困难时,先查文档,再搜索社区,大部分问题都能找到答案。
祝你Go语言学习之旅愉快!有问题随时交流。
