Learn to write down 6

Keywords: Go

function

Start the function, which is almost beginning to enter the deep water of golang

Function declaration

In fact, when I started to write the first program using golang, I began to use the function, that is, the main function, but the simple main function has no parameter list and return value list. Unlike C language, the return value of golang can be named like a formal parameter (not just the type of return value). At this time, each named return value will be declared as a local variable (initial 0 value). In addition, golang can return multiple values.

func f(i, j, k int, s, t string) {
	// ...
}

func sub(x, y int) (z int) {
	z = x - y
	return
}

recursion

The examples in the book are a little complex. Parsing HTML is a little "thick" to understand this concept. Here is a Fibonacci example

func fib(n int64) int64 {
	if n == 0 || n == 1 {
		return 1
	} else {
		return fib(n-1) + fib(n-2)
	}
}

The related problem of recursive function call is the call stack. Fortunately, the stack of golang is variable, up to the upper limit of 1GB, and will not overflow in normal applications.

Multiple return values

Function returns multiple values, which is normal in golang, such as a normal return result and an err. Multiple return values (returning multiple values) and named return values (bare return) can replace each other. On the one hand, bare return can reduce duplicate code, on the other hand, bare return may also cause unclear program return values. Therefore, bare return should be used conservatively (in fact, bare return is rarely seen)

error

The error mentioned by golang here is actually an exception in other languages, that is, the programmer can't control the situation that causes program errors. For this kind of situation, the programmer should have a "plan" in the programming stage. Other languages are exception handling, and golang is dealing with errors. Golang's error handling is also a point of make complaints about Wang Yin's great spirit. Perhaps it's not so modern to use Windows code to indicate errors like API. (golang has an exception, but it is roughly equivalent to Unhandled Exception)

In golang, the result used to indicate an error is usually the last return value. When there is only one error, the result is usually bool type (usually named ok). For complex errors, the result is the built-in interface type error.

golang uses a very "traditional" error handling method. Its advantages and disadvantages are obvious. The advantages are that the error location is relatively direct, the error information is simple and clear, and there is no need to track the stack to find errors. The disadvantage is that programmers have the responsibility to carefully handle all foreseeable exceptions.

	resp, err := http.Get(url)
	if err != nil {
		return nil, err
	}

The above code directly returns the error as part of the return value when handling the error calling http.Get()

	doc, err := html.Parse(resp.Body)
	resp.Body.Close()
	if err != nil {
		return nil, fmt.Errorf("parsing %s as HTML: %v", url, err)
	}

In the above code, when handling the error calling html.Parse(), reformat an error message with FMT. Errorfand return it

Fmt.errorfactually uses fmt.Sprintf to format the error message and return the error value. When reformatting the error message, only the description related to the behavior of the function itself (function and its parameters) is added. According to the book, the initial letter of the error message is not capitalized and does not wrap, so that the final message becomes a long string to understand the error path.

package main

import (
	"fmt"
	"log"
	"net/http"
	"time"
)

func main() {
	url := "http://www.baidu.com"
	if err := WaitForServer(url); err != nil {
		log.Fatalf("Site is down: %v\n", err)
	}
}

func WaitForServer(url string) error {
	const timeout = 1 * time.Minute
	deadline := time.Now().Add(timeout)
	for tries := 0; time.Now().Before(deadline); tries++ {
		_, err := http.Head(url)
		if err == nil {
			return nil
		}
		log.Printf("server not responding (%s); retrying ...", err)
		time.Sleep(time.Second << uint(tries)) // Exponential backoff
	}
	return fmt.Errorf("server %s failed to respond after %s", url, timeout)
}

The above code adopts different strategies for error handling, that is, repeated attempts within a certain period of time, which is usually the case for network programs. At the same time, it adopts the backoff strategy of exponential delay, and uses some functions of log package for error information. For the server program, there may not be standard output, but there should be a log to check the cause of the error later.

Obviously, the logging function of golang is still a little complicated. I'll skip it for the time being.

Among various errors, one error is not an error, that is, EOF. For the caller, reading the end of the file is indeed an "error" (a buffer cannot be read full). The IO package contains an error io.EOF to indicate EOF

Function variable

The function variable indicates that golang is a functional language to some extent. The name of the defined function can be regarded as "function constant", and the function constant can be assigned to the function variable. The function variable is a bit like a function pointer, and its usage is similar to that of JavaScript function (it can also be considered similar to php function, which are all function closures). The zero value of the function type is nil (which is almost the meaning of C++ nullptr).

Cannot call an empty function variable.

var f func(int) int
f(3)

The above code will cause downtime.

Function variables can be null (compared with nil), but they cannot be compared with = = or the same type.

Anonymous function

The golang function is a function closure, so it can be an anonymous closure, and it is also lexical scoped. The inner function can use the variables of the outer function. (there are a lot of reference articles. Here is a difference between lexical static scope and dynamic scope Lexical scope and dynamic scope - Jianshu (jianshu.com))

package main

import (
	"fmt"
)

func main() {
	f := squares()
	fmt.Println(f())
	fmt.Println(f())
	fmt.Println(f())
	fmt.Println(f())
}

func squares() func() int {	// The function returns a function of type func() int
	var x int				// It can be understood that x is a shared variable in the squares() closure environment
	return func() int {
		x++
		return x * x
	}
}

In the above code, the anonymous function inside the squares() function can update the local variable X of squares(). In the main() function, after the function squares() returns, the variable x is still alive (hidden in the function variable f), and the life cycle of variable x is not determined by its scope.

The examples of topological sorting in the book are slightly complex, because they are omitted for the time being in order to understand language concepts.

Like javascript, golang has advantages in using static scope, but it is easy to violate intuition when using iterative variables in loops

package main

import (
	"fmt"
	"os"
)

func main() {
	var rmdirs []func()
	for _, dir := range tempDirs() {
		os.MkdirAll(dir, 0755)
		rmdirs = append(rmdirs, func() {
			MyRemoveAll(dir)           // There will be problems here
		})
	}
	// ....
	for _, rmdir := range rmdirs {
		rmdir()
	}
}

func tempDirs() []string {
	return []string{"/tmp/dir1", "/tmp/dir2"}
}

func MyRemoveAll(dir string) {
	fmt.Println(dir)
	os.RemoveAll(dir)
}

The original intention of the program is to create the directories of the two paths returned by tempDirs() in turn, and finally delete the two directories in turn with a loop. When deleting, the deleted directory is output, but the actual output is

/tmp/dir2
/tmp/dir2

And if you look carefully, you can find that / tmp/dir1 still exists, and only / tmp/dir2 is deleted. In fact, if there are 100 directories, the last directory is deleted.

The reason is the same as in Javascript (but JS has no block scope before the let keyword, while golang has block scope). Closure - JavaScript | MDN (mozilla.org)

	for _, dir := range tempDirs() {
		os.MkdirAll(dir, 0755) // There will be problems here
		rmdirs = append(rmdirs, func() {
			MyRemoveAll(dir)
		})
	}

This for loop constitutes a block scope, func() parameter (actually null) and myremoveall () constitute the inner layer, and for constitutes the outer layer. Because dir cannot be found in the inner layer, dir comes from the outer layer, and the outer layer will share the identifier dir (the lexical scope recognizes the name but does not recognize the execution). The dir value will trigger MyRemoveAll() after the execution of rmdir() The value has been determined before execution, so it is the last valid value of the loop (the reason is the same as JS closure, just look at the MDN link).

There is no keyword like let in JS in golang, but because it has block scope, it only needs to introduce an internal variable to capture the iteration variable.

// ..................
		dir := dir
		rmdirs = append(rmdirs, func() {
			MyRemoveAll(dir)
		})
// ..................

Similarly, it is also wrong to write a for loop in the following form (not the result is wrong, but it cannot be compiled, because the shared i is already i when it is actually executed, and the subscript is out of bounds)

// ...................
	dirs := tempDirs()
	for i := 0; i < len(dirs); i++ {
		os.MkdirAll(dirs[i], 0755)
		rmdirs = append(rmdirs, func() {
			MyRemoveAll(dirs[i])
		})
	}
// ...................

In go and defer statements, there is also the problem of this kind of iterative variable capture. In short, when the real execution logic is delayed in definition, this problem will exist for lexical scoped cyclic variables.

Function with variable parameter length

Compared with C language to implement variable length parameters, it is much easier for golang to implement functions with variable length parameters, because it has slice and scatter.

func main() {
	fmt.Println(sum(1, 2, 3, 4, 5))
}

func sum(vals ...int) int {
	total := 0
	for _, val := range vals {
		total += val
	}
	return total
}

In the parameter, the type of vals is... Int, which means that it is a bunch of scattered integers (1,2,3,4,5). Inside the function, it is automatically wrapped into slice ([] int {1,2,3,4,5}). The type of... Int is different from [] int. therefore, it should be considered as the process of automatic conversion of golang language.

Deferred function call

This is a point that make complaints about Wang Yin's great spirit. It is also difficult and difficult to understand.

func title(url string) error {
	resp, err := http.Get(url)
	if err != nil {
		return err
	}
	ct := resp.Header.Get("Content-Type")
	if ct != "text/html" && !strings.HasPrefix(ct, "text/html;") {
		resp.Body.Close()    // Close before returning an error
		return fmt.Errorf("%s has type %s, not text/html", url, ct)
	}
	doc, err := html.Parse(resp.Body)
	resp.Body.Close()      // Close before returning an error
	if err != nil {
		return fmt.Errorf("parsing %s as HTML: %v", url, err)
	}
	doc, err := html.Parse(resp.Body)
	resp.Body.Close()       // Close before returning an error
	if err != nil {
		return fmt.Errorf("parsing %s as HTML: %v", url, err)
	}
	visitNode := func (n *html.Node)  {
		if n.Type == html.ElementNode && n.Data == "title" && n.FirstChild != nil {
			fmt.Println(n.FirstChild.Data)
		}
	}
	forEachNode(doc, visitNode, nil)
	return nil
}

In the above code, in order to ensure that the network is always turned off before an error occurs and returns, and the network is also turned off during normal execution, you need to write repeated shutdown codes many times. golang uses the defer mechanism to handle this situation.

golang does not limit the number of defer statements. The deferred statement will be executed only when the function is about to return, so it is delayed execution (but the expression and value are calculated when called). The execution order of multiple defer statements is just opposite to the calling order. Therefore, the common scenario of defer statements is the closing or release of resources. Using the defer statement, the opening and closing of resources will be

Failed to open resource A

(when successful) defer   Close resource A

Use resource A

Failed to open resource B

(when successful) defer   Close resource B

Using resource B

......

(before the function returns, no matter which path, it is implicitly executed   …… Close resource B, close resource A)

The defer statement can also be used to debug complex functions, that is, set the debugging behavior (such as printing statements) in the "entry" and "exit" of the function. The following example is to calculate the function execution time

func main() {
	bigSlowOperation()
}

func bigSlowOperation() {
	defer trace("bigSlowOperation")()
	// .... some jobs
	time.Sleep(10 * time.Second) // Analog slow operation
}

func trace(msg string) func() {
	start := time.Now()
	log.Printf("enter %s", msg)
	return func() {
		log.Printf("exit %s (%s)", msg, time.Since(start))
	}
}

output

2021/09/20 18:43:33 enter bigSlowOperation
2021/09/20 18:43:43 exit bigSlowOperation (10.0522629s)

In the above example, trace("bigSlowOperation") () is executed by defer, but the expression inside the trace function is calculated at the time of call, so it is executed at the entrance

	start := time.Now()
	log.Printf("enter %s", msg)

The trace function returns an anonymous function, so   trace("bigSlowOperation") () is followed by parentheses. The real execution of this anonymous function is about to arrive   bigSlowOperation() returns the previous, so it can calculate the function execution time.

The following code can output the parameters and results of each function call, and in triple, the use of defer changes the value returned by double(x) to triple

func double(x int) (result int) {
    defer func() { fmt.Printf("double(%d) = %d\n", x, result) }()
    return x + x
}

func triple(x int) (result int) {
    defer func() { result += x }()
    return double(x)
}

Using defer in a loop requires caution, such as

	for _, filename := range filenames {
		f, err := os.Open(filename)
		if err != nil {
			return err
		}
		defer f.Close()
		// use f
	}

Because the files that the operating system can open are limited (file descriptors will be exhausted), the above code may have problems when the defer execution file is closed. The solution is to package the opening, closing, and use of a single file into a function, namely

	for _, filename := range filenames {
		if err := doFile(filename); err != nil {
			return err
		}
	}

	func doFile(filefilename string) error  {
		f, err := os.Open(filename)
		if err != nil {
			return err
		}
		defer f.Close()
		// use f
	}

For file writing, there may be problems with using defer, that is, open the file, close defer, and then write the file. After that, many codes will not be executed until the defer operation is completed, that is, close the file. For many file systems, the operating system will not write the data immediately, and often postpone the writing (possibly until the file is closed). In this way, If the write fails, a write error occurs and the write error is returned too late, which may cause data loss.

panic (downtime, crash)

panic() accepts arbitrary parameters, and should be selected for situations where it would be worse to continue executing subsequent statements. However, when a panic occurs, all delay functions will execute (in reverse order, from the top of the stack to the main() function). Therefore, panic cannot be understood as exit (golang has os.Exit)

recovery

In some cases, we hope to do more after panic, such as better cleaning and even recovery in an appropriate way. If the built-in recover function is called inside the defer function, and the function containing the defer statement has a panic, the recover function will terminate the current panic state and return the value of the panic. The function with the panic will not continue to run from the panic, but will return normally. If the recover function is used elsewhere (for example, there is no panic), it has no effect and directly returns nil. Therefore, the recover function is generated from panic.

Posted by madmax on Mon, 20 Sep 2021 13:48:19 -0700