{"time":"2026-09-08T14:20:31.120+08:00","level":"error","service":"api","env":"production","operation":"post.publish","request_id":"5c6a9d7f8b1e4c20","trace_id":"4bf92f3577b34da6a3ce929d0e0e4736","user_id":1001,"post_id":9001,"dependency":"postgres","duration_ms":82,"error_type":"unique_violation","error":"insert post: duplicate key value violates unique constraint","msg":"publish post failed"}
log.Error().Err(err).Str("service","api").Str("operation","post.publish").Str("request_id",requestIDFrom(ctx)).Int64("user_id",userID).Int64("post_id",postID).Str("stage","database.insert").Str("dependency","postgres").Int64("duration_ms",time.Since(start).Milliseconds()).Str("error_type","unique_violation").Msg("publish post failed")
varErrPostNotFound=errors.New("post not found")typePostRepositorystruct{db*sql.DB}func(r*PostRepository)Find(ctxcontext.Context,postIDint64)(Post,error){varpostPosterr:=r.db.QueryRowContext(ctx,`SELECT id, user_id, content FROM posts WHERE id = $1`,postID,).Scan(&post.ID,&post.UserID,&post.Content)iferrors.Is(err,sql.ErrNoRows){returnPost{},fmt.Errorf("find post %d: %w",postID,ErrPostNotFound)}iferr!=nil{returnPost{},fmt.Errorf("query post %d: %w",postID,err)}returnpost,nil}
func(s*PostService)Publish(ctxcontext.Context,userID,postIDint64)error{post,err:=s.posts.Find(ctx,postID)iferr!=nil{returnfmt.Errorf("load post for publish: %w",err)}ifpost.UserID!=userID{returnErrForbidden}iferr:=s.posts.MarkPublished(ctx,postID);err!=nil{returnfmt.Errorf("mark post published: %w",err)}returnnil}
func(h*PostHandler)Publish(whttp.ResponseWriter,r*http.Request){start:=time.Now()userID:=currentUserID(r.Context())postID:=parsePostID(r)iferr:=h.service.Publish(r.Context(),userID,postID);err!=nil{errorType:="internal"iferrors.Is(err,ErrPostNotFound){errorType="post_not_found"}elseiferrors.Is(err,ErrForbidden){errorType="forbidden"}log.Error().Err(err).Str("service","api").Str("operation","post.publish").Str("request_id",requestIDFrom(r.Context())).Int64("user_id",userID).Int64("post_id",postID).Int64("duration_ms",time.Since(start).Milliseconds()).Str("error_type",errorType).Bool("retryable",errorType=="internal").Msg("publish post failed")switch{caseerrors.Is(err,ErrPostNotFound):render.Error(w,http.StatusNotFound,"帖子不存在")caseerrors.Is(err,ErrForbidden):render.Error(w,http.StatusForbidden,"没有操作权限")default:render.Error(w,http.StatusInternalServerError,"服务暂时不可用")}return}render.Success(w,http.StatusNoContent,"发布成功",nil)}
Repository 与 Service 均未冗余打印,错误由 %w 级联形成调用栈:
mark post published: query post 9001: connection refused
typerequestIDKeystruct{}funcwithRequestID(nexthttp.Handler)http.Handler{returnhttp.HandlerFunc(func(whttp.ResponseWriter,r*http.Request){varraw[16]byteif_,err:=rand.Read(raw[:]);err!=nil{http.Error(w,"request id unavailable",http.StatusInternalServerError)return}requestID:=hex.EncodeToString(raw[:])ctx:=context.WithValue(r.Context(),requestIDKey{},requestID)w.Header().Set("X-Request-ID",requestID)next.ServeHTTP(w,r.WithContext(ctx))})}funcrequestIDFrom(ctxcontext.Context)string{requestID,_:=ctx.Value(requestIDKey{}).(string)returnrequestID}