Claran's blog

路漫漫其修远兮

2025.11.15

学go的第一月结束了,目前只学到了MySQL,感觉有点慢了

但是后续大概还是不会加快速度,因为还是想兼顾一下高数和其他东西(

估计还是一周一个章节/知识点的龟速吧. . .

另外,学gin的这段时间搓了个人生中的第一个较大的Demo,看着满屏的代码成就感满满. . .

嗯 ~ 我对这个“长子”Demo非常满意

2025.11.22

这周基本没学什么新东西(除了协程池),全用来写红蓝作业了。。。。。。。

2025.11.23

感觉有点慢!这周学个Redis和MongoSQL吧,顺便把Linux看看


问题是什么?

我们现在每周的自习时间满打满算:

  • 周一 13:00 ~ 20:00
  • 周二 13:00 ~ 20:00 (旷电子实训、水课)
  • 周三 19:00 ~ 20:30(下午有高数和按人头坐的英语)
  • 周四 全天 (旷C、医学、水课)
  • 周五 没时间(早八高数、英语)
  • 周末 看情况

也就是说,悲观来说,有三天有较长的自习时间

在这其中,挑一天来总结运用知识并撰写博客,还剩两天

所以,前面几周我都是用这两天每周学一个技术栈并完成蓝山红岩的作业

这就是为什么学这么慢!kuso!

而且已经固定在9204自习了,去九教别的教室会赶出来()


接下来打算学Redis,MongoSQL,Linux,Docker,预计耗费4周(有期中!),希望最好能在12月中旬学完,然后开始着手于寒假考核qaq

另:退赛了CACC,因为和数学期中考重合了

2025.11.24

喜报,主包记错考试时间了,这周得看宋浩了,Redis下周再说

另:你妈的不该退赛CACC,记错考试时间了

另:不对该退赛CACC,当天体侧最后一天qwq

Go语言优雅错误处理指南

概述

本文档详细讲解如何在Go语言项目中实现优雅的错误处理机制,特别是在Gin框架的Handler-Service-DAO分层架构中。

核心原则

1. 错误是值,不是异常

Go语言将错误视为普通返回值,这要求开发者显式处理每个可能的错误。

2. 添加上下文信息

错误在传递过程中应该携带足够的上下文信息,便于问题定位。

3. 统一错误响应

API应该返回统一格式的错误响应,方便客户端处理。

分层错误处理架构

项目结构

1
2
3
4
5
6
7
8
9
10
11
12
13
project/
├── response/
│ └── response.go
├── errors/
│ └── business.go
├── handler/
│ └── user_handler.go
├── service/
│ └── user_service.go
├── dao/
│ └── user_dao.go
└── model/
└── user.go

为什么需要分层错误处理?

各层职责和错误处理策略

层级 职责 错误处理策略 为什么这样设计
DAO层 数据访问,纯技术操作 返回原始错误或基础业务错误 DAO层不应该关心业务逻辑,只负责技术错误
Service层 业务逻辑处理 将技术错误转换为业务错误,添加业务上下文 Service层理解业务含义,知道如何包装错误
Handler层 HTTP请求处理 捕获所有错误,转换为HTTP响应 Handler层是系统边界,需要统一响应格式

错误传递的哲学

  1. DAO层保持纯粹:只处理数据访问相关错误,不添加业务语义
  2. Service层添加业务语义:将技术错误翻译成业务人员能理解的错误
  3. Handler层统一格式化:将错误转换为客户端能理解的格式

源码实现

1. 统一响应包 (response/response.go)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package response

import (
"net/http"

"github.com/gin-gonic/gin"
)

type Response struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
Error string `json:"error,omitempty"`
}

func Success(c *gin.Context, data interface{}) {
c.JSON(http.StatusOK, Response{
Code: 0,
Message: "success",
Data: data,
})
}

func Error(c *gin.Context, code int, message string) {
c.JSON(http.StatusOK, Response{
Code: code,
Message: message,
})
}

func InternalError(c *gin.Context, err error) {
c.Error(err)
c.JSON(http.StatusOK, Response{
Code: 500,
Message: "内部服务器错误",
})
}

func BadRequest(c *gin.Context, message string) {
c.JSON(http.StatusOK, Response{
Code: 400,
Message: message,
})
}

func NotFound(c *gin.Context, message string) {
c.JSON(http.StatusOK, Response{
Code: 404,
Message: message,
})
}

设计理由:统一响应格式确保客户端始终收到结构一致的响应,便于错误处理和用户体验优化。

2. 业务错误定义 (errors/business.go)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package errors

import "fmt"

const (
CodeUserNotFound = 10001
CodeInvalidParam = 10002
CodeDBError = 10003
)

type BusinessError struct {
Code int
Message string
Err error
}

func (e *BusinessError) Error() string {
if e.Err != nil {
return fmt.Sprintf("业务错误[%d]: %s, 原因: %v", e.Code, e.Message, e.Err)
}
return fmt.Sprintf("业务错误[%d]: %s", e.Code, e.Message)
}

func (e *BusinessError) Unwrap() error {
return e.Err
}

func NewBusinessError(code int, message string) *BusinessError {
return &BusinessError{
Code: code,
Message: message,
}
}

func WrapBusinessError(code int, message string, err error) *BusinessError {
return &BusinessError{
Code: code,
Message: message,
Err: err,
}
}

var (
ErrUserNotFound = NewBusinessError(CodeUserNotFound, "用户不存在")
ErrInvalidParam = NewBusinessError(CodeInvalidParam, "参数错误")
)

设计理由:自定义错误类型可以携带丰富的上下文信息(错误码、消息、原始错误),支持错误链追踪。

3. 数据模型 (model/user.go)

1
2
3
4
5
6
7
8
package model

type User struct {
ID int64 `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Status string `json:"status"`
}

4. DAO层 (dao/user_dao.go)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package dao

import (
"database/sql"
"fmt"

"your-project/errors"
"your-project/model"
)

type UserDAO struct {
db *sql.DB
}

func NewUserDAO(db *sql.DB) *UserDAO {
return &UserDAO{db: db}
}

func (d *UserDAO) GetUserByID(userID int64) (*model.User, error) {
const query = "SELECT id, name, email, status FROM users WHERE id = ? AND deleted = 0"

var user model.User
err := d.db.QueryRow(query, userID).Scan(&user.ID, &user.Name, &user.Email, &user.Status)

if err != nil {
if err == sql.ErrNoRows {
return nil, errors.ErrUserNotFound
}
return nil, fmt.Errorf("查询用户失败 (ID: %d): %w", userID, err)
}

return &user, nil
}

func (d *UserDAO) CreateUser(user *model.User) error {
const query = "INSERT INTO users (name, email) VALUES (?, ?)"

result, err := d.db.Exec(query, user.Name, user.Email)
if err != nil {
return fmt.Errorf("创建用户失败: %w", err)
}

userID, err := result.LastInsertId()
if err != nil {
return fmt.Errorf("获取用户ID失败: %w", err)
}

user.ID = userID
return nil
}

DAO层错误传递策略

  • 遇到sql.ErrNoRows时返回业务错误ErrUserNotFound,因为”用户不存在”是业务逻辑的一部分
  • 其他数据库错误使用%w包装,保留原始错误信息但添加上下文
  • 不直接返回HTTP状态码,因为DAO层不应该知道HTTP协议

5. Service层 (service/user_service.go)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
package service

import (
"your-project/dao"
"your-project/errors"
"your-project/model"
)

type UserService struct {
userDAO *dao.UserDAO
}

func NewUserService(userDAO *dao.UserDAO) *UserService {
return &UserService{userDAO: userDAO}
}

type CreateUserRequest struct {
Name string `json:"name" binding:"required"`
Email string `json:"email" binding:"required,email"`
}

func (s *UserService) GetUser(userID int64) (*model.User, error) {
if userID <= 0 {
return nil, errors.WrapBusinessError(
errors.CodeInvalidParam,
"用户ID必须大于0",
nil,
)
}

user, err := s.userDAO.GetUserByID(userID)
if err != nil {
var businessErr *errors.BusinessError
if errors.As(err, &businessErr) {
return nil, err
}

return nil, errors.WrapBusinessError(
errors.CodeDBError,
"获取用户信息失败",
err,
)
}

if user.Status == "banned" {
return nil, errors.NewBusinessError(10004, "用户已被封禁")
}

return user, nil
}

func (s *UserService) CreateUser(req *CreateUserRequest) (*model.User, error) {
if req.Name == "" {
return nil, errors.WrapBusinessError(
errors.CodeInvalidParam,
"用户名不能为空",
nil,
)
}

if len(req.Name) < 2 || len(req.Name) > 20 {
return nil, errors.WrapBusinessError(
errors.CodeInvalidParam,
"用户名长度必须在2-20个字符之间",
nil,
)
}

user := &model.User{
Name: req.Name,
Email: req.Email,
}

if err := s.userDAO.CreateUser(user); err != nil {
return nil, errors.WrapBusinessError(
errors.CodeDBError,
"创建用户失败",
err,
)
}

return user, nil
}

Service层错误传递策略

  • 进行业务参数验证,将无效参数转换为业务错误
  • 使用errors.As()检查错误类型,如果是业务错误直接传递
  • 将DAO层的技术错误包装为业务错误,添加业务语义
  • 实现业务规则验证(如用户状态检查)
  • 不涉及HTTP概念,保持业务逻辑的纯粹性

6. Handler层 (handler/user_handler.go)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
package handler

import (
"net/http"
"strconv"

"github.com/gin-gonic/gin"

"your-project/errors"
"your-project/response"
"your-project/service"
)

type UserHandler struct {
userService *service.UserService
}

func NewUserHandler(userService *service.UserService) *UserHandler {
return &UserHandler{userService: userService}
}

func (h *UserHandler) GetUser(c *gin.Context) {
userIDStr := c.Param("id")
userID, err := strconv.ParseInt(userIDStr, 10, 64)
if err != nil {
response.BadRequest(c, "无效的用户ID")
return
}

user, err := h.userService.GetUser(userID)
if err != nil {
h.handleError(c, err)
return
}

response.Success(c, user)
}

func (h *UserHandler) CreateUser(c *gin.Context) {
var req service.CreateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "请求参数格式错误")
return
}

user, err := h.userService.CreateUser(&req)
if err != nil {
h.handleError(c, err)
return
}

response.Success(c, user)
}

func (h *UserHandler) handleError(c *gin.Context, err error) {
var businessErr *errors.BusinessError
if errors.As(err, &businessErr) {
response.Error(c, businessErr.Code, businessErr.Message)
return
}

response.InternalError(c, err)
}

Handler层错误处理策略

  • 处理HTTP特定错误(参数解析、数据绑定)
  • 统一的错误处理入口handleError
  • 区分业务错误系统错误,分别处理
  • 将错误转换为统一的HTTP响应格式
  • 记录错误日志(在生产环境中可能隐藏内部错误细节)

7. 主程序 (main.go)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package main

import (
"database/sql"
"log"

"github.com/gin-gonic/gin"
_ "github.com/go-sql-driver/mysql"

"your-project/dao"
"your-project/handler"
"your-project/service"
)

func main() {
db, err := sql.Open("mysql", "user:password@/dbname")
if err != nil {
log.Fatal("数据库连接失败:", err)
}
defer db.Close()

userDAO := dao.NewUserDAO(db)
userService := service.NewUserService(userDAO)
userHandler := handler.NewUserHandler(userService)

r := gin.Default()

r.GET("/users/:id", userHandler.GetUser)
r.POST("/users", userHandler.CreateUser)

log.Println("服务器启动在 :8080")
r.Run(":8080")
}

错误处理流程示例

成功流程

1
2
3
4
5
6
7
请求: GET /users/123
Handler: 解析参数,调用service.GetUser(123)
Service: 参数验证,调用dao.GetUserByID(123)
DAO: 执行SQL,返回用户数据
Service: 返回用户数据
Handler: response.Success(c, user)
响应: { "code": 0, "message": "success", "data": { ... } }

错误流程(用户不存在)

1
2
3
4
5
6
7
请求: GET /users/999
Handler: 解析参数,调用service.GetUser(999)
Service: 参数验证,调用dao.GetUserByID(999)
DAO: SQL返回ErrNoRows,返回errors.ErrUserNotFound
Service: 传递errors.ErrUserNotFound
Handler: h.handleError → response.Error(10001, "用户不存在")
响应: { "code": 10001, "message": "用户不存在" }

错误流程(数据库连接失败)

1
2
3
4
5
6
7
请求: GET /users/123
Handler: 解析参数,调用service.GetUser(123)
Service: 参数验证,调用dao.GetUserByID(123)
DAO: 数据库连接失败,返回原始错误
Service: 包装为业务错误"获取用户信息失败"
Handler: h.handleError → response.InternalError
响应: { "code": 500, "message": "内部服务器错误" }

各层错误传递的设计哲学

1. 关注点分离 (Separation of Concerns)

  • DAO层只关心数据访问技术细节
  • Service层只关心业务逻辑和规则
  • Handler层只关心HTTP协议和用户交互

2. 错误信息 enrichment(丰富化)

错误在向上传递过程中不断添加上下文信息:

  • DAO: “查询失败”
  • Service: “获取用户信息失败:查询失败”
  • Handler: HTTP 500 + 日志记录

3. 错误类型转换

将底层技术错误转换为高层业务概念:

  • sql.ErrNoRowsErrUserNotFound → HTTP 404
  • sql.ErrConnDoneErrDBError → HTTP 500

4. 防御性编程

每层都进行适当的验证,尽早失败,避免错误传播到不合适的层级。

最佳实践总结

  1. 分层处理:各司其职,避免层间职责混淆
  2. 错误包装:使用错误链保留完整上下文
  3. 统一格式:客户端友好的错误响应格式
  4. 适当日志:在适当层级记录适当详情的日志
  5. 错误分类:区分可预期业务错误和意外系统错误

通过这种架构,可以实现清晰、可维护的错误处理机制,提高代码质量和系统稳定性。

0%