Chapter 2 program structure
Go language, like other languages, a large program is composed of many program components. Variables hold values, and various operations form expressions. The basic types are aggregated into more complex data types such as structures and arrays. Then, for and if are used to organize and control the execution flow of expressions, and then multiple statements are organized into a function for code isolation and reuse. Functions are organized as source files and packages
2.1 naming
All naming of variable name, function name, constant name, type name, statement label and package name in Go language follow two basic guidelines:
Starting with a letter or underscore, uppercase means that it can be exported, that is, it can be accessed by external packages
Case sensitive
There are 25 keywords in Go language. Keywords can only be used in specific grammatical structures and cannot be used for other purposes. We will talk about the usage of these keywords later
In addition, we have 30 predefined names, such as int and true, mainly for built-in constants, types and functions
Built in constants: true, false, iota, nil
Built in type: int, int8; uint8,uint…; float32…; bool,byte,rune,string,error
Built in functions: make, len, cap, new, append, copy, close, delete, complex, real, imag, panic, recover
Hump naming is recommended for Go language
2.2 declaration
Declaration statement defines various entity objects and some or all attributes of the program. It mainly has four types of declarations: var, const, type and func, which correspond to variables, constants, types and function entities respectively
A program written in go language corresponds to one or more source files ending in. Go. Each source file starts with the declaration statement of the package, indicating which package the source file belongs to. The package declaration statement is followed by the import of dependent package, and then the declaration statement of package level variables, types, functions and constants, as follows:
package main import "fmt" const boilingF = 212.0 func main() { var f = boilingF var c = (f-32)*5/9 fmt.Printf("boiling point = %v℃ or %vF\n",f,c) }
Analysis code:
We first declare a constant at the package level
const boilingF = 212.0
Then we define two variables inside the function, which can only be called inside the function
var f = boilingF var c = (f-32)*5/9
Function structure: the function declaration consists of function name, parameter list (the caller of the function provides the specific value of parameter variables), an optional return value list and function body. The function can have no return value. The function is executed in the order of statements in the function body. It will return when it encounters return. If there is no return statement, it will be executed to the end of the function and then returned to the function caller
func main() { const freezingF, boilingF = 32.0,212.0 fmt.Printf("%v℉ = %v℃\n",freezingF,ftoC(freezingF)) fmt.Printf("%v℉ = %v℃\n",boilingF,ftoC(boilingF)) } func ftoC(f float64)float64{ return (f-32)*5/9 }
We can define a function (execute logic) and call it many times elsewhere
2.3 variables
The syntax of variable declaration is as follows:
//var variable name type = expression
var a = 45 var b int fmt.Println(a,b)//45 0
When declaring, you can choose either = or type. If you select =, it is an initialization variable. If you select int, you can change the default value of the type; The value type is 0, the string type is an empty string, the boolean type is false, and the zero value corresponding to the variables of interface and reference types (including slice, pointer, map, chan and function) is nil
Declare the same set of variables and different variables at the same time
var i,k,j int var d,f,s = true, 3.2, "four" fmt.Println(i,k,j,d,f,s)//0 0 0 true 3.2 four
The initialization of variables can also be done by calling the function to complete the assignment of the return value of the function
var f,err = os.Open(fileName)
2.3.1 short variable declaration
The short variable declaration does not use the var keyword and uses the following format. Because of its simplicity and convenience, short declarations are widely used in the declaration and initialization of local variables
Variable name := expression
anim := gif.GIF{LoopCount: nframes} fred := rand.Float64()*3.0 t := 0.0
Declare the same set of variables and different variables at the same time
i , j := 0,1 a, b := 2, true
The initialization of variables can also be done by calling the function to complete the assignment of the return value of the function
f, err := os.Open(fileName) if err != nil{ return err } f.Close()
2.3.2 pointer
A variable corresponds to a memory space that holds the corresponding type value of the variable. Ordinary variables are bound to a variable name when the declaration statement is created.
The value of one pointer is the address of another variable. A pointer corresponds to the storage location of the variable in memory. Not every value will have a memory address, but there must be a corresponding memory address for each variable. The pointer can directly read and update the value of the variable without knowing the name of the variable (if the variable has a name)
The pointer of variable x is & X, and the data type corresponding to the pointer is * variable type. If the pointer name is p, it can be said that "P pointer points to variable X", or "P saves the memory address of variable X", and the * P expression corresponds to the variable value pointed to by P pointer
x := 1 p := &x *p = 2 fmt.Println(p, *p) //0xc00001e090 2
For each member of the aggregation type, such as each member in the structure or each element of the array, it also corresponds to a variable, so it can be addressed
The zero value of any type of pointer is nil and can be tested for equality
var x, y int fmt.Println(x==y,&x==&y)//true false
In the Go function, it is also safe to return the address of the local variable in the function. Every time we call it in the main function, we create a variable v, such as:
func main() { fmt.Println(f())//0xc000100010 } var p = f() func f() *int{ v:=1 return &v }
You can update the value by using pointers. In fact, * p is the alias of v. in addition to pointers, other types will also create aliases. slice, map, chan, and even structures, arrays, and interfaces will create aliases for the referenced variables
func main() { v := 1 incr(&v) fmt.Println(incr(&v))//3 } func incr(p *int) int{ *p++ return *p }
Pointer is the key technology to realize the flag package in the standard library. It uses the command line parameters to set the value of the corresponding variables, and the variables of these command line flag parameters may be scattered in the whole program
2.3.3 new function
Another way to create variables is to call the built-in new function
The expression new(T) will create an anonymous variable of type T, initialize it to the zero value of type T, and then return the variable address. The returned pointer type is * t, as follows:
p := new(int) var q int x := &q fmt.Println(p,q,*p,*x)//0xc0000b2008 0xc0000b2010 0 0 *p=2 *x = 4 fmt.Println(*p,*x)//2 4
Using the new function omits creating a pointer to a non anonymous variable as a new temporary variable, which is simpler than the above comparison: in addition, we can use new(T) in the expression, that is, the new function is a syntax sugar rather than a new basic concept. The following two functions actually implement the same function, And each call creates a new value
func main() { p := newInt1() q := newInt2() fmt.Println(p,q)//0xc00001e090 0xc00001e098 } func newInt1() *int{ return new(int) } func newInt2() *int{ var dummy int return &dummy }
The new function is rarely used. It is only a predefined function, not a keyword. We can redefine the new name to another type. In the following functions, because new has been defined as int type, we can't use the built-in new function inside the delta function
func delta(old, new int) int {return new - old}
2.3.4 life cycle of variables
The life cycle of variables declared at the package level is consistent with the whole Chen Xu operation cycle. Local variables are created from its creation until they are no longer referenced, and then the storage space of variables may be recycled. Function parameters and return values are local variables. They are created every time a function is called. You can see the previous examples
So how does the Go language's automatic garbage collector know when a variable can be recycled? Let's ignore the technical details and introduce the idea: start with each package level variable and each local variable of each currently running function, and traverse through the access path of the pointer or reference to see whether the variable can be found. If the path does not exist, it indicates that the variable is unreachable, that is, whether it exists or not will not affect the subsequent calculation results of the program
Because the effective period of a variable only depends on whether it is reachable, the life cycle of local variables within a loop iteration may exceed its scope. At the same time, local variables may still exist after the function returns
The compiler will automatically choose whether to allocate storage space for local variables on the stack or heap, but this has nothing to do with the way variables are declared with var or new
var global *int func f() { var x int x= 1 global = &x } func g() { y :=new(int) *y = 1 }
The x variable in the f function must be allocated on the heap because it can still be found through the package level global variable after the function exits. Although it is internally defined, it escapes. On the contrary, the variable * y in the g function can be recycled immediately. Escape variables do not mean that the program will be incorrect, but will need to allocate additional memory, which will have a slight impact on the performance optimization of the program
Therefore, although the automatic garbage collection mechanism of Go language is a great help to write code, it does not mean that you don't need to consider memory at all. This should be noted after understanding the declaration cycle of variables
2.4 assignment
Assignment is to update the value of a variable. The simplest assignment statement is that the variable is on the left and the expression of the new value is on the right
x = 1 *p = true person.name = "bob" count[x] = count[x]*scale
The compound operation of specific binary arithmetic operators and assignment statements has a brief form. For example, the last statement above can be written as
count[x] *= scale
Numeric variables can also support + + and –, while expressions such as x = i + + are wrong
2.4.1 tuple assignment
Tuple assignment allows you to update the values of multiple variables at the same time (in fact, we have covered this in the previous example)
x,y,z = 2,3,5 x, y = y, x a[i], a[j] = a[j], a[i] //greatest common factor func gcd(x,y int) int{ for y != 0{ x,y = y, x%y } return x } //Calculate Fibonacci sequence func fib(n int) int{ x,y := 0,1 for i:=0;i<n;i++ { x,y = y, x+y } return x }
When assigning the return value of a function to a variable, there are usually multiple return values (in many cases, an operation success value and an error value)
v,ok = m[key]//map lookup v,ok = x.(T)//Type Asserts v,ok = <-ch//Pain to receive
If the function returns multiple values, but we don't need them, we can use_ discard
-, err = io.Copy(dst, src) -, ok = x.(T)
2.4.2 assignability
The assignment statement is an explicit assignment form, but there are many implicit assignment behaviors in the program:
The function call assigns the value of the call parameter to the function parameter variable
A return statement implicitly assigns the value of the return operation to the result variable
A literal of a compound type also produces an assignment behavior
medals := []string{ "gold","sliver","bronze"} medals[0] = "gold" medals[1] = "sliver" medals[2] = "bronze"
Although the elements of map and chan are not ordinary variables, they also have similar implicit assignment behavior
Whether the assignment is implicit or explicit, the variable on the left and the final value on the right of the assignment statement must have the same data type
2.5 type
The type of variable or expression defines the attribute characteristics of the corresponding stored value, such as the storage size of values in memory, how they are expressed internally, whether some operators are supported, and their own associated method sets
Different programs may have the same internal structure, but they represent completely different concepts, especially variables
The declaration of type is generally at the package level, and the capitalized initial of type means that external packages can also be used
type Type name underlying type
type Celsius float64 type Fahrenheit float64 const ( AbsoluteZeroC Celsius = -273.15 FreezingC Celsius = 0 BoilingC Celsius = 100 ) func CToF(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) } func FToC(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) }
Although the basic types of Celsius and Fahrenheit are the same, they are different data types, so they can not be compared or mixed in an expression operation, but the two types with the same basic types can be explicitly converted to each other. The underlying data type determines the internal structure and expression method, and also determines whether they can support built-in operators like the underlying type, Undertake the above case:
func main() { var c Celsius var f Fahrenheit fmt.Println(c == 0)//true fmt.Println(f > 0)//false fmt.Println(c==f)//mismatched types Celsius and Fahrenheit fmt.Println(c==Celsius(f))//true }
Naming types can make writing more convenient; A named type can also define new behaviors for the value of the type (these behaviors are represented as a set of functions associated with the type, which we call the type method set), such as:
func (c Celsius) String() string {return fmt.Sprintf("%g˚C",c)}
Many types define a String method that uses fmt package printing
c := FToC(212.0) fmt.Println(c.String())//100˚C fmt.Printf("%v\n",c)//100˚C
2.6 packages and documents
The concept of package in Go language is similar to that of library or module in other languages. The purpose is to support modularization, encapsulation, separate compilation and code reuse
Each package has a separate namespace
Packages allow us to hide internal implementation information by controlling which names are externally visible
Let's define a package whose function is to realize temperature conversion
Package name: tempconv. The package contains two. go source files with the following contents
package tempconv import "fmt" type Celsius float64 type Fahrenheit float64 const ( AbsoluteZeroC Celsius = -273.15 FreezingC Celsius = 0 BoilingC Celsius = 100 ) func (c Celsius) String() string { return fmt.Sprintf("%g˚C", c) } func (f Fahrenheit) String() string { return fmt.Sprintf("%g˚F", f) }
func CToF(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) } func FToC(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) }
Call in main function
package main import ( "awesomeProject/src/tempconv"//Here you can see the path of the package we defined "fmt" ) func main() { fmt.Printf("Brrrr! %v\n", tempconv.AbsoluteZeroC)//Brrrr! - two hundred and seventy-three point one five ˚ C / / casually call a constant under the tempconv package }
2.6.1 import package
In Go language programs, each package has a globally unique import path. A string similar to "awesomeProject/src/tempconv" corresponds to the import path of the package. Each package has a name, which has been specified at the time of declaration
2.6.2 package initialization
Package initialization first solves the dependency order of package level variables, and then initializes in the order of package level variable declaration
If the package contains multiple. Go source files, they will be initialized in the order they are sent to the compiler. The go Language Builder will first sort the. Go files by file name, and then call the compiler in turn
For variables that live at the package level, if there are initialization expressions, they are initialized with expressions, and some have no initialization expressions. For example, initialization of some table data is not a simple assignment process. In this case, we can use a special init initialization function to simplify the initialization work, and each file can contain multiple init functions
func init() {/* ... */}
Except that such init functions cannot be called or referenced, other behaviors are similar to ordinary functions
package popcount var pc [256]byte func init() { for i := range pc { pc[i] = pc[i/2] + byte(i&1) } } func PopCount(x uint64) int { return int(pc[byte(x>>(0*8))] + pc[byte(x>>(1*8))] + pc[byte(x>>(2*8))] + pc[byte(x>>(3*8))] + pc[byte(x>>(4*8))] + pc[byte(x>>(5*8))] + pc[byte(x>>(6*8))] + pc[byte(x>>(7*8))]) }
For initialization that requires complex processing such as pc, the initialization logic can be wrapped as an anonymous function, as follows
var pc [256]byte = func() (pc [256]byte) { for i := range pc { pc[i] = pc[i/2] + byte(i&1) } return }()
2.7 scope
The scope is different from the life cycle. It is a code text area
A syntax block is a series of statements contained in curly braces whose internal declarations cannot be accessed externally. Some declarations are not explicitly wrapped in curly braces in the code, which is called lexical block. For global code, there is an overall lexical block, which is called global lexical block; For each package; Each for, if and switch statement also corresponds to lexical blocks. Each select or switch branch also has independent syntax blocks, including explicitly written lexical blocks
The built-in types, functions and constants are in the global scope, and the control flow labels: break, continue and goto are the function level scope
A program may contain multiple declarations of the same name, but it doesn't matter as long as they are in different lexical domains
The compiler will first look up the innermost lexical domain like the global scope. The internal declaration will shield the external declaration so that it cannot be accessed. The lexical domain can also be deeply nested
The following three XS are declared in different lexical domains, one in the function lexical domain, one in the initialization lexical domain, and one in the cyclic lexical domain
func main(){ x := "hello" for _, x:= range x { x:= x+ 'A' - 'a' fmt.Printf("%c",x) } }
if and switch statements also create implicit lexical domains in the condition section
At this point, we have seen how packages, files, declarations and statements express a program structure. Next, we will explore data structures