Go 没有 try-catch。错误就是一个值,跟 int、string 一样可以被:
gotype error interface {
Error() string
}
只要实现了 Error() string 就算是一个 error。
| 语言 | 错误处理方式 | 特点 |
|---|---|---|
| Java/Python | try-catch 异常 | 调用链自动展开,可能被忽略 |
| Go | error 作为返回值 | 显式处理,不会漏掉 |
| Rust | Result<T, E> | 类似 Go 的 (T, error) |
Go 的哲学: 错误是正常流程的一部分,不是意外。所以你要显式处理它。
goimport "errors"
var ErrNotFound = errors.New("item not found")
errors.New 底层就是返回一个 *errorString:
gofunc New(text string) error {
return &errorString{text}
}
type errorString struct {
s string
}
func (e *errorString) Error() string {
return e.s
}
特点: 包级变量,全局唯一,调用方可以用 == 比较。
goerr := fmt.Errorf("user %d not found: %w", userID, ErrNotFound)
%w 创建 wrapped error(见第 5 节),%v 只生成字符串不保留链。
gotype ValidationError struct {
Field string
Value interface{}
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation failed on %s: %s", e.Field, e.Message)
}
见第 4 节详细展开。
"哨兵"这个名字来自于 C 语言中表示边界的特殊值。这里指用特定变量标识一种已知错误。
gopackage errors
import "errors"
var (
ErrNotFound = errors.New("resource not found")
ErrPermission = errors.New("permission denied")
ErrTimeout = errors.New("operation timed out")
ErrInvalidInput = errors.New("invalid input")
)
通常放在一个独立的包(比如 pkg/errors/ 或 internal/errors/)里。
gofunc GetUserByID(id int64) (*User, error) {
row := db.QueryRow("SELECT * FROM users WHERE id = ?", id)
var u User
err := row.Scan(&u.Name, &u.Email)
if err == sql.ErrNoRows {
return nil, ErrNotFound // 返回哨兵
}
return &u, nil
}
// 调用方
user, err := GetUserByID(42)
if errors.Is(err, ErrNotFound) {
// 处理"找不到"的情况
} else if err != nil {
// 处理其他错误
}
go// ❌ 不推荐
if err == ErrNotFound { ... }
// ✅ 推荐
if errors.Is(err, ErrNotFound) { ... }
因为 %w 包装后的错误不再是原来的哨兵,但它们包含原哨兵:
goerr := fmt.Errorf("查询用户失败: %w", ErrNotFound)
// err != ErrNotFound ✅ 成立
// errors.Is(err, ErrNotFound) ✅ 也成立
所以 errors.Is 会沿着错误链递归查找,不管包装了多少层。
gosql.ErrNoRows // 查询无结果
io.EOF // 读到文件尾
io.ErrUnexpectedEOF // 意外读完
context.Canceled // context 被取消
context.DeadlineExceeded // 超时
gotype AppError struct {
Code int // 业务错误码
Message string // 人类可读消息
Err error // 原始错误(可选)
}
// 实现 error 接口
func (e *AppError) Error() string {
if e.Err != nil {
return fmt.Sprintf("code=%d: %s (cause: %v)", e.Code, e.Message, e.Err)
}
return fmt.Sprintf("code=%d: %s", e.Code, e.Message)
}
// 实现 Unwrap,支持 errors.Is/As 链式查找
func (e *AppError) Unwrap() error {
return e.Err
}
go// 创建
func processOrder(id int) error {
if id <= 0 {
return &AppError{
Code: 400,
Message: "invalid order id",
Err: fmt.Errorf("received id=%d", id),
}
}
// ...
}
// 调用方提取
var appErr *AppError
if errors.As(err, &appErr) {
// 拿到了 appErr,可以读 Code 和 Message
fmt.Printf("业务错误 %d: %s\n", appErr.Code, appErr.Message)
}
// 或者直接 switch
var appErr *AppError
if errors.As(err, &appErr) {
switch appErr.Code {
case 400:
// 参数错误
case 401:
// 未认证
}
}
gotype ErrorWithStack struct {
Err error
Stack string
}
func (e *ErrorWithStack) Error() string {
return e.Err.Error()
}
func (e *ErrorWithStack) Unwrap() error {
return e.Err
}
func New(msg string) error {
return &ErrorWithStack{
Err: errors.New(msg),
Stack: getStackTrace(),
}
}
func getStackTrace() string {
// runtime.Caller 获取调用栈
var buf [1024]byte
n := runtime.Stack(buf[:], false)
return string(buf[:n])
}
Go 1.13 引入了 %w,可以在错误周围"包裹"上下文,同时保留原始错误。
go// ❌ 不包装:丢失原始错误
func LoadConfig() error {
err := readFile("config.yaml")
if err != nil {
return fmt.Errorf("加载配置失败: %v", err)
// %v 只把 err 转成字符串,调用方拿不到原始错误了
}
}
// ✅ 包装:保留原始错误
func LoadConfig() error {
err := readFile("config.yaml")
if err != nil {
return fmt.Errorf("加载配置失败: %w", err)
// %w 保留了原始错误,errors.Is/As 可以穿透
}
}
// 调用方
err := LoadConfig()
if errors.Is(err, os.ErrNotExist) {
// %v: ❌ 穿透不了
// %w: ✅ 能穿透,找到 os.ErrNotExist
}
go// 三层嵌套的错误链
func readFile(path string) error {
return os.ErrNotExist // 最底层
}
func LoadConfig() error {
err := readFile("config.yaml")
if err != nil {
return fmt.Errorf("加载配置失败: %w", err) // 中间层
}
}
func StartServer() error {
err := LoadConfig()
if err != nil {
return fmt.Errorf("启动服务失败: %w", err) // 最外层
}
}
// 调用方
err := StartServer()
if errors.Is(err, os.ErrNotExist) {
// ✅ 穿透 3 层包装,找到 os.ErrNotExist
}
StartServer() 返回: "启动服务失败: 加载配置失败: file does not exist" ↑ ↑ ↑ fmt.Errorf(%w) fmt.Errorf(%w) os.ErrNotExist (最外层) (中间层) (最底层)
errors.Is 从最外层往里递归遍历,调用每个 Unwrap() 直到找到目标或 nil。
这两个函数是 Go 1.13 引入的,用来在错误链中查找特定错误。
go// 定义
func Is(err, target error) bool
// 判断 err 是否包含 target(沿着错误链查找)
查找逻辑:
err == target → trueerr 实现了 Is(error) bool → 调这个方法err 实现了 Unwrap() error → 递归err 实现了 Unwrap() []error(Go 1.20+)→ 遍历每个分支使用场景: 检查是否"是某种错误"
goif errors.Is(err, ErrNotFound) {
// 是"找不到"错误
}
if errors.Is(err, io.EOF) {
// 是"读到文件尾"错误
}
if errors.Is(err, context.DeadlineExceeded) {
// 是"超时"错误
}
go// 定义
func As(err error, target interface{}) bool
// target 必须是指向某 error 类型的指针
// 如果找到,会把错误赋值给 target,返回 true
使用场景: 需要获取错误的额外字段
go// 定义了一个带 Code 的错误类型
type HTTPError struct {
Code int
Message string
}
func (e *HTTPError) Error() string {
return fmt.Sprintf("HTTP %d: %s", e.Code, e.Message)
}
// 调用方
var httpErr *HTTPError
if errors.As(err, &httpErr) {
// 拿到了原始的 *HTTPError
fmt.Printf("HTTP status: %d\n", httpErr.Code)
if httpErr.Code == 401 {
// 需要重新登录
}
}
| errors.Is | errors.As | |
|---|---|---|
| 查找目标 | 值(哨兵) | 类型(结构体) |
| 目标类型 | error 值 | error 类型的指针 (e.g. *MyError) |
| 典型场景 | errors.Is(err, ErrNotFound) | errors.As(err, &myErr) |
| 匹配方式 | 按值比较 | 按类型赋值 |
| 多层包装 | ✅ 递归 Unwrap | ✅ 递归 Unwrap |
go// 先判断大类,再提取详情
if errors.Is(err, ErrValidation) {
var validErr *ValidationError
if errors.As(err, &validErr) {
fmt.Printf("字段 %s 验证失败: %s", validErr.Field, validErr.Message)
}
}
panic 不等于其他语言的 throw/raise。 panic 意味着程序遇到了不可恢复的错误。
✅ 适合 panic 的场景:
go// 1. 程序初始化失败(启动时)
func init() {
lis, err := net.Listen("tcp", ":8080")
if err != nil {
panic(fmt.Sprintf("端口 8080 被占用: %v", err))
// 启动不了的服务器没有意义
}
_ = lis
}
// 2. 不可恢复的内部错误
func (s *Server) Start() {
if s == nil {
panic("nil pointer receiver")
}
// ...
}
❌ 不适合 panic 的场景:
go// 输入校验失败 → 返回 error
func GetUser(id int) (*User, error) {
if id <= 0 {
return nil, ErrInvalidID // ✅ return error
// panic("invalid id") ❌ 不能用 panic
}
}
recover 只在 defer 中 有效:
gofunc SafeCall(fn func()) (err error) {
defer func() {
if r := recover(); r != nil {
// 把 panic 转成 error 返回
err = fmt.Errorf("panic: %v", r)
}
}()
fn()
return nil
}
gofunc safeExecute(ctx context.Context, fn func() error) (err error) {
defer func() {
if r := recover(); r != nil {
// 从 panic 中恢复
const size = 64 << 10 // 64KB
buf := make([]byte, size)
buf = buf[:runtime.Stack(buf, false)]
err = fmt.Errorf("panic recovered: %v\nstack:\n%s", r, buf)
// 记录到日志
log.Printf("panic recovered: %v\n%s", r, buf)
}
}()
return fn()
}
// 使用
err := safeExecute(ctx, func() error {
// 这里面的 panic 都会被捕获
return someRiskyOperation()
})
if 程序还能继续 → return error if 程序必须退出 → panic(或 log.Fatal)
gofunc ReadFile(path string) ([]byte, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close() // 一行搞定,但 Close 的错误被忽略了
return io.ReadAll(f)
}
大多数场景下这样够了。但要更严谨:
gofunc ReadFileSafe(path string) (data []byte, err error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
// 在 defer 中捕获 Close 的错误
defer func() {
if cerr := f.Close(); cerr != nil {
// 如果函数本身没有返回错误,则用 Close 的错误
if err == nil {
err = cerr
}
// 如果函数已经有错误了,Close 的错误可以打 log 但不要覆盖
}
}()
data, err = io.ReadAll(f)
return
}
gofunc CopyFile(src, dst string) (written int64, err error) {
srcF, err := os.Open(src)
if err != nil {
return
}
defer srcF.Close()
dstF, err := os.Create(dst)
if err != nil {
return
}
defer func() {
if cerr := dstF.Close(); cerr != nil {
if err == nil {
err = cerr
}
}
}()
written, err = io.Copy(dstF, srcF)
return
}
关键: 在 defer 中通过闭包修改了命名返回值 err。
HTTP层(handler)→ Service层 → Repository层(DB)
go// pkg/ecode/ecode.go
package ecode
type Code int
const (
// 通用错误
Success Code = 0
Unknown Code = -1
InternalError Code = 500
// 业务错误 (4xx)
BadRequest Code = 400
NotFound Code = 404
AlreadyExists Code = 409
ValidationError Code = 422
// 认证错误
Unauthorized Code = 401
Forbidden Code = 403
)
func (c Code) Message() string {
switch c {
case Success:
return "success"
case NotFound:
return "resource not found"
case ValidationError:
return "validation failed"
default:
return "unknown error"
}
}
go// internal/errors/errors.go
package errors
import (
"fmt"
"runtime"
"myapp/pkg/ecode"
)
type AppError struct {
Code ecode.Code
Message string
Err error // 原始错误(内部使用)
Stack string // 错误发生位置的栈
}
func (e *AppError) 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 *AppError) Unwrap() error {
return e.Err
}
// 工厂函数
func New(code ecode.Code, msg string) *AppError {
return &AppError{
Code: code,
Message: msg,
Stack: captureStack(2),
}
}
func Wrap(err error, code ecode.Code, msg string) *AppError {
return &AppError{
Code: code,
Message: msg,
Err: err,
Stack: captureStack(2),
}
}
func captureStack(skip int) string {
const depth = 32
var pcs [depth]uintptr
n := runtime.Callers(skip+1, pcs[:])
frames := runtime.CallersFrames(pcs[:n])
var stack string
for {
frame, more := frames.Next()
stack += fmt.Sprintf("\n %s\n %s:%d", frame.Function, frame.File, frame.Line)
if !more {
break
}
}
return stack
}
// 判断是否是 4xx 客户端错误
func IsClientError(err error) bool {
var appErr *AppError
if errors.As(err, &appErr) {
return appErr.Code >= 400 && appErr.Code < 500
}
return false
}
go// Repository 层:返回原始错误或哨兵
type UserRepo struct { db *sql.DB }
func (r *UserRepo) FindByID(ctx context.Context, id int64) (*User, error) {
var u User
err := r.db.QueryRowContext(ctx, "SELECT * FROM users WHERE id = ?", id).
Scan(&u.Name, &u.Email)
if errors.Is(err, sql.ErrNoRows) {
return nil, sql.ErrNoRows // 返回哨兵
}
return &u, nil
}
// Service 层:包装为业务错误
type UserService struct { repo *UserRepo }
func (s *UserService) GetUser(ctx context.Context, id int64) (*User, error) {
if id <= 0 {
return nil, apperrors.New(ecode.BadRequest, "invalid user id")
}
user, err := s.repo.FindByID(ctx, id)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, apperrors.Wrap(err, ecode.NotFound, "user not found")
}
return nil, apperrors.Wrap(err, ecode.InternalError, "query user failed")
}
return user, nil
}
// Handler 层:将错误转为 HTTP 响应
type UserHandler struct { svc *UserService }
func (h *UserHandler) GetUser(w http.ResponseWriter, r *http.Request) {
id, _ := strconv.ParseInt(r.URL.Query().Get("id"), 10, 64)
user, err := h.svc.GetUser(r.Context(), id)
if err != nil {
var appErr *apperrors.AppError
if errors.As(err, &appErr) {
// 给调用方返回结构化的错误
w.WriteHeader(httpStatusCode(appErr.Code))
json.NewEncoder(w).Encode(map[string]interface{}{
"code": appErr.Code,
"message": appErr.Message,
})
} else {
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]interface{}{
"code": ecode.InternalError,
"message": "internal error",
})
}
return
}
json.NewEncoder(w).Encode(user)
}
这种设计的好处:
| 场景 | Repository | Service | Handler |
|---|---|---|---|
| 用户不存在 | 返回 sql.ErrNoRows | 包装为 NotFoundException | 返回 404 JSON |
| DB 挂了 | 返回 driver.ErrBadConn | 包装为 InternalError | 返回 500 JSON |
| 参数错误 | — | 返回 BadRequestError | 返回 400 JSON |
每层只关注自己该关注的,错误像洋葱一样层层包裹。
go// ❌ 丢失原始错误
return fmt.Errorf("something: %v", err)
// ✅ 保留错误链
return fmt.Errorf("something: %w", err)
goerr := fmt.Errorf("wrap: %w", ErrNotFound)
// ❌ 不成立
if err == ErrNotFound { ... }
// ✅ 成立
if errors.Is(err, ErrNotFound) { ... }
go// ❌ 反直觉
if err {
// ...
}
// ✅ Go 的习惯
if err != nil {
// ...
}
gof, _ := os.Open("file")
defer f.Close()
// Close 的错误被静默丢弃了
// 可能导致数据没写全但你不知道
go// ❌ 把异常当流程控制用
func parseInt(s string) (int, error) {
defer func() {
if r := recover(); r != nil {
// 从 strconv 的 panic 中恢复?别这么干
}
}()
return strconv.Atoi(s) // 实际上 Atoi 返回 error,不 panic
}
// ✅ 正确处理
func parseInt(s string) (int, error) {
return strconv.Atoi(s)
}
gotype MyError struct {
Msg string
}
// ✅ 指针接收者才能被 errors.As 匹配
func (e *MyError) Error() string {
return e.Msg
}
// ❌ 值接收者
func (e MyError) Error() string {
return e.Msg
} // errors.As(&myErr) 需要指针
go// ❌ 如果没有 errors.Is,只能==比较
switch err {
case ErrNotFound:
// ...
case ErrPermission:
// ...
}
// ✅ 用 errors.Is
switch {
case errors.Is(err, ErrNotFound):
// ...
case errors.Is(err, ErrPermission):
// ...
}
创建一个 myerrors 包,要求:
go// 必须支持以下用法
func Process(id int) error {
if id <= 0 {
return myerrors.New(400, "invalid id").
WithField("id", id)
}
return nil
}
// 调用方
var bizErr *myerrors.BizError
if errors.As(err, &bizErr) {
fmt.Println(bizErr.Code, bizErr.Fields)
}
包含:
gopackage myerrors
import (
"fmt"
"runtime"
)
type BizError struct {
Code int
Msg string
Err error
Fields map[string]any
Stack string
}
func getStackTrace() string {
pc := make([]uintptr, 32)
n := runtime.Callers(3, pc) // 跳过 Callers 自身 + getStackTrace + New
frames := runtime.CallersFrames(pc[:n])
var sb strings.Builder
for {
frame, more := frames.Next()
fmt.Fprintf(&sb, "%s\n\t%s:%d\n", frame.Function, frame.File, frame.Line)
if !more {
break
}
}
return sb.String()
}
func New(code int, msg string) *BizError {
bz := &BizError{
Code: code,
Msg: msg,
Err: nil,
Fields: make(map[string]any, 10),
Stack: getStackTrace(),
}
return bz
}
func (bz *BizError) Error() string {
if bz.Err != nil {
return fmt.Sprintf("Code=%d: %s (cause %v)", bz.Code, bz.Msg, bz.Err)
}
return fmt.Sprintf("Code=%d: %s", bz.Code, bz.Msg)
}
func (bz *BizError) Unwrap() error {
return bz.Err
}
func (bz *BizError) WithField(key string, value any) *BizError {
bz.Fields[key] = value
return bz
}
go// 这段代码的问题在哪里?重构它。
func GetOrder(id int) (*Order, error) {
rows, err := db.Query("SELECT * FROM orders WHERE id = ?", id)
if err != nil {
return nil, err // 原始错误直接返回给了调用方
}
defer rows.Close()
if !rows.Next() {
return nil, errors.New("not found") // 每次 new,不能 == 比较
}
var o Order
err = rows.Scan(&o.ID, &o.Amount)
if err != nil {
panic(err) // 啊?panic?
}
return &o, nil
}
govar ErrOrderNotFound = errors.New("order not found")
func GetOrder(id int) (*Order, error) {
rows, err := db.Query("SELECT * FROM orders WHERE id = ?", id)
if err != nil {
return nil, fmt.Errorf("Query Order %d: %w", id, err)
}
defer func() {
if cerr := rows.Close(); cerr != nil && err == nil {
err = cerr
}
}()
if !rows.Next() {
return nil, fmt.Errorf("order %d: %w", id, ErrOrderNotFound) // 每次 new,不能 == 比较
}
var o Order
if err = rows.Scan(&o.ID, &o.Amount); err != nil {
return nil, fmt.Errorf("Scan Order %d: %w", id, err) // 啊?panic?
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("rows iteration order %d: %w", id, err)
}
return &o, nil
}
写程序模拟:
gopackage main
import (
"errors"
"fmt"
"log"
"net/http"
)
// ============================================================
// 1. Domain model
// ============================================================
type User struct {
ID int `json:"id"`
Name string `json:"name"`
}
// ============================================================
// 2. Repository layer — 哨兵错误定义在这里
// ============================================================
var (
ErrUserNotFound = errors.New("user not found")
ErrUserDisabled = errors.New("user disabled")
ErrDatabaseDown = errors.New("database is down")
ErrDuplicateEmail = errors.New("duplicate email")
)
// UserRepository 接口(便于测试和切换实现)
type UserRepository interface {
FindByID(id int) (*User, error)
Save(u *User) error
}
// userRepo 模拟实现
type userRepo struct {
store map[int]*User
}
func NewUserRepository() UserRepository {
return &userRepo{
store: map[int]*User{
1: {ID: 1, Name: "Alice"},
// ID=2 故意缺失,模拟"未找到"
// ID=3 的 user disabled
3: {ID: 3, Name: "Bob"},
},
}
}
func (r *userRepo) FindByID(id int) (*User, error) {
// 模拟数据库宕机
if id == 999 {
return nil, ErrDatabaseDown
}
u, ok := r.store[id]
if !ok {
return nil, ErrUserNotFound
}
if u.ID == 3 {
return nil, ErrUserDisabled
}
return u, nil
}
func (r *userRepo) Save(u *User) error {
// 模拟重复邮箱
if u.ID == 1 {
return fmt.Errorf("save user: %w", ErrDuplicateEmail)
}
return nil
}
// ============================================================
// 3. Service layer — 将哨兵错误包装成业务错误码
// ============================================================
// ErrorCode 业务错误码枚举
type ErrorCode int
const (
CodeOK ErrorCode = 0
CodeNotFound ErrorCode = 1001
CodeUserDisabled ErrorCode = 1002
CodeDatabaseError ErrorCode = 1003
CodeDuplicateEmail ErrorCode = 1004
CodeInternalError ErrorCode = 9999
)
// AppError 业务层统一错误结构
type AppError struct {
Code ErrorCode `json:"code"`
Message string `json:"message"`
Err error `json:"-"` // 原始错误,不暴露给客户端
}
// Error 实现 error 接口
func (e *AppError) 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)
}
// Unwrap 支持 errors.Is / errors.As 遍历错误链
func (e *AppError) Unwrap() error {
return e.Err
}
// WrapError 将 repository 的哨兵错误包装为业务 AppError
func WrapError(err error) error {
if err == nil {
return nil
}
// 如果已经是 AppError,直接返回
var appErr *AppError
if errors.As(err, &appErr) {
return err
}
// 根据底层哨兵错误映射到业务错误码
switch {
case errors.Is(err, ErrUserNotFound):
return &AppError{
Code: CodeNotFound,
Message: "请求的资源不存在",
Err: err,
}
case errors.Is(err, ErrUserDisabled):
return &AppError{
Code: CodeUserDisabled,
Message: "用户已被禁用",
Err: err,
}
case errors.Is(err, ErrDatabaseDown):
return &AppError{
Code: CodeDatabaseError,
Message: "数据库暂时不可用",
Err: err,
}
case errors.Is(err, ErrDuplicateEmail):
return &AppError{
Code: CodeDuplicateEmail,
Message: "邮箱已被注册",
Err: err,
}
default:
return &AppError{
Code: CodeInternalError,
Message: "服务器内部错误",
Err: err,
}
}
}
// UserService 业务接口
type UserService interface {
GetUser(id int) (*User, error)
CreateUser(u *User) error
}
type userService struct {
repo UserRepository
}
func NewUserService(repo UserRepository) UserService {
return &userService{repo: repo}
}
// GetUser — Service 层调用 Repository,并包装错误
func (s *userService) GetUser(id int) (*User, error) {
user, err := s.repo.FindByID(id)
if err != nil {
// 用 %w 保留原始错误链,再用 WrapError 包装为 AppError
return nil, WrapError(fmt.Errorf("get user %d: %w", id, err))
}
return user, nil
}
func (s *userService) CreateUser(u *User) error {
if err := s.repo.Save(u); err != nil {
return WrapError(fmt.Errorf("create user: %w", err))
}
return nil
}
// ============================================================
// 4. Handler layer — 根据 AppError.Code 返回不同 HTTP 状态码
// ============================================================
// errorToHTTPStatus 映射业务错误码 → HTTP 状态码
func errorToHTTPStatus(code ErrorCode) int {
switch code {
case CodeNotFound:
return http.StatusNotFound // 404
case CodeUserDisabled:
return http.StatusForbidden // 403
case CodeDuplicateEmail:
return http.StatusConflict // 409
case CodeDatabaseError:
return http.StatusServiceUnavailable // 503
case CodeInternalError:
return http.StatusInternalServerError // 500
default:
return http.StatusInternalServerError
}
}
// UserHandler HTTP handler
type UserHandler struct {
svc UserService
}
func NewUserHandler(svc UserService) *UserHandler {
return &UserHandler{svc: svc}
}
// GetUserHandler 处理 GET /user?id=xxx
func (h *UserHandler) GetUserHandler(w http.ResponseWriter, r *http.Request) {
// 从查询参数解析 id
id := 1 // 默认
if s := r.URL.Query().Get("id"); s != "" {
fmt.Sscanf(s, "%d", &id)
}
user, err := h.svc.GetUser(id)
if err != nil {
// 统一错误处理:提取 AppError → 返回对应 HTTP 状态码 + JSON body
var appErr *AppError
if errors.As(err, &appErr) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(errorToHTTPStatus(appErr.Code))
fmt.Fprintf(w, `{"code":%d,"message":"%s"}`, appErr.Code, appErr.Message)
log.Printf("[Handler] 返回错误: code=%d, msg=%s, original=%v",
appErr.Code, appErr.Message, appErr.Err)
} else {
// 非 AppError 兜底(理论上不会走到这里)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprint(w, `{"code":9999,"message":"服务器内部错误"}`)
log.Printf("[Handler] 未知错误类型: %v", err)
}
return
}
// 成功返回
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, `{"id":%d,"name":"%s"}`, user.ID, user.Name)
}
// ============================================================
// 5. main — 启动模拟服务器并打印测试说明
// ============================================================
func main() {
// 组装依赖链:Handler → Service → Repository
repo := NewUserRepository()
svc := NewUserService(repo)
handler := NewUserHandler(svc)
http.HandleFunc("/user", handler.GetUserHandler)
addr := ":8080"
fmt.Printf("模拟服务器启动在 http://localhost%s\n\n", addr)
fmt.Println("========== 测试用 URL ==========")
fmt.Println(" GET /user?id=1 → 200 OK(Alice)")
fmt.Println(" GET /user?id=2 → 404 Not Found")
fmt.Println(" GET /user?id=3 → 403 Forbidden")
fmt.Println(" GET /user?id=999 → 503 Service Unavailable")
fmt.Println("================================")
if err := http.ListenAndServe(addr, nil); err != nil {
log.Fatalf("服务器启动失败: %v", err)
}
}
go// 实现一个 WriteFile 函数
// 1. 创建文件
// 2. 写入数据
// 3. 在 defer 中 Close
// 4. 如果 Close 失败且 Write 没有失败,返回 Close 的错误
func WriteFile(path string, data []byte) (err error) {
// 你的代码
}
go// 实现一个 WriteFile 函数
// 1. 创建文件
// 2. 写入数据
// 3. 在 defer 中 Close
// 4. 如果 Close 失败且 Write 没有失败,返回 Close 的错误
func WriteFile(path string, data []byte) (err error) {
// 你的代码
file, err := os.Create(path)
if err != nil {
return err
}
defer func() {
if cerr := file.Close(); cerr != nil && err == nil {
err = cerr
}
}()
buf := bufio.NewWriter(file)
if _, berr := buf.Write(data); berr != nil {
return
}
if ferr := buf.Flush(); ferr != nil {
return
}
return
}
go// 📌 创建错误
errors.New("msg") // 最简单的哨兵
fmt.Errorf("wrap: %w", err) // 带上下文的包装
&AppError{Code: 400, ...} // 自定义类型
// 📌 检查错误
errors.Is(err, ErrNotFound) // 检查值(哨兵)
errors.As(err, &myErr) // 提取类型(自定义)
err != nil // 检查是否有错误
// 📌 包装和解包
fmt.Errorf("ctx: %w", err) // 包装(保留链)
errors.Unwrap(err) // 解一层
// 📌 panic
panic("fatal") // 只有不可恢复时用
defer func() { recover() } // 在 defer 中捕获
// 📌 黄金原则
// return error > panic
// %w > %v when wrapping
// errors.Is/As > ==