05 Functions and Methods What's the Difference Between Go's Functions and Methods

05 Functions and Methods - What’s the Difference Between Go’s Functions and Methods #

In the previous lesson’s practice, we created a two-dimensional array and used it. In the previous lesson, I mainly introduced one-dimensional arrays. In fact, two-dimensional arrays are also very simple. You can follow the example of one-dimensional arrays, as shown in the code below:

aa := [3][3]int{}

aa[0][0] = 1

aa[0][1] = 2

aa[0][2] = 3

aa[1][0] = 4

aa[1][1] = 5

aa[1][2] = 6

aa[2][0] = 7

aa[2][1] = 8

aa[2][2] = 9

fmt.Println(aa)

I believe you have also completed it. Now let’s learn about functions and methods in this lesson.

Functions and methods are the first step towards code reuse and collaborative development. Through functions, we can break down development tasks into small units that can be reused by other units, thereby improving development efficiency and reducing code duplication. In addition, since existing functions have been thoroughly tested and used, other functions are also safer when using these functions, and the bug rate is lower than if you were to write a similar function with similar functionality from scratch.

In this lesson, I will explain in detail the functions and methods in Go, and understand their declaration, usage, and differences. Although Go has two concepts: functions and methods, they are very similar, except for the objects they belong to. Let’s start with functions.

Functions #

Exploring Functions #

In the previous four lessons, you have already seen a very important function in Go: the main function. It is the entry function of a Go program, and I will use it repeatedly when demonstrating code examples.

Here is an example of a main function:

func main() {

}

It consists of the following parts:

  1. Any function definition starts with the func keyword, which declares a function just like the var keyword declares a variable.
  2. Next is the name of the function, main. The name should follow the Go naming conventions, for example, it cannot start with a digit.
  3. The pair of parentheses () after the function name cannot be omitted. Inside the parentheses, you can define the parameters used by the function. In this case, the main function has no parameters, so the parentheses are empty ().
  4. After the parentheses (), there can be a return value for the function. Since the main function has no return value, it is not defined here.
  5. Finally, the function body is enclosed in curly braces {}. You can write code in the function body to define its own business logic.

Function Declaration #

With the introduction in the previous section, I believe you already have a clear understanding of the structure of functions in Go. Now let’s summarize the declaration format of functions together, as shown in the code below:

func funcName(params) result {

    body

}

This is the signature definition of a function, which includes the following parts:

  1. The func keyword.
  2. The function name, funcName.
  3. The parameters of the function, params, which are used to define the variable names and types of the formal parameters. There can be one or more parameters, or none.
  4. result is the returned function value, used to define the type of the return value. If there is no return value, it can be omitted, or there can be multiple return values.
  5. The body is the function body, where you can write the code logic of the function.

Now, according to the function declaration format above, let’s define our own function, as shown below:

ch05/main.go

func sum(a int, b int) int {

    return a + b

}

This is a function that calculates the sum of two numbers. The name of the function is sum, and it has two parameters, a and b, both of which are of type int. The return value of the sum function is also of type int. The body of the function simply adds a and b together and returns the result using the return keyword. If the function has no return value, the return keyword can be omitted.

Finally, we can declare our own function. Congratulations on taking a big step!

The definition of formal parameters in a function is the same as defining variables. The variable name comes first, followed by the variable type. The difference is that in a function, the variable name is called a parameter name, which is the formal parameter of the function, and the formal parameter can only be used within the function body. The value of the formal parameter in the function is provided by the caller, and this value is also called the actual parameter of the function. Now let’s pass actual arguments to the sum function to demonstrate how to call a function, as shown in the code below:

ch05/main.go

func main() {

    result := sum(1, 2)

    fmt.Println(result)

}

In the main function, we directly call the custom sum function. When calling the function, you need to provide the actual parameters, which are the values 1 and 2. The return value of the function is assigned to the variable result and then printed out. If you run it yourself, you can see that the result is 3. In this way, we achieve the purpose of adding two numbers through the function sum. If other business logic also requires adding two numbers, you can directly use this sum function without redefining it.

In the above function definition, the types of the a and b parameters are the same, so we can omit the declaration of one of the types, as shown below:

func sum(a, b int) int {

    return a + b

}

Variables separated by commas can be used this way, with a unified type int, similar to variable declarations. Multiple variables of the same type can be declared this way.

Multiple Return Values #

Unlike some programming languages, Go functions can return multiple values, also known as multiple return values. In the Go standard library, you can see many functions like this: the first value returns the result of the function, and the second value returns the error information of the function. This is a classic application of multiple return values.

For the sum function, suppose we don’t allow negative arguments. We can modify it like this: when the arguments are negative, we use multiple return values to return the function’s error message, as shown in the following code:

func sum(a, b int) (int, error){

    if a<0 || b<0 {

        return 0,errors.New("a or b cannot be negative")

    }

    return a + b,nil

}

Here, it is important to note that if a function has multiple return values, the type definition of the return values needs to be enclosed in parentheses, like (int, error). This means that the sum function has two return values, the first one being of type int and the second one being of type error. When returning results in the function body, it should also follow this order of types.

In the function body, you can use return to return multiple values, separated by commas. The order of the types of the returned values should match the order of the types defined in the function signature. For example, in the following example:

return 0, errors.New("a or b cannot be negative")

The first returned value 0 is of type int, and the second value is of type error, which exactly matches the types defined in the function declaration.

After defining a function with multiple return values, now let’s try calling it with the following code:

func main() {

    result,err := sum(1, 2)

    if err!=nil {

        fmt.Println(err)

    }else {

        fmt.Println(result)

    }

}

When a function has multiple return values, multiple variables are needed to receive their values. In the example, the variables result and err are used, separated by commas.

If some of the function’s return values are not needed, you can use an underscore _ to discard them. This approach is also used in the lesson on for-range loops, as shown below:

result,_ := sum(1, 2)

This way, the error information returned by the sum function is ignored, and no further checks need to be done.

Tip: The error used here is an interface built into Go, used to represent program error information. I will provide a detailed introduction in future lessons.

Named Return Parameters #

Not only can function parameters have variable names, but so can function return values. This means that you can assign a name to each return value, allowing you to use them within the function body, just like parameters.

Now, let’s continue to modify the example of the sum function by adding names to its return values, as shown in the following code:

func sum(a, b int) (sum int, err error){

    if a<0 || b<0 {

        return 0,errors.New("a or b cannot be negative")

    }

    sum=a+b

    err=nil

    return 

}

Naming return values is the same as naming parameters or variables: the name comes first and the type comes after. In the example above, there are two named return values, sum and err, which can now be used within the function body.

Using the following approach in the example directly assigns values to the named return parameters. It is equivalent to the function having return values, so the values returned by return can be omitted. This means that there is only one return statement in the example, with no values to be returned.

sum=a+b
err = nil

By assigning the value through named return parameters, and by using the return statement directly, the results are the same. Therefore, when calling the sum function above, the result returned is the same.

Although Go language supports named return values, it is not very common. Depending on your own needs, choose whether to name the return values of functions.

Variadic Parameters #

Variadic parameters allow a function to have a variable number of arguments. The most common example is the fmt.Println function.

With this function, you can pass zero, one, two, or more arguments. This kind of function is called a variadic function, as shown below:

fmt.Println()
fmt.Println("飞雪")
fmt.Println("飞雪", "无情")

The following is the declaration of the Println function, from which we can see that to define variadic parameters, we just need to add three dots ... before the parameter type:

func Println(a ...interface{}) (n int, err error)

Now we can also define our own variadic functions. Let’s use the sum function as an example. In the following code, I calculate the sum of all the arguments passed by the caller using the variadic parameter:

ch05/main.go

func sum1(params ...int) int {
    sum := 0
    for _, i := range params {
        sum += i
    }
    return sum
}

To distinguish it from the sum function, I defined a new function named sum1, which has a variadic parameter. The function then calculates the sum of these parameters using a for range loop.

Now that we’ve come this far, I believe you understand that the type of variadic parameters is actually a slice. For example, the type of the params parameter in the example is []int, so you can use a for range loop to iterate over it.

With variadic parameters, functions can be used flexibly.

As shown in the example below, you can pass any number of arguments, which is very convenient and flexible:

ch05/main.go

fmt.Println(sum1(1, 2))
fmt.Println(sum1(1, 2, 3))
fmt.Println(sum1(1, 2, 3, 4))

Here, it should be noted that if your function has both normal and variadic parameters, the variadic parameters must be placed at the end of the parameter list, for example, sum1(tip string, params ...int), where params must be the last parameter.

Package-level Functions #

Whether it’s a custom function like sum or sum1, or a function we use like Println, it belongs to a package, i.e. a package. The sum function belongs to the main package, and the Println function belongs to the fmt package.

Functions in the same package, even if they are private (function names start with lowercase letters), can still be called. If a function in a different package needs to be called, the scope of the function must be public, i.e. the first letter of the function name must be capitalized, like Println.

I will explain this in detail in the later lessons on packages, scopes, and modularity. For now, remember:

  1. Function names starting with lowercase letters represent private functions and can only be called in the same package.
  2. Function names starting with uppercase letters represent public functions and can be called in different packages.
  3. Every function belongs to a package.

Tip: Go doesn’t use public or private modifiers to indicate whether a function is public or private. Instead, it uses the case of the function name. This eliminates the need for cumbersome modifiers and makes the code more concise.

Anonymous Functions and Closures #

As the name suggests, an anonymous function is a function without a name, which is its main difference from normal functions.

In the example below, the value corresponding to the variable sum2 is an anonymous function. It is worth noting that sum2 is only a variable of the function type, not the name of the function.

ch05/main.go

func main() {
    sum2 := func(a, b int) int {
        return a + b
    }
    fmt.Println(sum2(1, 2))
}

With sum2, we can call the anonymous function. The result of the example above is 3, which is the same as using a normal function.

With anonymous functions, you can define functions within functions (nested functions). The anonymous function defined within a function can also be called an inner function, and more importantly, the inner function defined within a function can use variables of the outer function, which is also known as a closure.

Let’s demonstrate this with the following code:

ch05/main.go

func main() {
    cl := closure()
    fmt.Println(cl())
    fmt.Println(cl())
    fmt.Println(cl())
}

func closure() func() int {
    i := 0
    return func() int {
        i++
        return i
    }
}

By using cl, we can call the anonymous function. The results of the example above are 1, 2, and 3, which illustrates the concept of closures.

}
}

When you run this code, you will see that the output printed is:

1
2
3

This is thanks to the ability of anonymous function closures. Our custom closure function can return an anonymous function and hold the variable i from the outer function closure. Therefore, each time cl() is called in the main function, the value of i will be incremented.

Tip: In Go, functions are also a type and can be used to declare variables, parameters, or as the return type of another function.

Methods #

Different from Functions #

In Go, methods and functions are two different concepts, but they are very similar. The main difference is that methods must have a receiver, which is a type. This binds the method to the type and is called a method of that type.

In the example below, type Age uint represents a new type Age, which is equivalent to uint. It can be understood as renaming the uint type. The keyword type is a Go keyword that defines a type. I will explain in detail about structures and interfaces in later courses.

ch05/main.go

type Age uint
func (age Age) String(){
    fmt.Println("the age is",age)
}

In the example, the method String() belongs to the type Age. The type Age is the receiver of the String() method.

Unlike functions, when defining a method, we add a receiver (age Age) between the keyword func and the method name String. The receiver is enclosed in parentheses.

The receiver is defined the same way as normal variables and function parameters. The variable name comes before the type of the receiver.

Now the method String() is bound to the type Age. String() is a method of type Age.

After defining a method with a receiver, you can call the method using the dot operator, as shown in the code below:

ch05/main.go

func main() {
    age:=Age(25)
    age.String()
}

When you run this code, you will see the following output:

the age is 25

The receiver is the main difference between functions and methods. Apart from that, methods have all the abilities that functions have.

Tip: Since 25 is also of type uint and uint is equivalent to the Age type I defined, 25 can be forcibly converted to the Age type.

Value Receiver and Pointer Receiver #

The receiver of a method can not only be a value type (as shown in the previous example), but also a pointer type.

If the receiver type in a method is a pointer, then the modifications made to the pointer will be effective, whereas if it is not a pointer, the modifications will have no effect, as shown below:

ch05/main.go

func (age *Age) Modify(){
    *age = Age(30)
}

After calling the Modify() method once and then calling the String() method to see the result, you will see that it has become 30. This shows that modifications based on pointers are effective, as shown below:

age:=Age(25)
age.String()
age.Modify()
age.String()

Tip: When calling a method, the receiver passed is essentially a copy, but one is a copy of that value, and the other is a copy of the pointer pointing to that value. Pointers have the ability to point to the original value, so modifying the value pointed to by the pointer also modifies the original value. We can simply understand that when a value receiver is used, the method is called using a copy of the value, and when a pointer receiver is used, the method is called using the actual value.

In the example, when calling the pointer receiver method, a value type variable is used, not a pointer type. In fact, using a pointer variable to call it is also possible, as shown in the code below:

(&age).Modify()

This is what the Go compiler does automatically for us:

  • If a pointer type receiver method is called using a value type variable, the Go compiler will automatically take the pointer to call it to satisfy the requirements of the pointer receiver.
  • Similarly, if a value type receiver method is called using a pointer type variable, the Go compiler will automatically dereference the pointer to call it to meet the requirements of the value type receiver.

In short, the caller of the method can be either a value or a pointer. You don’t have to pay too much attention to this. Go will automatically handle the conversion for us, greatly improving development efficiency and avoiding bugs caused by carelessness.

Whether to use a value type receiver or a pointer type receiver depends on your requirements: Do you want to change the value of the current receiver when operating on the type, or do you want to create a new value and return it? These factors will determine which receiver to use.

Summary #

In Go, although there are functional and method concepts, they are basically the same. The only difference is the object they belong to. Functions belong to a package, while methods belong to a type. Therefore, methods can be simply understood as functions associated with a type.

Whether it’s a function or a method, they are the first step in code reuse and the foundation of code responsibility separation. Mastering functions and methods can help you write code with clear responsibilities, clear tasks, and reusability, improve development efficiency, and reduce bug rates.

The thinking question left for you in this lesson is: Can methods be assigned as expressions to a variable? If so, how can you call the method through this variable?