You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

GoLang Web服务:如何翻译包内声明的错误(GIN框架场景下)

Great question! Let's break this down into two parts: the right way to declare errors in your controller package, and a practical implementation to auto-generate the translation JSON (since runtime reflection for package-level variables in Go has limitations).

Best Practices for Declaring Errors in Your Controller Package

Option 1: Simple Exported Global Variables (Your Current Approach)

Your existing setup works perfectly for basic business errors. The key rules to follow are:

  • Export variables: Ensure error names start with a capital letter so they're accessible outside the package (and detectable by tools).
  • Consistent naming: Use a Err prefix for all business errors to make filtering and identification straightforward.

Example:

package controller

import "errors"

var (
    ErrTooManyAttempts = errors.New("too many attempts")
    ErrNoPermission    = errors.New("no permission")
    ErrNotAvailable    = errors.New("not available")
)

Option 2: Custom Error Type (For Enhanced Context)

If you need to carry extra metadata (like error codes, explicit translation keys, or debug context), a custom error type is a better choice. This eliminates the need for a separate map[error]string later, as you can embed the translation key directly in the error itself.

Example:

package controller

type AppError struct {
    TranslationKey string // Explicit key for translation lookups
    DebugMsg       string // For internal logging/debugging
}

// Implement the standard error interface
func (e *AppError) Error() string {
    return e.DebugMsg
}

var (
    ErrTooManyAttempts = &AppError{
        TranslationKey: "controller-ErrTooManyAttempts",
        DebugMsg:       "too many attempts",
    }
    ErrNoPermission = &AppError{
        TranslationKey: "controller-ErrNoPermission",
        DebugMsg:       "no permission",
    }
    ErrNotAvailable = &AppError{
        TranslationKey: "controller-ErrNotAvailable",
        DebugMsg:       "not available",
    }
)
Auto-Generating Translation Files

Runtime reflection can't easily traverse package-level variables in Go, so the most reliable approach is to build a small CLI tool that parses your controller package's source code to extract errors. This tool can generate (or update) your JSON translation file automatically.

Step 1: Build the Code-Generation Tool

Create a file gen_err_translations.go in your project root:

package main

import (
	"encoding/json"
	"fmt"
	"go/ast"
	"go/parser"
	"go/token"
	"os"
	"strings"
)

func main() {
	// Configure paths (adjust these to match your project structure)
	controllerPkgDir := "./controller"
	outputFile := "./translations.json"

	// Parse the controller package source code
	fset := token.NewFileSet()
	pkgs, err := parser.ParseDir(fset, controllerPkgDir, nil, parser.AllErrors)
	if err != nil {
		fmt.Printf("Failed to parse controller package: %v\n", err)
		os.Exit(1)
	}

	controllerPkg, ok := pkgs["controller"]
	if !ok {
		fmt.Println("Could not find 'controller' package")
		os.Exit(1)
	}

	// Collect valid error variables and their default translations
	errorEntries := make(map[string]string)

	// Traverse all files in the package
	for _, file := range controllerPkg.Files {
		for _, decl := range file.Decls {
			// Filter variable declarations
			varDecl, isVarDecl := decl.(*ast.GenDecl)
			if !isVarDecl || varDecl.Tok != token.VAR {
				continue
			}

			for _, spec := range varDecl.Specs {
				valueSpec, isValueSpec := spec.(*ast.ValueSpec)
				if !isValueSpec {
					continue
				}

				for _, name := range valueSpec.Names {
					// Only process exported variables starting with "Err"
					if !ast.IsExported(name.Name) || !strings.HasPrefix(name.Name, "Err") {
						continue
					}

					// Verify it's an error type or initialized with errors.New()
					isError := false
					// Check explicit error type
					if valueSpec.Type != nil {
						ident, isIdent := valueSpec.Type.(*ast.Ident)
						isError = isIdent && ident.Name == "error"
					}
					// Check if assigned via errors.New()
					if !isError && len(valueSpec.Values) > 0 {
						callExpr, isCall := valueSpec.Values[0].(*ast.CallExpr)
						if isCall {
							selExpr, isSel := callExpr.Fun.(*ast.SelectorExpr)
							isError = isSel && selExpr.Sel.Name == "New" && selExpr.X.(*ast.Ident).Name == "errors"
						}
					}

					if isError {
						// Generate translation key
						key := fmt.Sprintf("%s-%s", "controller", name.Name)
						// Set user-friendly default messages
						switch name.Name {
						case "ErrTooManyAttempts":
							errorEntries[key] = "Too many attempts. Please try again later."
						case "ErrNoPermission":
							errorEntries[key] = "Permission to perform this action is denied."
						case "ErrNotAvailable":
							errorEntries[key] = "Service not available. Please try again later."
						default:
							// Fallback for new errors
							defaultMsg := strings.ReplaceAll(strings.TrimPrefix(name.Name, "Err"), "", " ")
							errorEntries[key] = strings.Title(strings.ToLower(defaultMsg[:1])) + defaultMsg[1:] + "."
						}
					}
				}
			}
		}
	}

	// Merge with existing translations (preserve existing customizations)
	if _, err := os.Stat(outputFile); err == nil {
		existingBytes, err := os.ReadFile(outputFile)
		if err != nil {
			fmt.Printf("Failed to read existing translations: %v\n", err)
			os.Exit(1)
		}
		existingEntries := make(map[string]string)
		if err := json.Unmarshal(existingBytes, &existingEntries); err != nil {
			fmt.Printf("Failed to parse existing translations: %v\n", err)
			os.Exit(1)
		}
		// Keep existing translations, add new ones
		for k, v := range errorEntries {
			if _, exists := existingEntries[k]; !exists {
				existingEntries[k] = v
			}
		}
		errorEntries = existingEntries
	}

	// Generate formatted JSON output
	jsonBytes, err := json.MarshalIndent(errorEntries, "", "    ")
	if err != nil {
		fmt.Printf("Failed to marshal JSON: %v\n", err)
		os.Exit(1)
	}

	// Write to the translation file
	if err := os.WriteFile(outputFile, jsonBytes, 0644); err != nil {
		fmt.Printf("Failed to write translation file: %v\n", err)
		os.Exit(1)
	}

	fmt.Printf("Successfully updated translation file with %d entries\n", len(errorEntries))
}

Step 2: Run the Tool

Execute the tool with:

go run gen_err_translations.go

This will generate (or update) translations.json with exactly the format you requested:

{
    "controller-ErrTooManyAttempts": "Too many attempts. Please try again later.",
    "controller-ErrNoPermission": "Permission to perform this action is denied.",
    "controller-ErrNotAvailable": "Service not available. Please try again later."
}
Integrating Translations in Your Gin Server

Once you have the translation file, use a middleware to handle errors and return translated messages to clients. Here's how to do it with the custom AppError type:

package main

import (
	"encoding/json"
	"fmt"
	"net/http"
	"os"

	"github.com/gin-gonic/gin"
	"your-project-path/controller"
)

var translations map[string]string

func init() {
	// Load translations from file
	transBytes, err := os.ReadFile("./translations.json")
	if err != nil {
		panic(fmt.Sprintf("Failed to load translations: %v", err))
	}
	if err := json.Unmarshal(transBytes, &translations); err != nil {
		panic(fmt.Sprintf("Failed to parse translations: %v", err))
	}
}

// ErrorHandler middleware to translate errors for clients
func ErrorHandler() gin.HandlerFunc {
	return func(c *gin.Context) {
		c.Next()

		// Skip if no errors exist
		if len(c.Errors) == 0 {
			return
		}

		latestErr := c.Errors.Last().Err
		responseMsg := "An unexpected error occurred."

		// Handle custom AppError with translation key
		if appErr, ok := latestErr.(*controller.AppError); ok {
			if msg, exists := translations[appErr.TranslationKey]; exists {
				responseMsg = msg
			}
		}

		// Return the translated error response
		c.JSON(http.StatusBadRequest, gin.H{"error": responseMsg})
	}
}

func main() {
	r := gin.Default()
	r.Use(ErrorHandler())

	// Example route using controller errors
	r.GET("/login", func(c *gin.Context) {
		// Simulate too many login attempts
		c.AbortWithError(http.StatusBadRequest, controller.ErrTooManyAttempts)
	})

	r.Run(":8080")
}

This setup ensures your debug-friendly errors are automatically translated into user-friendly messages for clients, without the hassle of manual error-to-key mapping.

内容的提问来源于stack exchange,提问作者Bob

火山引擎 最新活动