Golang中如何为API响应定义通用的成功/错误结果结构

Golang中如何为API响应定义通用的成功/错误结果结构 我来自 TypeScript 背景,想要为我的 API 响应定义对象。在 TS 中我使用了:

type Result<T> = { isSuccessful: true; value: T; } | { isSuccessful: false; code: string; message: string; };

在 Go 中,我最初是这样开始的:

type ResponseResultError struct {
	Code    string `json:"code"`
	Message string `json:"message"`
}

type ResponseResult[T any] struct {
	Ok    bool                `json:"ok"`
	Value T                   `json:"value"`
	Error ResponseResultError `json:"error"`
}

但由于 Go 支持元组等特性,也许有更好的解决方案来解决这个问题?你会如何定义一个“东西”,让使用者可以创建包含以下内容的成功响应:

{ isSuccessful: true; value: T; }

或者包含以下内容的错误响应:

{ isSuccessful: false; code: string; message: string; }


或者我应该采用以下方式?

type InternalErrorResponseResult struct {
	IsSuccessful bool   `json:"isSuccessful"`
	Code         string `json:"code"`
	Message      string `json:"message"`
}

func NewInternalErrorResponseResult(code string, message string) InternalErrorResponseResult {
	return InternalErrorResponseResult{
		Code:    code,
		Message: message,
	}
}

type OperationResponseResult[T any] struct {
	IsSuccessful bool `json:"isSuccessful"`
	Value        T    `json:"value"`
}

func NewOperationResponseResult[T any](isSuccessful bool, value T) OperationResponseResult[T] {
	return OperationResponseResult[T]{
		IsSuccessful: isSuccessful,
		Value:        value,
	}
}

更多关于Golang中如何为API响应定义通用的成功/错误结果结构的实战教程也可以访问 https://www.itying.com/category-94-b0.html

8 回复

不需要,JSON序列化本身包含大量反射操作。

更多关于Golang中如何为API响应定义通用的成功/错误结果结构的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


为你的API响应定义一个统一的结构。这个结构应该包含用于指示响应状态、消息和数据(如果适用)的字段。

在响应中包含一个状态字段,以指示API请求的结果。例如,你可以使用诸如“success”或“error”这样的值来表示整体状态。

你好 @jtuchel

你计划在哪里使用这个 Response 结构体?你需要提供更多关于此的细节,它可能是你应用程序特有的东西。

Go 语言的惯用方式是 result, err := methodName(parameters)。对于一个 HTTP 服务器,你将状态码写入头部,将结果写入响应体。因此,使用一个结构体来存储结果和任何可能的错误,并不是很常见的做法。

当然,那样做是可行的。

另一种方法是:你总是发送响应头(OK 或失败状态),对于 OK 的响应头,你将值/结果以 JSON 格式放在响应体中发送。对于非 OK 的响应头(任何类型的错误头),你在响应体中发送错误信息(或描述具体情况的特定错误信息——但不要提供内部细节,如错误堆栈跟踪或任何内部数据)。

许多公共REST API都是这样工作的。成功时仅返回状态码200和“值”作为结果。仅在出现错误时返回错误状态码和错误信息。这为API的使用者和生产者都创造了可读性强的代码。

// Go 生产者端:
result, err := myFunction(...)
if err != nil {
  http.Error(w, toJSON(err), http.InternalServerError)
  return
}

writeJSON(w, result)
return

// 辅助函数:
func toJSON(err error) string {
  cErr := codedError{code: "UnknownError", msg: err.Message()}
  errors.As(err, &cErr) // 从 codedError 获取详细信息
  b, jsonErr := json.Marshal(cErr)
  if jsonErr != nil {
    log.Error("this should not happen...")
    return err.Message()
  }
  return string(b)
}
// JavaScript 消费者端
const resp = await fetch('/api/...')
if (!resp.ok) {
  throw new CodedError(await resp.json())
}

return resp.json()

抱歉,你说得对。我想将这个结构体用于HTTP响应体。这个结构体不会包含HTTP状态码。因此,每当我需要向API使用者返回“一个值”时,我都希望使用一个通用的响应结构。根据回答,我尝试了以下方案:

type OperationResponse[T any] struct {
	IsSuccessful bool   `json:"isSuccessful"`
	Value        T      `json:"value"`
	ErrorCode    string `json:"errorCode,omitempty"`
	Message      string `json:"message,omitempty"`
}

其中 ErrorCode 表示类似 LicenseExpired 这样的含义。然后我创建了一些工具函数:

func GetInternalErrorResponse() OperationResponse[*string] {
	return OperationResponse[*string]{
		IsSuccessful: false,
		Value:        nil,
		ErrorCode:    "??? TODO ???",
		Message:      "The server encountered an unexpected condition that prevented it from fulfilling the request.",
	}
}

func GetOperationFailureResponse(errorCode string, message string) OperationResponse[*string] {
	return OperationResponse[*string]{
		IsSuccessful: false,
		Value:        nil,
		ErrorCode:    errorCode,
		Message:      message,
	}
}

func GetOperationSuccessResponse[T any](value T) OperationResponse[T] {
	return OperationResponse[T]{
		IsSuccessful: true,
		Value:        value,
		ErrorCode:    "",
		Message:      "",
	}
}

你觉得怎么样?

你的代码在我看来没问题。问题在于你只想要一个结构体类型吗?如果是这样,你应该看看 omitEmpty

“omitempty” 选项指定,如果字段具有空值(定义为 false、0、nil 指针、nil 接口值以及任何空数组、切片、映射或字符串),则应从编码中省略该字段。

你可以像下面这样定义你的结构体:

type Result[T any] struct {
	IsSuccessful bool   `json:"isSuccessful"`
	Value        *T     `json:"value,omitempty"`
	Code         string `json:"code,omitempty"`
	Message      string `json:"message,omitempty"`
}

… 这样当它们是零值或 nil 时,就不会发送这些键。由于使用 err != nil 来判断是否出错是惯用做法,如果你愿意,也可以将构造函数代码简化成这样:

func NewResult[T any](value *T, err error) Result[T] {
	if err != nil {
		return Result[T]{
			IsSuccessful: false,
			Code:         http.StatusText(http.StatusInternalServerError),
			Message:      err.Error(),
		}
	}
	return Result[T]{
		IsSuccessful: true,
		Value:        value,
	}
}

因为同样地,惯用做法是 resp, err := doSomething(),在我看来,直接传递 resp, err 到这个 NewResult 函数中会很流畅。结合一些简单的测试代码:

type someItem struct {
	ID    int
	Value string
}

func main() {
	value := NewResult(&someItem{23, "Hello"}, nil)
	json.NewEncoder(os.Stdout).Encode(value)
	errorValue := NewResult[someItem](nil, errors.New("Problem contacting database."))
	json.NewEncoder(os.Stdout).Encode(errorValue)
}

… 将产生:

{"isSuccessful":true,"value":{"ID":23,"Value":"Hello"}}
{"isSuccessful":false,"code":"Internal Server Error","message":"Problem contacting database."}

总之,只是一些想法供你参考!你也可以将其推送到一个像 SendJSON[T any](value *T, err error, w http.ResponseWriter) 这样的函数中。它可以检查 err,如果为 nil 则执行一种操作,如果不为 nil 则执行另一种操作。这是上面代码的 Playground 链接:

Go Playground - The Go Programming Language

在Go中实现类似TypeScript的联合类型,可以使用接口和自定义类型。以下是两种常见方案:

方案1:使用接口和自定义错误类型

package main

import (
	"encoding/json"
	"fmt"
)

type Result[T any] interface{}

type Success[T any] struct {
	IsSuccessful bool `json:"isSuccessful"`
	Value        T    `json:"value"`
}

type Failure struct {
	IsSuccessful bool   `json:"isSuccessful"`
	Code         string `json:"code"`
	Message      string `json:"message"`
}

func NewSuccess[T any](value T) Success[T] {
	return Success[T]{
		IsSuccessful: true,
		Value:        value,
	}
}

func NewFailure(code, message string) Failure {
	return Failure{
		IsSuccessful: false,
		Code:         code,
		Message:      message,
	}
}

// 使用示例
func handleResult[T any](result Result[T]) {
	switch r := result.(type) {
	case Success[T]:
		fmt.Printf("Success: %v\n", r.Value)
	case Failure:
		fmt.Printf("Error: %s - %s\n", r.Code, r.Message)
	}
}

func main() {
	// 成功响应
	successResult := NewSuccess(map[string]string{"name": "John"})
	successJSON, _ := json.Marshal(successResult)
	fmt.Println(string(successJSON))
	// 输出: {"isSuccessful":true,"value":{"name":"John"}}

	// 错误响应
	errorResult := NewFailure("NOT_FOUND", "Resource not found")
	errorJSON, _ := json.Marshal(errorResult)
	fmt.Println(string(errorJSON))
	// 输出: {"isSuccessful":false,"code":"NOT_FOUND","message":"Resource not found"}

	// 类型判断
	handleResult(successResult)
	handleResult(errorResult)
}

方案2:使用带标签的联合体(更接近你的需求)

package main

import (
	"encoding/json"
	"fmt"
)

type Result[T any] struct {
	IsSuccessful bool   `json:"isSuccessful"`
	Value        T      `json:"value,omitempty"`
	Code         string `json:"code,omitempty"`
	Message      string `json:"message,omitempty"`
}

func NewSuccessResult[T any](value T) Result[T] {
	return Result[T]{
		IsSuccessful: true,
		Value:        value,
	}
}

func NewErrorResult(code, message string) Result[any] {
	return Result[any]{
		IsSuccessful: false,
		Code:         code,
		Message:      message,
	}
}

// 辅助函数检查结果类型
func (r Result[T]) IsSuccess() bool {
	return r.IsSuccessful
}

func (r Result[T]) GetValue() (T, bool) {
	return r.Value, r.IsSuccessful
}

func (r Result[T]) GetError() (string, string, bool) {
	return r.Code, r.Message, !r.IsSuccessful
}

// 使用示例
type User struct {
	ID   int    `json:"id"`
	Name string `json:"name"`
}

func main() {
	// 成功响应
	user := User{ID: 1, Name: "Alice"}
	success := NewSuccessResult(user)
	successJSON, _ := json.Marshal(success)
	fmt.Println(string(successJSON))
	// 输出: {"isSuccessful":true,"value":{"id":1,"name":"Alice"}}

	// 错误响应
	err := NewErrorResult("VALIDATION_ERROR", "Invalid input")
	errJSON, _ := json.Marshal(err)
	fmt.Println(string(errJSON))
	// 输出: {"isSuccessful":false,"code":"VALIDATION_ERROR","message":"Invalid input"}

	// 使用辅助函数
	if success.IsSuccess() {
		value, _ := success.GetValue()
		fmt.Printf("User: %+v\n", value)
	}

	if !err.IsSuccess() {
		code, msg, _ := err.GetError()
		fmt.Printf("Error %s: %s\n", code, msg)
	}
}

方案3:使用指针和omitempty(更简洁的JSON输出)

package main

import (
	"encoding/json"
	"fmt"
)

type Result[T any] struct {
	IsSuccessful bool    `json:"isSuccessful"`
	Value        *T      `json:"value,omitempty"`
	Error        *Error  `json:"error,omitempty"`
}

type Error struct {
	Code    string `json:"code"`
	Message string `json:"message"`
}

func NewSuccess[T any](value T) Result[T] {
	return Result[T]{
		IsSuccessful: true,
		Value:        &value,
	}
}

func NewError(code, message string) Result[any] {
	return Result[any]{
		IsSuccessful: false,
		Error: &Error{
			Code:    code,
			Message: message,
		},
	}
}

func main() {
	// 成功响应
	success := NewSuccess("operation completed")
	successJSON, _ := json.Marshal(success)
	fmt.Println(string(successJSON))
	// 输出: {"isSuccessful":true,"value":"operation completed"}

	// 错误响应
	err := NewError("INTERNAL_ERROR", "Something went wrong")
	errJSON, _ := json.Marshal(err)
	fmt.Println(string(errJSON))
	// 输出: {"isSuccessful":false,"error":{"code":"INTERNAL_ERROR","message":"Something went wrong"}}
}

第一种方案最接近TypeScript的联合类型语义,第二种方案更实用且易于序列化,第三种方案提供最干净的JSON输出。根据你的具体需求选择合适方案。

回到顶部