5. Functions

In this chapter, we discuss a core part of any language: functions. In Go, functions are first-class citizens that can be used in many ways. First, we show you how to create functions using the func keyword. Then we explain how to handle inbound arguments and return values. Finally, we cover advance function topics, such as variadic arguments, deferring functions, and the init function.

Function Definitions

Functions in Go resemble functions from most other programming languages. Listing 5.1 shows the definition of two functions: main and sayHello. It also shows two different function calls: sayHello() and fmt.Println("Hello").

Listing 5.1 A Program That Prints Hello

Arguments

Functions can take N number of arguments, including other functions. Function arguments are declared as (name type). Because Go is a typed language, you must define the types of the arguments in the function definition. The type of the argument follows the name of the argument.

In Listing 5.2, the function, sayHello, accepts one argument named greeting of type string.

Listing 5.2 Using Function Arguments

Arguments of the Same Type

When declaring multiple arguments of the same type, the type only needs to be declared at the end of the argument list. In Listing 5.3, both examples are functionally identical. The more explicit (longer) version is preferred for clarity.

Listing 5.3 Declaring Multiple Arguments of the Same Type

When declaring the type only once at the end of the argument list, if you need to insert an argument between the two and you don’t declare the type for the first argument, it inherits the type from the newly inserted argument. In Listing 5.4, the type of greeting was string and now is of type int.

Listing 5.4 Possible Bug Caused by Not Declaring All Argument Types

By always declaring the type for each argument, you make your code more readable, maintainable, and bug resistant.

Return Arguments

Functions can return 0 to N numbers of values, though it is considered best practice to not return more than two or three.

The type of the return value(s) is declared after the function definition (Listing 5.5).

Listing 5.5 Returning Values

When returning an error, which is covered in Chapter 9, it is considered best practice to return that value as the last value. In Listing 5.6, we see three examples of functions that return values. The functions two and three both return multiple values, with an error as the last value.

Listing 5.6 Returning an error

Multiple Return Arguments

Multiple return arguments need to be placed inside of a set of (). In Listing 5.7, the function two returns three values: string, int, and int.

Listing 5.7 Multiple Return Arguments Need to Be Placed Inside of ()

When multiple return arguments are used, then all return arguments must be used. For example, in Listing 5.8, the info function returns three values. Each of these values must be captured. In this case, the values returned from the info function are assigned to the variables gs, length, and capacity.

Listing 5.8 Using Multiple Return Arguments

Unwanted return arguments can be ignored by using the _ placeholder. In Listing 5.9, for example, we are only interested in capturing the second return value from the info function. Using the _, operator, we can discard the values we don’t want.

Listing 5.9 Ignoring Unwanted Return Arguments

Named Returns

You can give the return argument of a Go function a name and use it just like another variable declaration within the function. In Listing 5.10, the IsValid function has a named returned, valid, of type bool. Inside of the IsValid function, the valid variable is already initialized and ready for use. The following are rules for named returns:

  • Variables are automatically initialized to their zero values within the function scope.

  • Executing a return with no value returns the value of the named return.

  • Names are not mandatory.

Listing 5.10 Named Returns

It is best practice to not use named returns, unless they are needed for documentation.

Given the coordinates function in Listing 5.11 that returns latitude and longitude, it would be hard to remember which parameter is meant for which value.

Listing 5.11 A Function Returning Two Unnamed Arguments

In Listing 5.12, using named returns for the coordinates function helps to clarify the intentions of the returned values. Using named returns should only be used for documentation purposes. Even then, it could be argued that they still should be avoided.

Listing 5.12 A Function Returning Two Named Arguments

When Named Returns Go Bad

Listing 5.13 is an example of the confusion, and bugs, that can occur when using a named return. For example, what does the following return? Can you tell by looking at the code? Before running the code, what is your expectation of the outcome?

Listing 5.13 Named Returns Confusion

When using a named return, that return is automatically assigned to the value returned by the function. In Listing 5.13, when return 42 is executed, the value of the named return variable meaning is assigned the value of 42. However, a defer statement is executed when the function finishes but not before it returns. This gives the deferred function a chance to change the value of meaning before the function returns.

First-Class Functions

In Go, functions are first-class citizens. That means you can treat functions as you do any other types. Function signatures are a type. Everything you’ve learned so far about the Go type system applies to functions as well. Functions can be arguments and return values to/from other functions, just as any other Go type can.

Functions as Variables

Like other types you have seen, such as slices and maps, a function definition can also be assigned to a variable for use later. In Listing 5.14, for example, we define and initialize a new variable, f, to a function definition, fun(), which neither accepts nor returns any values. Later, the f variable can be called just like any other function call.

Listing 5.14 Functions as Variables

Functions as Arguments

In Go, functions behave as any other type we’ve discussed so far. Because functions in Go are types, you can pass them as arguments to another function. Declaring arguments for functions requires a name for the variable and the type of the variable: (s string). To define an argument that is a function, you have to include the full method signature as the type for the argument.

In Listing 5.15, the name of the argument/variable is fn, and its type is func() string. The signature of the function being passed in must match the function signature of the argument in the callee function.

Listing 5.15 Functions as Arguments

In Listing 5.16, we are declaring a new variable, f, and assigning it to a function that matches the required signature of the sayHello function. Then we can pass the variable f to the sayHello function. As you see from the output, the sayHello function executed the f variable we had created.

Listing 5.16 Calling a Function with a Function as an Argument

Closures

Functions close over their environment when they are defined. This means that a function has access to arguments declared before its declaration. Consider Listing 5.17. First, we declare and initialize a new variable name with the value Janis. Next, we assign a function definition to the f variable. This time, however, we use the name variable when the function is executed. Finally, the f variable is passed to the sayHello function as an argument and executed.

The output of Listing 5.17 demonstrates that the name variable declared in the main function was properly closed over and made available when the f variable is executed later.

Listing 5.17 Closing over a Variable

Anonymous Functions

Anonymous functions are functions that aren’t assigned to a variable or that don’t have a “name” associated with them.

This is a very common pattern in Go, and one you should get comfortable seeing and using in Go code. Using anonymous functions is very common when working with the net/http1 package, creating your own iterators, or allowing others to extend your application’s functionality. In Listing 5.18, for example, we are calling the sayHello function with a brand new function we are defining inline.

1. https://pkg.go.dev/net/http

Listing 5.18 Anonymous Functions

Functions Accepting Arguments from Functions

Functions can accept the return arguments of one function as its input arguments. This is only possible as long as the return types and number of return arguments are identical to the input arguments and number of input arguments. In Listing 5.19, the returnTwo() function returns two strings. The takeTwo function accepts two strings as arguments. Because the return types and number of return arguments are identical to the input arguments and number of input arguments, the takeTwo function can accept the return arguments of returnTwo() as its input arguments.

Listing 5.19 Results from One Function Being Passed Directly to Another Function

Variadic Arguments

Functions can take variadic arguments, meaning 0 to N arguments of the same type. This is done by pre-pending the type of the argument with .... In Listing 5.20, the sayHello function accepts a variadic argument of type string.

Listing 5.20 Accepting Variadic Arguments

In Listing 5.21, we call the now variadic function sayHello three different ways. The first is with many different names. Next, we call the function with only one value. Finally, the function is called with no values passed. The result of this last call produces no output because no arguments were passed to the sayHello function to print.

Listing 5.21 Calling Variadic Functions

Variadic Argument Position

Variadic arguments must be the last argument of the function. In Listing 5.22, the sayHello function is defined with a variadic argument as the first of two arguments. The result is a compilation error indicating that the variadic argument must be the last argument.

Listing 5.22 Variadic Arguments Must Be the Last Argument

Expanding Slices

Slices cannot be passed directly to functions using variadic arguments. In Listing 5.23, when trying to pass a slice directly to the sayHello function, the result is a compilation error indicating that slices cannot be used as variadic arguments.

Listing 5.23 Slices Cannot Be Used as Variadic Arguments

Slices can be “expanded” using a trailing variadic operator, ..., to send them as individual arguments to a function. In Listing 5.24, we expand the users slice, using the variadic operator, to be passed as individual values to the sayHello function.

Listing 5.24 Expanding Slices Using the Variadic Operator

When to Use a Variadic Argument

While using a variadic argument is not the common use case, it certainly can make your code look nicer and be easier to read and use. The most common reason to use a variadic is when your function is commonly called with zero, one, or many arguments.

Consider the LookupUsers function in Listing 5.25. It accepts a []int as an argument, and returns a []User.

Listing 5.25 LookupUsers Function Definition Before Variadic Arguments

You may have one or many users to look up. Without using a variadic signature, calling this function can be messy and hard to read. Consider Listing 5.26. When calling LookupUsers with a single id, a new []int is created and passed to the function.

Listing 5.26 LookupUsers Function Call Before Variadic Arguments

In Listing 5.27, the LookupUsers function has a variadic signature and can be called with a single id or a list of ids.

Listing 5.27 LookupUsers Function Definition After Variadic Arguments

With a variadic function signature, the LookupUsers function can now be called with a single id or a list of ids. The result, as shown in Listing 5.28, is cleaner, more efficient, and easier to read.

Listing 5.28 LookupUsers Function Call After Variadic Arguments

Deferring Function Calls

The defer keyword in Go allows you to defer the execution of a function call until the return of the parent function. Listing 5.29 shows an example of deferring a function call. When executed, hello is first printed, and then, when the function exits, goodbye is printed.

Listing 5.29 Deferring a Function Call

Deferring with Multiple Returns

The defer keyword is useful for ensuring functions get called regardless of which path your code takes. When working with various types of IO in Go, it is very common to use defer to ensure the appropriate resources are cleaned up when the function exits.

In Listing 5.30, for example, once we open a file, using os.Open, we immediately defer a call to the Close method on the file. This ensures that regardless of how the rest of the program exits, the Close method will be called, and the underlying file will be properly closed. This prevents the leaking of file descriptors and a potential crash of the application.

Listing 5.30 Deferring Cleanup of Resources on Function Exit

Defer Call Order

The order of execution for deferred function calls is LIFO (last in, first out). In Listing 5.31, you see the output of the function is printed in reverse order of the defer statements.

Listing 5.31 Deferred Function Calls Are Executed in LIFO Order

Deferred Calls and Panic

If one or many deferred function calls panics, the panic is caught. The rest of the deferred calls are still executed and then the panic is reraised. Panics, and recovering from them using the recover2 function, are a very common use case to defer and are covered in Chapter 9.

2. https://pkg.go.dev/builtin#recover

In Listing 5.32, we have three function calls being deferred. The first and third function calls are simple print statements. The second deferred function call, however, calls the panic function, which raises an application panic that crashes the application. As you see from the output in Listing 5.32, the third function is called first, the first function is called second, and the panic is called last. As you see from this example, an application panic won’t prevent deferred function calls, like closing a file, from happening.

Listing 5.32 Deferred Calls Are Executed Even if Another Deferred Call Panics

Defers and Exit/Fatal

Deferred calls will not fire if the code explicitly calls os.Exit,3 as in Listing 5.33, or if the code calls log.Fatal,4 as in Listing 5.34.

3. https://pkg.go.dev/os#Exit

4. https://pkg.go.dev/log#Fatal

Listing 5.33 Deferred Calls Are Not Executed if the Code Exits

Listing 5.34 Deferred Calls Are Not Executed if the Code Logs a Fatal Message

Defer and Anonymous Functions

It is also common to combine cleanup tasks into a single defer using an anonymous function. In Listing 5.35, instead of deferring the Close function on each file directly, they are being wrapped in an anonymous function, along with logging. This anonymous function is then deferred and executed when the fileCopy function exits.

Listing 5.35 Deferring an Anonymous Function

Defer and Scope

It is important to understand scope when using the defer keyword. What is your expectation of the outcome for the program in Listing 5.36? It would be reasonable to expect that time since program started should be at least 50ms.

Listing 5.36 Deferring a Function Call with No Scope

As shown by the program’s output in Listing 5.36, the result of the time.Since5 function, Listing 5.37, is far short of the expected value of 50ms.

5. https://pkg.go.dev/time#Since

Listing 5.37 The time.Since Function

The reason the output in Listing 5.36 is not as expected is because it is only the fmt.Printf6 call that is being deferred. The arguments being sent to the fmt.Printf function are not being deferred and are being executed immediately. As a result, the time.Since function is being called almost immediately after the now variable has been initialized.

6. https://pkg.go.dev/fmt#Printf

To make sure your variables are evaluated when a deferred function executes and not when they are scheduled, you need to scope them via an anonymous function. In Listing 5.38, it is only when the deferred anonymous function executes that the time.Since function is called, producing the expected output.

Listing 5.38 Properly Scoping a Deferred Function Call

Init

During initialization of a .go file, the compiler executes any init functions it finds in the file before the main function.

In Listing 5.39, we have an init function that prints the word init. The main function prints main but does not call the init function directly. As you see from the output, the init function is called before the main function.

Listing 5.39 A Basic Program with an init() Function

Multiple Init Statements

Unlike all other functions that can only be declared once, the init() function can be declared multiple times throughout a package or file. However, multiple init()s can make it difficult to know which one has priority over the others.

When declaring multiple init functions within the same file, the init() functions execute in the order that you encounter them. In Listing 5.40, the four init functions are executed in the order that they are declared in the file.

Listing 5.40 Multiple init() Functions in the Same File

Init Order

If you have multiple init() declarations in a package in different files, the compiler runs them based on the order in which it loads files.

Given the directory structure in Listing 5.41, if you had an init() declaration in a.go and b.go, the first init() to run would be from a.go. However, if you renamed a.go to c.go, the init() from b.go would run first.

Care must be taken if any init() declarations have an order of precedence when running and exist in different files.

Listing 5.41 A Directory Structure with init() Declarations in Different Files

Using init Functions for Side Effects

In Go, it is sometimes required to import a package not for its content but for the side effects that occur upon importing the package. This often means that there is an init() statement in the imported code that executes before any of the other code, allowing for the developer to manipulate the state in which their program is starting. This technique is called importing for a side effect.

A common use case for importing for side effects is to register functionality in your code, which lets a package know what part of the code your program needs to use. In the image7 package, for example, the image.Decode8 function needs to know which format of the image it is trying to decode (jpg, png, gif, and so on) before it can execute. You can accomplish this by first importing a specific program that has an init() statement side effect.

7. https://pkg.go.dev/image

8. https://pkg.go.dev/image#Decode

Consider that you are trying to use image.Decode on a .png file with the function in Listing 5.42.

Listing 5.42 A Program That Uses image.Decode to Decode a .png File

A program with this code still compiles, but any time you try to decode a png image, you get an error, as shown in Listing 5.43.

Listing 5.43 Compilation Error Trying to Decode a png

To fix this, you need to first register the .png image format for image.Decode to use. When imported, the image/png9 package, which contains the init() statement in Listing 5.44, is called, and the package registers itself with the image package as an image format.

9. https://pkg.go.dev/image/png

Listing 5.44 An init() Statement in the image.png Package Registering the .png Image Format

In Listing 5.45, we import image.png package into our application; then the image.RegisterFormat()10 function in image/png runs before any of our code and registers the .png format before we try to use it.

10. https://pkg.go.dev/image#RegisterFormat()

Listing 5.45 A Program That Uses image.Decode to Decode a .png File

While this use of import statements and init functions can be found in numerous places in the standard library, this is considered an anti-pattern in Go and should never be used. Always be explicit in your code.

Summary

In this chapter, we covered functions in Go. We explained how, in Go, functions are first-class citizens. We showed how you can create new types in the type system that are based on functions. We also discussed using variadic arguments to allow a function to accept an arbitrary number of arguments of one type. We also covered deferring functions, which allows you to defer a function call until a later time. We also explained how to use deferred functions to ensure that resources are cleaned up in the correct order and how to recover from panics. Finally, we showed you how to use the init function to initialize a package and explained the dangers of using it.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset