ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Go 后端开发实战(10):做一个完整的 Go 后端服务

Go 后端开发实战(10):做一个完整的 Go 后端服务 前九篇依次建立语言与错误基础、并发生命周期、HTTP、框架边界、数据库、中间件、可观测性、测试和部署。本篇把方法收束为一个可直接运行的任务服务API 支持创建与查询仓储可替换输入有边界并具备优雅关闭路径。一、先按用例切出最小纵向功能完整不等于功能繁多而是关键生命周期闭环。任务服务包含POST /v1/tasks、GET /v1/tasks/{id}、GET /healthz创建请求校验标题service 生成状态repository 保存Handler 映射错误。生产版把内存仓储替换为 PostgreSQL并保留相同接口。推荐目录为cmd/api/main.go、internal/task/service.go、internal/store/postgres.go、internal/httpapi/handler.go。main 是组合根只读取配置、创建 logger/DB/service/server 并管理关闭。包依赖从入口和适配器指向领域领域不导入 Gin、SQL 驱动或环境变量。示例为了复制方便放在单文件但边界用类型清楚表达。创建接口要返回 201 与 Location数据库负责 ID、时间和唯一约束时响应以提交后的记录为准。读取不存在映射 404。请求体限制大小、拒绝未知字段并确认只有一个 JSON 值。任务标题在 service 再校验一次因为调用者将来可能不止 HTTP。下面是完整单文件 API通过httptest运行创建与查询全流程。保存后go run main.go即可复现不依赖前文产物。packagemainimport(contextencoding/jsonerrorsfmtnet/httpnet/http/httpteststrconvstringssync)varErrNotFounderrors.New(task not found)typeTaskstruct{IDintjson:id;Titlestringjson:title;Statusstringjson:status}typeRepositoryinterface{Create(context.Context,Task)(Task,error);Get(context.Context,int)(Task,error)}typeMemoryRepositorystruct{mu sync.RWMutex;nextint;itemsmap[int]Task}func(r*MemoryRepository)Create(_context.Context,task Task)(Task,error){r.mu.Lock();deferr.mu.Unlock()r.next;task.IDr.next;r.items[task.ID]task;returntask,nil}func(r*MemoryRepository)Get(_context.Context,idint)(Task,error){r.mu.RLock();deferr.mu.RUnlock()task,ok:r.items[id];if!ok{returnTask{},ErrNotFound};returntask,nil}typeServicestruct{repo Repository}func(s Service)Create(ctx context.Context,titlestring)(Task,error){titlestrings.TrimSpace(title);iflen(title)2{returnTask{},fmt.Errorf(invalid title)}returns.repo.Create(ctx,Task{Title:title,Status:pending})}typeAPIstruct{service Service}func(a API)routes()http.Handler{mux:http.NewServeMux();mux.HandleFunc(POST /v1/tasks,a.create);mux.HandleFunc(GET /v1/tasks/{id},a.get);returnmux}func(a API)create(w http.ResponseWriter,r*http.Request){varinputstruct{Titlestringjson:title}iferr:json.NewDecoder(r.Body).Decode(input);err!nil{writeJSON(w,400,map[string]string{code:invalid_json});return}task,err:a.service.Create(r.Context(),input.Title);iferr!nil{writeJSON(w,422,map[string]string{code:invalid_title});return}w.Header().Set(Location,fmt.Sprintf(/v1/tasks/%d,task.ID));writeJSON(w,201,task)}func(a API)get(w http.ResponseWriter,r*http.Request){id,err:strconv.Atoi(r.PathValue(id));iferr!nil{writeJSON(w,400,map[string]string{code:invalid_id});return}task,err:a.service.repo.Get(r.Context(),id);iferrors.Is(err,ErrNotFound){writeJSON(w,404,map[string]string{code:not_found});return}writeJSON(w,200,task)}funcwriteJSON(w http.ResponseWriter,statusint,value any){w.Header().Set(Content-Type,application/json);w.WriteHeader(status);json.NewEncoder(w).Encode(value)}funcmain(){repo:MemoryRepository{items:make(map[int]Task)};api:API{service:Service{repo:repo}};handler:api.routes()created:httptest.NewRecorder();handler.ServeHTTP(created,httptest.NewRequest(POST,/v1/tasks,strings.NewReader({title:ship API})))fmt.Printf(create%d location%s body%s,created.Code,created.Header().Get(Location),created.Body.String())read:httptest.NewRecorder();handler.ServeHTTP(read,httptest.NewRequest(GET,/v1/tasks/1,nil))fmt.Printf(get%d body%s,read.Code,read.Body.String())}运行输出create201 location/v1/tasks/1 body{id:1,title:ship API,status:pending} get200 body{id:1,title:ship API,status:pending}二、补齐生产入口的生命周期业务纵向切片通过后再接入真实 Server。配置在 main 解析数据库 Ping 只在启动与 readiness 的受控检查中执行logger 和指标通过构造函数注入。启动 HTTP Server 的 goroutine 把错误送回 channel主 goroutine同时等待 OS 信号与服务异常不能只等待信号而忽略监听失败。关停次序是停止接流、关闭 HTTP、停止后台消费者、关闭数据库与日志缓冲。每一步共享总预算但最好保留阶段性上限。若 Shutdown 超时记录仍在处理的请求后调用 Close消息消费者需停止领取新消息再决定完成或重新入队已领取任务。下列程序给出可运行的生产入口骨架。它使用随机本地端口并自行发出 SIGTERM因此不会常驻换成固定地址并删除自发信号部分即可部署。packagemainimport(contextfmtnetnet/httposos/signalsyscalltime)funcmain(){mux:http.NewServeMux()mux.HandleFunc(GET /healthz,func(w http.ResponseWriter,r*http.Request){w.Write([]byte(ok))})server:http.Server{Handler:mux,ReadHeaderTimeout:2*time.Second,IdleTimeout:30*time.Second}listener,err:net.Listen(tcp,127.0.0.1:0);iferr!nil{panic(err)}serverErrors:make(chanerror,1)gofunc(){serverErrors-server.Serve(listener)}()ctx,stop:signal.NotifyContext(context.Background(),os.Interrupt,syscall.SIGTERM)deferstop()process,_:os.FindProcess(os.Getpid())gofunc(){time.Sleep(10*time.Millisecond);process.Signal(syscall.SIGTERM)}()select{case-ctx.Done():fmt.Println(eventshutdown_requested)caseerr:-serverErrors:panic(err)}shutdown,cancel:context.WithTimeout(context.Background(),time.Second)defercancel()iferr:server.Shutdown(shutdown);err!nil{panic(err)}err-serverErrors fmt.Printf(eventserver_stopped expected%t\n,errhttp.ErrServerClosed)}运行输出eventshutdown_requested eventserver_stopped expectedtrue三、用验收清单把示例推进到生产数据层使用版本化迁移创建 tasks 表字段至少包含主键、标题、状态、创建与更新时间并为列表游标建立(created_at,id)索引。repository 所有方法接收 context查询使用占位符Rows 必须关闭。创建幂等需求则增加 idempotency_key 唯一索引而不是依赖单机内存。安全层限制请求体、超时和并发认证验证 issuer/audience/expiry授权在资源层判断日志字段白名单secret 不入日志。部署层使用非 root 多阶段镜像只读根文件系统readiness/liveness 分离。数据库迁移与滚动升级兼容镜像按 digest 发布。质量门禁依次执行go fmt、go vet、go test -race、集成测试、govulncheck和构建。对创建成功、非法 JSON、无效标题、不存在任务与关停中的请求写契约测试。压测观察 p95/p99、数据库等待、错误率和内存而不是只报最高 QPS。上线后用 SLI 驱动运维成功请求比例、延迟分布、队列积压与依赖错误。告警附带 runbook发布标记写入指标与日志故障时能从 request ID 关联 trace再定位数据库查询。容量不足先找瓶颈再决定扩实例、加连接还是优化查询。这个系列的最终成果不是某个框架模板而是一套可迁移方法入口校验、业务边界、资源预算、错误链、可观测性、自动验证和可回滚交付。以本篇纵向切片为起点先替换数据库再加入认证和列表接口每次只扩展一个可测试契约就能持续演进为真实服务。参考来源Go 官方文档Organizing a Go moduleGo 官方文档net/httpOpenTelemetry Go 文档Google SREService Level Objectives 觉得有用就点个赞 收藏方便回头查阅有疑问直接在评论区留言我看到都会回。 本文属于《Go 后端开发实战》系列持续更新关注不迷路。 文章里的代码都能直接跑。想要可直接 clone 的完整工程 配套部署脚本 / 踩坑清单评论一声或发邮件到cj2664qq.com我免费发你。如果你正好在做类似系统、或有工程化难题想找人做也欢迎邮件聊一句——我按实际情况评估能落地的就接单或出方案。评论和邮件都能直接找到我不用跳别的平台。
返回列表