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).
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
Errprefix 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", } )
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." }
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




