跪拜 Guibai
← Back to the summary

Go's Error Handling Forces You to Account for Every Failure Path

This is the third article in the "Frontend Developer Transitioning to Go Full-Stack" series.

Previous article: Frontend Developer Transitioning to Go Full-Stack (Part 2): From Hello Go to Type Conversion, I'm Getting Used to Go's "Strictness"

In the previous article, starting from my first Hello, Go!, I learned about variables, constants, zero values, scope, and type conversion. This article will continue to explain the value, err := ... left over from last time, officially entering Go's functions and error handling.

Preface: The Two Variables Left from Last Time, Finally Explained

When learning type conversion in the previous article, I wrote code like this many times:

age, err := strconv.Atoi("30")

At the time, I just memorized its usage:

age: the converted integer
err: the error generated during the conversion process

But I always had a few questions in my mind:

Why can a function return two values at the same time?
Why use two variables on the left to receive them?
What exactly is err?
Why is err nil when successful?

So at this stage, I didn't rush to learn new data types but formally entered Go's functions.

At first glance, Go functions aren't drastically different from JavaScript or TypeScript:

func add(a, b int) int {
	return a + b
}

It's just replacing function with func and writing the parameter type after the variable name.

But after continuing to learn, I found that the truly interesting part of Go functions isn't just the syntax.

It can directly return multiple values and often returns the "execution result" and "error information" together.

In other words, Go doesn't just require the function to tell the caller:

What I successfully obtained.

It also requires it to explicitly tell the caller:

If it failed, what the reason for the failure is.

This article will record how I went from my first custom function, step by step, to error and error propagation.


Creating a Function Practice Project

This time, I created a new practice directory:

D:\LearningGO\04-functions

Since I'm using Git Bash, I executed:

cd /d/LearningGO
mkdir 04-functions
cd 04-functions
go mod init functions

Terminal output:

go: creating new go.mod: module functions

Then I created main.go. The current directory structure is as follows:

04-functions/
├── go.mod
└── main.go

Next, I officially started writing my own functions.


The First Custom Function

Start with a function that has no parameters and no return value:

package main

import "fmt"

func sayHello() {
	fmt.Println("Hello, I am learning Go functions.")
}

func main() {
	sayHello()
}

Run:

go run .

Output:

Hello, I am learning Go functions.

The function definition is:

func sayHello() {
}

Breaking it down:

func        declares the function
sayHello    function name
()          parameter list
{}          function body

If converted to JavaScript, the syntax would be roughly:

function sayHello() {
  console.log("Hello, I am learning Go functions.");
}

Go uses func to declare functions, and calls them by adding parentheses after the function name:

sayHello()

This part isn't difficult; you can basically understand it at a glance.


Passing Parameters to Functions

Next, I added two parameters to the function:

package main

import "fmt"

func introduce(name, job string) {
	fmt.Printf("Hello everyone, I am %s, currently a %s.\n", name, job)
}

func main() {
	introduce("Starry", "Frontend Developer")
}

Run result:

Hello everyone, I am Starry, currently a Frontend Developer.

Function definition:

func introduce(name, job string)

Indicates both parameters are string:

name: string
job: string

If adjacent parameters have the same type, they can be written together like above.

The complete way to write it is:

func introduce(name string, job string)

Both ways have the same effect.

Compared with TypeScript:

function introduce(name: string, job: string): void {
  console.log(`Hello everyone, I am ${name}, currently a ${job}.`);
}

The position of the type differs between the two:

TypeScript: parameterName: type
Go:         parameterName type

Go is Strict About the Number of Parameters

After a successful call, I intentionally passed the wrong parameters.

The function definition requires two strings:

func introduce(name, job string)

Passing One Less Parameter

If only one parameter is passed:

introduce("Starry")

The compiler will prompt:

not enough arguments in call to introduce
	have (string)
	want (string, string)

The prompt here is very intuitive:

have: what was actually passed
want: what the function needs

That is:

Actually passed: 1 string
Function needs: 2 strings

Passing One More Parameter

Next, I tried passing one more parameter:

introduce("Starry", "Frontend Developer", "Go")

The compiler prompts:

too many arguments in call to introduce
	have (string, string, string)
	want (string, string)

This is different from the experience with vanilla JavaScript.

In JavaScript, extra parameters usually don't directly prevent the function from executing:

function introduce(name, job) {
  console.log(name, job);
}

introduce("Starry", "Frontend Developer", "Go");

Although the third parameter is passed, the function doesn't use it internally.

Go requires:

The actual number of parameters must match the function definition; one more is not allowed, one less is not allowed.

If you really need to receive a variable number of parameters, Go also provides variadic parameters, but I'll learn that part later.


Parameter Types Must Also Match

I also changed the second parameter to an integer:

introduce("Starry", 30)

But the function requires the second parameter to be string:

func introduce(name, job string)

So the compiler continues to refuse:

cannot use 30 (untyped int constant) as string value in argument to introduce

That is:

Function needs: string
Actually passed: int

If you really want to pass a number as a string, you need to explicitly complete the conversion first:

introduce("Starry", strconv.Itoa(30))

These errors will all be discovered at the compilation stage, rather than being handled after the function actually runs.


Making a Function Return a Value

Next, I wrote a simple addition function:

package main

import "fmt"

func add(a, b int) int {
	return a + b
}

func main() {
	result := add(10, 20)

	fmt.Println("Calculation result:", result)
}

Output:

Calculation result: 30

The key part is this:

func add(a, b int) int

The last int indicates the type of the function's return value.

It can be broken down into:

add          function name
a, b         function parameters
first int    parameter type
last int     return value type

The syntax in TypeScript is:

function add(a: number, b: number): number {
  return a + b;
}

Go places the return value type after the parameter list:

func add(a, b int) int

When calling:

result := add(10, 20)

The 30 returned by the function will be saved into result.


Return as Many Values as You Have Variables to Receive

add only returns one value:

func add(a, b int) int {
	return a + b
}

But I intentionally used two variables to receive it:

result1, result2 := add(10, 20)

The compiler prompts:

assignment mismatch: 2 variables but add returns 1 value

That is:

Two variables prepared on the left
The function on the right only returns one value
The number of return values cannot match

If the original output statements are still kept when modifying the code, the compiler might simultaneously find other problems:

declared and not used: result1
declared and not used: result2
undefined: result

This also reconfirms the rules learned in the previous article:

Local variables must be used after declaration
Non-existent variables cannot be accessed
The number of return values and receiving variables must match

Go Functions Can Directly Return Multiple Values

Next, I finally entered the part I wanted to understand the most.

I wrote a function that calculates both addition and multiplication simultaneously:

package main

import "fmt"

func calculate(a, b int) (int, int) {
	sum := a + b
	product := a * b

	return sum, product
}

func main() {
	sum, product := calculate(10, 20)

	fmt.Println("Addition result:", sum)
	fmt.Println("Multiplication result:", product)
}

Run result:

Addition result: 30
Multiplication result: 200

In the function definition:

func calculate(a, b int) (int, int)

The last:

(int, int)

Indicates the function will return two integers.

When returning, write:

return sum, product

When calling, write:

sum, product := calculate(10, 20)

The two return values will be assigned to the variables on the left according to their position:

First return value sum      → sum on the left
Second return value product  → product on the left

The whole process can be understood as:

calculate(10, 20)
→ returns 30 and 200
→ sum receives 30
→ product receives 200

At this point, looking back at the code from the previous article:

age, err := strconv.Atoi(ageText)

It's no longer so mysterious.

strconv.Atoi also returns two values:

First return value: the converted integer
Second return value: the error generated during the conversion process

If You Don't Need a Return Value, You Can Use _

Suppose I only care about the multiplication result, I can write:

_, product := calculate(10, 20)

fmt.Println("Multiplication result:", product)

If I only care about the addition result, I can write:

sum, _ := calculate(10, 20)

fmt.Println("Addition result:", sum)

The _ here is the blank identifier, meaning:

There is a return value in this position, but I explicitly choose not to use it.

To temporarily ignore a variable in the previous article, I also used:

_ = name

Now it can also be used to ignore function return values.

However, some return values cannot be casually discarded just for convenience, especially errors.

For example:

age, _ := strconv.Atoi("30 years old")

Although this code can compile, it directly ignores the conversion error.

At this point, age will get the zero value of the int type:

0

But the caller can no longer determine:

Is this 0 because the user really input 0
Or is it the zero value obtained after the string conversion failed?

So _ can be used, but you cannot develop the habit of "just throwing away errors when you see them."


Returning error Myself for the First Time

After understanding multiple return values, I started writing a function that might fail myself.

The simplest example is division, because the divisor cannot be 0.

package main

import (
	"errors"
	"fmt"
)

func divide(a, b int) (int, error) {
	if b == 0 {
		return 0, errors.New("Divisor cannot be 0")
	}

	return a / b, nil
}

func main() {
	result, err := divide(10, 2)

	if err != nil {
		fmt.Println("Calculation failed:", err)
		return
	}

	fmt.Println("Calculation result:", result)
}

Normal call:

divide(10, 2)

Output:

Calculation result: 5

The function declaration is:

func divide(a, b int) (int, error)

Indicating the function will return:

First value: calculation result of type int
Second value: error of type error

On success:

return a / b, nil

Where:

a / b: normal calculation result
nil: no error

On failure:

return 0, errors.New("Divisor cannot be 0")

Here:

errors.New("Divisor cannot be 0")

Creates an error containing the error message.

The preceding 0 is just the zero value of the int type.

What truly indicates the operation failed is the second return value error.


How to Understand error and nil Initially?

error is a built-in interface type in Go used to represent errors.

At this stage, there's no need to delve into the underlying principles of interfaces immediately. You can first understand it like this:

error: used to save and describe errors
nil: currently no error

Therefore, the common way to check is:

if err != nil {
	// An error occurred
}

Conversely:

if err == nil {
	// No error
}

Many functions that might fail adopt a similar return form:

func doSomething() (ResultType, error)

On success:

return normalResult, nil

On failure:

return zeroValueOfResultType, specificError

This is a common convention, not a compiler-enforced requirement that all functions must be designed this way.


Intentionally Making Division Fail

I changed the call to:

result, err := divide(10, 0)

Run result:

Calculation failed: Divisor cannot be 0

The program entered the following branch:

if err != nil {
	fmt.Println("Calculation failed:", err)
	return
}

This can first be understood as:

err == nil    No error
err != nil    An error occurred

When an error occurs, the normal result returned by the function is usually no longer trustworthy.

Therefore, you should handle the error first, then continue using the result:

result, err := divide(10, 0)

if err != nil {
	fmt.Println("Calculation failed:", err)
	return
}

fmt.Println("Calculation result:", result)

This structure will appear repeatedly in Go code later:

result, err := executeSomeOperationThatMightFail()

if err != nil {
	handle error
	return
}

continue using result

Encapsulating More Realistic Parameter Validation with Functions

Just doing addition, subtraction, multiplication, and division felt too much like syntax exercises.

So, I brought back the string-to-age conversion from the previous article and encapsulated it into a function:

package main

import (
	"errors"
	"fmt"
	"strconv"
)

func parseAge(ageText string) (int, error) {
	age, err := strconv.Atoi(ageText)

	if err != nil {
		return 0, errors.New("Age must be an integer")
	}

	if age < 0 {
		return 0, errors.New("Age cannot be less than 0")
	}

	if age > 150 {
		return 0, errors.New("Age cannot be greater than 150")
	}

	return age, nil
}

func printAge(ageText string) {
	age, err := parseAge(ageText)

	if err != nil {
		fmt.Printf("Input %q, processing failed: %v\n", ageText, err)
		return
	}

	fmt.Printf("Input %q, age is: %d\n", ageText, age)
}

func main() {
	printAge("30")
	printAge("30 years old")
	printAge("-1")
	printAge("200")
}

Run result:

Input "30", age is: 30
Input "30 years old", processing failed: Age must be an integer
Input "-1", processing failed: Age cannot be less than 0
Input "200", processing failed: Age cannot be greater than 150

This code already has a bit of the feel of backend parameter processing:

Receive string
→ Convert to integer
→ Check if conversion succeeded
→ Validate business range
→ Successfully return age
→ Return specific error on failure

The four test values cover different situations:

"30"          Format and range are correct
"30 years old" Cannot be converted to an integer
"-1"          Can be converted, but less than the reasonable range
"200"         Can be converted, but exceeds the reasonable range

This also made me start to realize:

Correct parameter type doesn't mean the business data is necessarily valid.

"-1" and "200" can both be normally converted to integers, but they don't meet the age rules defined here.

So after receiving data, the backend usually doesn't just complete type conversion; it also needs to continue with business validation.


Type Conversion and Business Validation Are Not the Same Thing

Through the parseAge example, two concepts can be further distinguished.

Type Conversion

age, err := strconv.Atoi(ageText)

It is only responsible for determining whether the string can be converted to an integer.

For example:

"30"   → Can be converted
"-1"   → Can be converted
"200"  → Can be converted
"30 years old" → Cannot be converted

Business Validation

if age < 0 {
	return 0, errors.New("Age cannot be less than 0")
}

if age > 150 {
	return 0, errors.New("Age cannot be greater than 150")
}

It is responsible for determining whether the successfully converted data conforms to the current business rules.

Therefore, for a parameter to be finally usable normally, it must pass at least two layers of judgment:

Is the format legal?
→ Does the data conform to business rules?

When receiving backend request parameters in the future, similar problems will be encountered constantly:

Can the page number be converted to an integer?
Is the page number greater than 0?

Is the email a string?
Is the email format legal?

Is the password a string?
Does the password length meet the requirements?

Just "correct type" does not mean the data can directly enter the database.


return Ends the Current Function

There is this piece of code in printAge:

if err != nil {
	fmt.Printf("Input %q, processing failed: %v\n", ageText, err)
	return
}

The return here will only end the current invocation of printAge.

For example:

printAge("30 years old")
printAge("-1")
printAge("200")

After the first call fails and executes return, the subsequent two calls will still continue to execute.

Because the program's call relationship is:

main
→ calls printAge("30 years old")
→ this current printAge executes return
→ returns to main
→ continues to call printAge("-1")
→ this current printAge executes return
→ returns to main
→ continues to call printAge("200")

If return is executed directly in the main function, what ends is the main function, and the program will also end accordingly.

So:

return ends the currently executing function, not unconditionally ending the entire program.


Static Errors and Dynamic Errors

The error messages before were all static:

errors.New("Age cannot be less than 0")

But when actually troubleshooting problems, only seeing:

Age cannot be less than 0

might not be intuitive enough.

I'd prefer to know:

What exactly was the value passed in?

So I started using fmt.Errorf:

func parseAge(ageText string) (int, error) {
	age, err := strconv.Atoi(ageText)

	if err != nil {
		return 0, fmt.Errorf("Age %q is not a valid integer", ageText)
	}

	if age < 0 {
		return 0, fmt.Errorf("Age cannot be less than 0, current value is %d", age)
	}

	if age > 150 {
		return 0, fmt.Errorf("Age cannot be greater than 150, current value is %d", age)
	}

	return age, nil
}

Re-test:

func main() {
	printAge("30")
	printAge("30 years old")
	printAge("-1")
	printAge("200")
}

Run result:

Input "30", age is: 30
Input "30 years old", processing failed: Age "30 years old" is not a valid integer
Input "-1", processing failed: Age cannot be less than 0, current value is -1
Input "200", processing failed: Age cannot be greater than 150, current value is 200

The two ways to create errors can be understood like this for now.

errors.New

errors.New("Static error message")

Suitable for fixed errors that don't need to insert variables.

For example:

errors.New("Divisor cannot be 0")

fmt.Errorf

fmt.Errorf("Age cannot be less than 0, current value is %d", age)

Suitable for scenarios where specific data needs to be written into the error message.

For example:

fmt.Errorf("Age %q is not a valid integer", ageText)

When the input is "30 years old", the error message will become:

Age "30 years old" is not a valid integer

Compared to static prompts, this makes it easier to know exactly which input caused the problem.


Don't Discard the Underlying Error

Although the code just now generated clearer error messages, it also has a problem:

if err != nil {
	return 0, fmt.Errorf("Age %q is not a valid integer", ageText)
}

A completely new error is created here, but the original error returned by strconv.Atoi is not preserved.

If there's a need to continue checking the underlying error type later, the original information is already lost.

A more appropriate way to write it is to use %w to wrap the original error:

if err != nil {
	return 0, fmt.Errorf("Age %q is not a valid integer: %w", ageText, err)
}

This supplements the current business context while also preserving the underlying error.

For example, when inputting "30 years old", the complete error might be:

Age "30 years old" is not a valid integer: strconv.Atoi: parsing "30 years old": invalid syntax

At this stage, you can first remember:

Just want to create a static error        → errors.New
Need to insert dynamic data                → fmt.Errorf
Need to preserve the underlying error      → fmt.Errorf + %w

Errors Can Be Passed to the Upper Layer

Next, I added a createUser function:

func createUser(name, ageText string) error {
	age, err := parseAge(ageText)

	if err != nil {
		return fmt.Errorf("Failed to create user %q: %w", name, err)
	}

	fmt.Printf("User created successfully: Name=%s, Age=%d\n", name, age)

	return nil
}

This function only returns an error:

func createUser(name, ageText string) error

Because there's no need to return a user object here yet, it just needs to tell the caller:

Creation succeeded
or creation failed

On success, return:

return nil

On failure, return:

return fmt.Errorf("Failed to create user %q: %w", name, err)

Then handle the final error in main:

func main() {
	err := createUser("Starry", "30 years old")

	if err != nil {
		fmt.Println("Program processing failed:", err)
		return
	}

	fmt.Println("Program execution completed")
}

Output on failure:

Program processing failed: Failed to create user "Starry": Age "30 years old" is not a valid integer: strconv.Atoi: parsing "30 years old": invalid syntax

If changed to the correct age:

err := createUser("Starry", "30")

Output:

User created successfully: Name=Starry, Age=30
Program execution completed

This time, the error went through multiple layers of transmission:

strconv.Atoi
→ parseAge
→ createUser
→ main

strconv.Atoi is responsible for discovering the lowest-level conversion problem:

parsing "30 years old": invalid syntax

parseAge supplements the context of the current parameter:

Age "30 years old" is not a valid integer

createUser supplements the current business scenario:

Failed to create user "Starry"

Finally, main decides how to handle it:

Print error
→ End program

This reflects a very common way of error propagation:

The lower-level function describes the specific reason for failure, the upper-level function supplements the current business scenario, and the outermost layer decides how to respond.

In backend interfaces later, the outermost layer might not simply print the error, but:

Log the error
Return HTTP status code
Return JSON error message
End the current request

How Should %w Be Understood Currently?

In createUser, I used:

fmt.Errorf("Failed to create user %q: %w", name, err)

The %w here is used to wrap the original error.

The final generated error contains both the upper-level information:

Failed to create user "Starry"

and preserves the underlying error:

Age "30 years old" is not a valid integer

For now, you can first simply distinguish:

%v: Formats the error content into a string
%w: Wraps the original error when creating a new error

For example:

fmt.Errorf("Failed to create user: %v", err)

Although the text of the original error can also be seen in the output, it doesn't establish a wrapping relationship that Go's error tools can continue to inspect.

Whereas:

fmt.Errorf("Failed to create user: %w", err)

preserves the error chain.

Later, it can be used with:

errors.Is()
errors.As()

to check if a specific error exists in the error chain.

I'll continue learning this part when I encounter actual scenarios later.

At this stage, first remember:

When supplementing context to an error, don't just keep the upper-level description; try to preserve the real underlying cause as well.


A Complete Example of Parameter Validation and Error Propagation

Putting the previous code together, we get the following complete example:

package main

import (
	"fmt"
	"strconv"
)

func parseAge(ageText string) (int, error) {
	age, err := strconv.Atoi(ageText)

	if err != nil {
		return 0, fmt.Errorf("Age %q is not a valid integer: %w", ageText, err)
	}

	if age < 0 {
		return 0, fmt.Errorf("Age cannot be less than 0, current value is %d", age)
	}

	if age > 150 {
		return 0, fmt.Errorf("Age cannot be greater than 150, current value is %d", age)
	}

	return age, nil
}

func createUser(name, ageText string) error {
	age, err := parseAge(ageText)

	if err != nil {
		return fmt.Errorf("Failed to create user %q: %w", name, err)
	}

	fmt.Printf("User created successfully: Name=%s, Age=%d\n", name, age)

	return nil
}

func main() {
	err := createUser("Starry", "30 years old")

	if err != nil {
		fmt.Println("Program processing failed:", err)
		return
	}

	fmt.Println("Program execution completed")
}

The call relationship in this example is:

main
└── createUser
    └── parseAge
        └── strconv.Atoi

Errors are passed upwards in the opposite direction:

strconv.Atoi discovers format error
→ parseAge supplements parameter context
→ createUser supplements business context
→ main decides how to handle it ultimately

This already has a bit of the code structure of an actual backend project:

The bottom layer is responsible for completing specific operations
The business layer is responsible for organizing business logic
The entry layer is responsible for returning or displaying the final result

Looking at Go's Error Handling from a Frontend Perspective

In JavaScript and TypeScript, I'm more familiar with:

try {
  const result = await doSomething();
} catch (error) {
  console.error(error);
}

Errors usually enter catch by throwing exceptions.

When Go handles predictable errors, the common way is:

result, err := doSomething()

if err != nil {
	return err
}

That is, treating the error as a normal return value and explicitly handing it to the caller.

At first, seeing a lot of:

if err != nil

does feel a bit repetitive.

But after writing divide, parseAge, and createUser myself, I started to understand what this design intends to express.

Every time a function that might fail is called, the code reminds me:

This operation might fail
→ Did you check the error?
→ What are you going to do if it fails?

Errors won't automatically disappear just because no one pays attention.

They usually have three destinations:

Handled directly by the current function
→ Continue returning after supplementing context
→ Explicitly choose to ignore

Among them, ignoring an error should be a decision made after consideration, not just using _ to save a few lines of code.


Does Go Have No Exceptions at All?

A misunderstanding needs to be avoided here:

Go is not completely without mechanisms similar to exceptions.

Go also provides:

panic
recover

But for predictable problems like parameter errors, file read failures, database query failures, network request failures, etc., Go usually uses error return values for handling.

panic is more suitable for indicating that the program has encountered a serious problem that prevents it from continuing normal execution, and should not be treated as a normal business error handling method.

I haven't formally learned panic and recover yet, so at this stage, I'll focus on the most common pattern:

result, err := doSomething()

if err != nil {
	// Handle or return error
}

Real Pitfalls Encountered at This Stage

Looking back at this round, the most worth recording are still these prompts given by the compiler.

Too Few Parameters Passed

not enough arguments in call to introduce
	have (string)
	want (string, string)

Too Many Parameters Passed

too many arguments in call to introduce
	have (string, string, string)
	want (string, string)

Wrong Parameter Type

cannot use 30 (untyped int constant) as string value in argument to introduce

Mismatch Between Number of Return Values and Receiving Variables

assignment mismatch: 2 variables but add returns 1 value

After Ignoring an Error, the Normal Return Value Is Not Necessarily Trustworthy

age, _ := strconv.Atoi("30 years old")

At this point, we get:

age = 0

But it's already impossible to determine through code:

Did the user input "0"
Or is it the zero value 0 obtained after conversion failure?

Error Cannot Be Judged Solely by the Normal Return Value

The correct way to write it should be:

age, err := strconv.Atoi("30 years old")

if err != nil {
	fmt.Println("Conversion failed:", err)
	return
}

fmt.Println("Age:", age)

What really should be judged is:

err != nil

not:

age == 0

Summary of Knowledge Points at This Stage

The content actually learned at this stage can be organized into the following table:

Content Example
Declare function func sayHello() {}
Receive parameters func introduce(name string) {}
Shorthand for same-type params func add(a, b int) {}
Return one value func add(a, b int) int
Return multiple values func calculate(a, b int) (int, int)
Ignore return value result, _ := calculate()
Return error func divide(a, b int) (int, error)
Create static error errors.New("Divisor cannot be 0")
Create dynamic error fmt.Errorf("Current value is %d", value)
Wrap underlying error fmt.Errorf("Operation failed: %w", err)
Check if failed if err != nil {}
Return empty error on success return result, nil
Pass error upwards return fmt.Errorf("Business failed: %w", err)

The common error handling structure in Go is:

result, err := doSomething()

if err != nil {
	return err
}

fmt.Println(result)

If the current function also needs to supplement business context, it can be written as:

result, err := doSomething()

if err != nil {
	return fmt.Errorf("Failed to execute some business: %w", err)
}

Final Words

At this stage, starting from the simplest function, I learned all the way to:

Define function
→ Pass parameters
→ Return one value
→ Return multiple values
→ Use blank identifier
→ Return error
→ Judge if err is nil
→ Create static error
→ Create error with context
→ Wrap underlying error
→ Continue returning error to the upper layer

Now looking back at the code left from the previous article:

age, err := strconv.Atoi(ageText)

It's no longer just a fixed syntax that needs to be memorized by rote.

It expresses:

Attempt to convert string to integer
→ Return conversion result
→ Simultaneously return whether an error occurred

On success:

age is a valid result
err is nil

On failure:

age gets the zero value of int type
err holds the specific error

This stage gave me another layer of feeling about Go:

Go doesn't just require the success path to be written clearly, it also requires the failure path to explicitly exist.

A function cannot just tell the caller "I'm done."

If something might fail, it also has to hand over the reason for failure, letting the caller decide what to do next.

For a frontend developer who previously mainly focused on page display and interface results, this kind of thinking is slowly shifting my focus from:

Can it run under normal circumstances?

to expanding to:

What if the input is illegal?
What if the operation fails?
Where should the error be handled?
Does business context need to be supplemented?
Should it continue to be returned to the upper layer?
What should the outermost layer return to the user?

This should also be an ability that must be gradually built up when moving from frontend to full-stack.

In the next stage, I will continue learning function-related content:

Named return values
→ defer
→ Execution order of defer
→ Why files and resources need to be closed
→ Basic understanding of panic and recover

Still following the current method:

Write code first
→ Actively create problems
→ Observe compiler and run results
→ Understand rules
→ Then organize into an article

See you in the next article.

Comments

Top 2 from juejin.cn, machine-translated. The original thread is authoritative.

Tardis

[666]

妙码生花

[给力][给力]