// $GOROOT/src/builtin/builtin.go // 堆代码 duidaima.com // The error built-in interface type is the conventional interface for // representing an error condition, with the nil value representing no error. type error interface { Error() string }显式返回值
package main import ( "fmt" "os" ) func readFileContent(filename string) (string, error) { data, err := os.ReadFile(filename) // ReadFile returns ([]byte, error) if err != nil { // If an error occurs (e.g., file not found), return it return "", fmt.Errorf("failed to read file %s: %w", filename, err) // Wrap the original error } return string(data), nil // Success, return data and nil error } func main() { content, err := readFileContent("my_file.txt") if err != nil { // The iconic check fmt.Fprintf(os.Stderr, "Error reading file: %v\n", err) // Here you would typically handle the error (log, return, etc.) return } fmt.Println("File content:", content) // Slightly shorter form for functions returning only error (like Close) // Use dummy file creation/opening for example that runs f, createErr := os.Create("temp_file.txt") if createErr != nil { fmt.Fprintf(os.Stderr, "Error creating file: %v\n", createErr) return } if f != nil { // Ensure file is closed even if writes fail later (using defer is better practice) defer f.Close() defer os.Remove("temp_file.txt") // Clean up the dummy file // Example usage... _, _ = f.WriteString("hello") // Now explicitly check close error if needed at the end of func, // though defer handles the call itself. // For demonstration of the if err := ... style on Close: // (Note: defer already schedules the close, this is just for syntax demo) // closerFunc := func() error { return f.Close() } // Wrap Close if needed // if err := f.Close(); err != nil { // Potential re-close if not careful with defer // fmt.Fprintf(os.Stderr, "Error closing file: %v\n", err) // } // A more practical place for this pattern might be a non-deferred close. } }示例中,对每一处返回错误的地方都做了显式检查,这保证了错误不会被轻易忽略,控制流清晰可见,但也导致了代码冗长。上面代码因my_file.txt文件不存在,会输出“Error reading file: failed to read file my_file.txt: open my_file.txt: no such file or directory”并退出。
package main import ( "errors" "fmt" "os" "time" ) // Custom error type type OperationError struct { Op string Err error // Underlying error Timestamp time.Time } // Implement the error interface func (e *OperationError) Error() string { return fmt.Sprintf("[%s] operation %s failed: %v", e.Timestamp.Format(time.RFC3339), e.Op, e.Err) } // Function that might return our custom error func performCriticalOperation() error { // Simulate a failure err := errors.New("connection refused") return &OperationError{ Op: "connect_database", Err: err, Timestamp: time.Now(), } } // (main function using this will be shown in the next point)错误检查
// (Continuing from previous snippet within the same package) func main() { err := performCriticalOperation() if err != nil { fmt.Fprintf(os.Stderr, "Operation failed: %v\n", err) // Prints the formatted custom error // Example: Check if the underlying error is a specific known error // Note: Standard errors package doesn't export connection refused directly, // this is conceptual. Real check might involve string matching or syscall types. // if errors.Is(err, someSpecificNetworkError) { // fmt.Println("It was specifically a network error") // } // Check if the error is of our custom type and extract it var opErr *OperationError if errors.As(err, &opErr) { fmt.Fprintf(os.Stderr, " Operation details: Op=%s, Time=%s, UnderlyingErr=%v\n", opErr.Op, opErr.Timestamp.Format(time.Kitchen), opErr.Err) // Can now use opErr.Op, opErr.Timestamp etc. for specific handling } } }该博主认为,Go的方式虽然有点“乏味”和冗长,但非常直接 (straightforward),且自定义错误携带丰富上下文的能力是一大优势,使得错误本身更具“可编程性”。
const std = @import("std"); // Define possible errors for our function const MyError = error{ InvalidInput, ConnectionFailed, SomethingElse, }; // Function signature indicating it can return MyError or u32 fn doSomething(input: u32) MyError!u32 { if (input == 0) { return MyError.InvalidInput; // Return a specific error } if (input > 100) { return MyError.ConnectionFailed; // Return another error } // Simulate success return input * 2; // Return the successful result (u32) } // Example usage needs a main function // pub fn main() !void { // Example main, !void indicates main can return error // const result = try doSomething(50); // std.debug.print("Result: {}\n", .{result}); // }强制处理
const std = @import("std"); const MyError = error{InvalidInput, ConnectionFailed}; // Simplified error set // Function definition (same as above) fn doSomething(input: u32) MyError!u32 { if (input == 0) return MyError.InvalidInput; if (input > 100) return MyError.ConnectionFailed; return input * 2; } // 堆代码 duidaima.com // This function also returns MyError or u32 fn processData(input: u32) MyError!u32 { // If doSomething returns an error, 'try' immediately propagates // that error from processData. Otherwise, result holds the u32 value. const result = try doSomething(input); // ... further processing on result ... std.debug.print("Intermediate result in processData: {}\n", .{result}); return result + 1; } pub fn main() !void { // Main now can return errors (due to try) const finalResult = try processData(50); // Propagate error from processData std.debug.print("Final result: {}\n", .{finalResult}); // Example of triggering an error propagation // Uncommenting the line below will cause main to return InvalidInput // _ = try processData(0); }注:Zig中的try可不同于Java等支持try-catch等错误处理机制中的try。Zig 的 try 用于传播错误,而 Java 的 try-catch 用于捕获和处理异常。
const std = @import("std"); const MyError = error{InvalidInput, ConnectionFailed}; fn doSomething(input: u32) MyError!u32 { /* ... */ if (input == 0) return MyError.InvalidInput; return input * 2; } pub fn main() void { // Main does not return errors itself const result = doSomething(0) catch |err| { // Error occurred, execution enters the catch block std.debug.print("Caught error: {s}\n", .{@errorName(err)}); // Prints "Caught error: InvalidInput" // Handle the error, maybe exit or log differently // For this example, we just print and return from main return; // Exit main gracefully }; // This line only executes if doSomething succeeded // If input was non-zero, this would print. std.debug.print("Success! Result: {}\n", .{result}); }与回退值结合 (catch fallbackValue),在出错时提供一个默认的成功值
const std = @import("std"); const MyError = error{InvalidInput, ConnectionFailed}; fn doSomething(input: u32) MyError!u32 { /* ... */ if (input == 0) return MyError.InvalidInput; return input * 2; } pub fn main() void { // If doSomething fails (input is 0), result will be assigned 999 const result = doSomething(0) catch 999; std.debug.print("Result (with fallback): {}\n", .{result}); // Prints 999 const success_result = doSomething(10) catch 999; std.debug.print("Result (with fallback, success case): {}\n", .{success_result}); // Prints 20 }与命名块结合
const std = @import("std"); const MyError = error{ FileNotFound, InvalidData, }; fn readDataFromFile(filename: []const u8) MyError![]const u8 { // 模拟读取文件,如果文件名是 "error.txt" 则返回错误 if (std.mem.eql(u8, filename, "error.txt")) { return MyError.FileNotFound; } // 模拟读取成功 const data: []const u8 = "Some valid data"; return data; } fn handleReadFile(filename: []const u8) []const u8 { return readDataFromFile(filename) catch |err| { std.debug.print("Error reading file: {any}\n", .{err}); std.debug.print("Using default data\n", .{}); return "Default data"; }; } pub fn main() !void { const filename = "data.txt"; const errorFilename = "error.txt"; const data = handleReadFile(filename); std.debug.print("Data: {s}\n", .{data}); const errorData = handleReadFile(errorFilename); std.debug.print("Error Data: {s}\n", .{errorData}); }注:对于Gopher而言,是不是开始感觉有些复杂了:)。
const std = @import("std"); const MyError = error{InvalidInput, ConnectionFailed, SomethingElse}; fn doSomething(input: u32) MyError!u32 { if (input == 0) return MyError.InvalidInput; if (input > 100) return MyError.ConnectionFailed; if (input == 55) return MyError.SomethingElse; // Add another error case return input * 2; } pub fn main() void { // Test Case 1: Success if (doSomething(10)) |successValue| { std.debug.print("Success via if/else (input 10): {}\n", .{successValue}); // Prints 20 } else |err| { std.debug.print("Error (input 10): {s}\n", .{@errorName(err)}); } // Test Case 2: ConnectionFailed Error if (doSomething(101)) |successValue| { std.debug.print("Success via if/else (input 101): {}\n", .{successValue}); } else |err| { std.debug.print("Error via if/else (input 101): ", .{}); switch (err) { MyError.InvalidInput => std.debug.print("Invalid Input\n", .{}), MyError.ConnectionFailed => std.debug.print("Connection Failed\n", .{}), // This branch runs else => std.debug.print("Unknown error\n", .{}), } } // Test Case 3: SomethingElse Error (falls into else) if (doSomething(55)) |successValue| { std.debug.print("Success via if/else (input 55): {}\n", .{successValue}); } else |err| { std.debug.print("Error via if/else (input 55): ", .{}); switch (err) { MyError.InvalidInput => std.debug.print("Invalid Input\n", .{}), MyError.ConnectionFailed => std.debug.print("Connection Failed\n", .{}), else => std.debug.print("Unknown error ({s})\n", .{@errorName(err)}), // This branch runs } } }catch unreachable
const std = @import("std"); // Assume this function logically should never fail based on guarantees elsewhere fn doSomethingThatShouldNeverFail() !u32 { // For demo, make it fail sometimes // if (std.time.timestamp() % 2 == 0) return error.UnexpectedFailure; return 42; } pub fn main() void { // If doSomethingThatShouldNeverFail returns an error, this will panic. // Useful when an error indicates a programming bug. const result = doSomethingThatShouldNeverFail() catch unreachable; std.debug.print("Result (unreachable case): {}\n", .{result}); // To see it panic, you'd need doSomethingThatShouldNeverFail to actually return an error. }该博主认为,Zig 的错误处理方式功能更丰富、更强大、也**更简洁 (concise)**。try 关键字尤其强大,极大地减少了错误传播的样板代码。
特性 | Go | Zig |
---|---|---|
核心哲学 | 错误即值 | 错误即值 |
实现机制 | error 接口, (T, error) 返回值 | 错误联合类型 (!T), 特殊错误枚举 |
处理方式 | 显式 if err != nil 检查 | try, catch (多种形式), if/else catch |
强制性 | 靠约定和 Linter | 编译器强制 |
简洁性 | 某些时候略冗长 | 简洁 (try) |
上下文 | 通过自定义类型携带丰富上下文 | 错误值本身通常不带上下文 (目前) |
编程性 | 高 (errors.Is/As, 自定义类型) | 相对较低 (主要靠错误类型区分) |
易理解性 | 非常直接 | 语法糖需学习,但逻辑算是清晰 |
Zig 则选择了更精巧、更简洁且由编译器强制保证的路径。 它通过强大的语法糖显著减少了样板代码,提升了编写体验,但在错误本身携带上下文信息方面目前有所欠缺。