Now it's time for something completely different and low level. You are free to skip this section if you like, but I bet that you are not reading this book for its easy topics but for the difficult ones, such as node trees!
A Go node is a struct with a large number of properties. You will learn more about defining and using Go structures in Chapter 4, The Uses of Composite Types. Everything in a Go program is being parsed and analyzed by the modules of the Go compiler according to the grammar of the Go programming language. The final product of this analysis is a tree that is specific to the provided Go code, and it represents the program in a different way that is suited for the compiler rather than for the developer.
This section will start with the following Go code, which is saved as nodeTree.go. It is presented here as an example of the kind of low-level information that the go tool can provide:
package main import ( "fmt" ) func main() { fmt.Println("Hello there!") }
The Go code of nodeTree.go is pretty easy to understand, so you will not be surprised by its output, which comes next:
$ go run nodeTree.go Hello there!
Now it's time to see some of the internal workings of Go by executing the following command:
$ go tool compile -W nodeTree.go before main . CALLFUNC l(8) tc(1) STRUCT-(int, error) . . NAME-fmt.Println a(true) l(4) x(0) class(PFUNC) tc(1) used FUNC-func(...interface {}) (int, error) . . DDDARG l(8) esc(no) PTR64-*[1]interface {} . CALLFUNC-list . . CONVIFACE l(8) esc(h) tc(1) implicit(true) INTER-interface {} . . . NAME-main.statictmp_0 a(true) l(8) x(0) class(PEXTERN) f(1) tc(1) used string . VARKILL l(8) tc(1) . . NAME-main..autotmp_0 a(true) l(8) x(0) class(PAUTO) esc(N) used ARRAY-[1]interface {} after walk main . CALLFUNC-init . . AS l(8) tc(1) . . . NAME-main..autotmp_0 a(true) l(8) x(0) class(PAUTO) esc(N) tc(1) addrtaken assigned used ARRAY-[1]interface {} . . AS l(8) tc(1) . . . NAME-main..autotmp_2 a(true) l(8) x(0) class(PAUTO) esc(N) tc(1) assigned used PTR64-*[1]interface {} . . . ADDR l(8) tc(1) PTR64-*[1]interface {} . . . . NAME-main..autotmp_0 a(true) l(8) x(0) class(PAUTO) esc(N) tc(1) addrtaken assigned used ARRAY-[1]interface {} . . BLOCK l(8) . . BLOCK-list . . . AS l(8) tc(1) hascall . . . . INDEX l(8) tc(1) assigned bounded hascall INTER-interface {} . . . . . IND l(8) tc(1) implicit(true) assigned hascall ARRAY-[1]interface {} . . . . . . NAME-main..autotmp_2 a(true) l(8) x(0) class(PAUTO) esc(N) tc(1) assigned used PTR64-*[1]interface {} . . . . . LITERAL-0 a(true) l(8) tc(1) int . . . . EFACE l(8) tc(1) INTER-interface {} . . . . . ADDR a(true) l(8) tc(1) PTR64-*uint8 . . . . . . NAME-type.string a(true) x(0) class(PEXTERN) tc(1) uint8 . . . . . ADDR l(8) tc(1) PTR64-*string . . . . . . NAME-main.statictmp_0 a(true) l(8) x(0) class(PEXTERN) f(1) tc(1) addrtaken used string . . BLOCK l(8) . . BLOCK-list . . . AS l(8) tc(1) hascall . . . . NAME-main..autotmp_1 a(true) l(8) x(0) class(PAUTO) esc(N) tc(1) assigned used SLICE-[]interface {} . . . . SLICEARR l(8) tc(1) hascall SLICE-[]interface {} . . . . . NAME-main..autotmp_2 a(true) l(8) x(0) class(PAUTO) esc(N) tc(1) assigned used PTR64-*[1]interface {} . CALLFUNC l(8) tc(1) hascall STRUCT-(int, error) . . NAME-fmt.Println a(true) l(4) x(0) class(PFUNC) tc(1) used FUNC-func(...interface {}) (int, error) . . DDDARG l(8) esc(no) PTR64-*[1]interface {} . CALLFUNC-list . . AS l(8) tc(1) . . . INDREGSP-SP a(true) l(8) x(0) tc(1) addrtaken main.__ SLICE-[]interface {} . . . NAME-main..autotmp_1 a(true) l(8) x(0) class(PAUTO) esc(N) tc(1) assigned used SLICE-[]interface {} . VARKILL l(8) tc(1) . . NAME-main..autotmp_0 a(true) l(8) x(0) class(PAUTO) esc(N) tc(1) addrtaken assigned used ARRAY-[1]interface {} before init . IF l(1) tc(1) . . GT l(1) tc(1) bool . . . NAME-main.initdone a(true) l(1) x(0) class(PEXTERN) tc(1) assigned used uint8 . . . LITERAL-1 a(true) l(1) tc(1) uint8 . IF-body . . RETURN l(1) tc(1) . IF l(1) tc(1) . . EQ l(1) tc(1) bool . . . NAME-main.initdone a(true) l(1) x(0) class(PEXTERN) tc(1) assigned used uint8 . . . LITERAL-1 a(true) l(1) tc(1) uint8 . IF-body . . CALLFUNC l(1) tc(1) . . . NAME-runtime.throwinit a(true) x(0) class(PFUNC) tc(1) used FUNC-func() . AS l(1) tc(1) . . NAME-main.initdone a(true) l(1) x(0) class(PEXTERN) tc(1) assigned used uint8 . . LITERAL-1 a(true) l(1) tc(1) uint8 . CALLFUNC l(1) tc(1) . . NAME-fmt.init a(true) l(4) x(0) class(PFUNC) tc(1) used FUNC-func() . AS l(1) tc(1) . . NAME-main.initdone a(true) l(1) x(0) class(PEXTERN) tc(1) assigned used uint8 . . LITERAL-2 a(true) l(1) tc(1) uint8 . RETURN l(1) tc(1) after walk init . IF l(1) tc(1) . . GT l(1) tc(1) bool . . . NAME-main.initdone a(true) l(1) x(0) class(PEXTERN) tc(1) assigned used uint8 . . . LITERAL-1 a(true) l(1) tc(1) uint8 . IF-body . . RETURN l(1) tc(1) . IF l(1) tc(1) . . EQ l(1) tc(1) bool . . . NAME-main.initdone a(true) l(1) x(0) class(PEXTERN) tc(1) assigned used uint8 . . . LITERAL-1 a(true) l(1) tc(1) uint8 . IF-body . . CALLFUNC l(1) tc(1) hascall . . . NAME-runtime.throwinit a(true) x(0) class(PFUNC) tc(1) used FUNC-func() . AS l(1) tc(1) . . NAME-main.initdone a(true) l(1) x(0) class(PEXTERN) tc(1) assigned used uint8 . . LITERAL-1 a(true) l(1) tc(1) uint8 . CALLFUNC l(1) tc(1) hascall . . NAME-fmt.init a(true) l(4) x(0) class(PFUNC) tc(1) used FUNC-func() . AS l(1) tc(1) . . NAME-main.initdone a(true) l(1) x(0) class(PEXTERN) tc(1) assigned used uint8 . . LITERAL-2 a(true) l(1) tc(1) uint8 . RETURN l(1) tc(1)
As you can appreciate, the Go compiler and its tools do many things behind the scenes, even for a small program such as nodeTree.go.
Look at the output of the next two commands:
$ go tool compile -W nodeTree.go | grep before before main before init $ go tool compile -W nodeTree.go | grep after after walk main after walk init
As you can see, the before keyword is about the beginning of the execution of a function. If your program had more functions, you would have gotten more output. This is illustrated in the following example:
$ go tool compile -W defer.go | grep before before d1 before d2 before d3 before main before d2.func1 before d3.func1 before init before type..hash.[2]interface {} before type..eq.[2]interface {}
The preceding example uses the Go code of defer.go, which is much more complicated than nodeTree.go. However, it should be obvious than the init() function is automatically created by Go as it exists in both examples (nodeTree.go and defer.go).
I will now present you with a juicier version of nodeTree.go, named nodeTreeMore.go:
package main import ( "fmt" ) func functionOne(x int) { fmt.Println(x) } func main() { varOne := 1 varTwo := 2 fmt.Println("Hello there!") functionOne(varOne) functionOne(varTwo) }
The nodeTreeMore.go program has two variables, named varOne and varTwo, and one additional function named functionOne. Searching the output of go tool compile -W for varOne, varTwo, and functionOne reveals the following information:
$ go tool compile -W nodeTreeMore.go | grep functionOne | uniq before functionOne after walk functionOne . . NAME-main.functionOne a(true) l(7) x(0) class(PFUNC) tc(1) used FUNC-func(int) $ go tool compile -W nodeTreeMore.go | grep varTwo | uniq . . NAME-main.varTwo a(true) g(2) l(13) x(0) class(PAUTO) f(1) tc(1) used int . . . NAME-main.varTwo a(true) g(2) l(13) x(0) class(PAUTO) f(1) tc(1) used int $ go tool compile -W nodeTreeMore.go | grep varOne | uniq . . NAME-main.varOne a(true) g(1) l(12) x(0) class(PAUTO) f(1) tc(1) used int . . . NAME-main.varOne a(true) g(1) l(12) x(0) class(PAUTO) f(1) tc(1) used int
Thus, varOne is represented as NAME-main.varOne while varTwo is indicated by NAME-main.varTwo. The functionOne() function is referenced as NAME-main.functionOne. Consequently, the main() function is referenced as NAME-main.
Now let's examine the following code of the debug parse tree of nodeTreeMore.go:
before functionOne . AS l(8) tc(1) . . NAME-main..autotmp_2 a(true) l(8) x(0) class(PAUTO) esc(N) tc(1) assigned used int . . NAME-main.x a(true) g(1) l(7) x(0) class(PPARAM) f(1) tc(1) used int
This data is related to the definition of functionOne(). The l(8) string tells us that the definition of this node can be found in line 8; that is, after reading line 7. The NAME-main..autotmp_2 integer variable is automatically generated by the compiler.
The next part of the debug parse tree output that I will explain below follows:
. CALLFUNC l(15) tc(1) . . NAME-main.functionOne a(true) l(7) x(0) class(PFUNC) tc(1) used FUNC-func(int) . CALLFUNC-list . . NAME-main.varOne a(true) g(1) l(12) x(0) class(PAUTO) f(1) tc(1) used int
The first line says that in line 15 of the program, which is specified by l(15), you will call NAME-main.functionOne, which is defined in line 7 of the program, as specified by l(7), which is a function that requires a single integer parameter, as specified by FUNC-func(int). The function list of parameters, which is specified after CALLFUNC-list, includes the NAME-main.varOne variable that is defined in line 12 of the program as l(12) shows.