Go Language Programming Notes 2: Basics

Go Language Programming Notes 2: Basics

Source: php.cn

style of language

Go language is derived from C language, so the language style is very similar to C language. The difference is that go is quite radical in language formatting.

Most notably, the Go language has strict requirements for indentation and line feed. In addition, it is also very strict in variable use and package import.

In Python, if unused packages are referenced or variables are not used, the execution of the program will not be affected, such as:

from typing import NamedTuple

message = 'How are you!'
print('hello world')

# hello world

The package NamedTuple and the variable message are unnecessary, but they will not prevent the normal execution of the program. Of course, it may cause performance waste. Excellent IDE will also remind you that these parts can be deleted through color highlighting. However, most programming languages (such as C++/Java/PHP, etc.) do not force developers to eliminate unnecessary references or variables, but Go language will:

package main

import "fmt"

func main() {
	message := "How are you!"
	fmt.Printf("Hello World")
}

// Build Error: go build -o C:\Users\70748\AppData\Local\Temp\__debug_bin2433955485.exe -gcflags all=-N -l .\hello.go
// # command-line-arguments
// .\hello.go:6:2: message declared but not used (exit status 2)

Although the message variable is declared and initialized, it is not really used in subsequent programs, so it is invalid code. Here, the compiler will directly prompt message declared but not used, and then interrupt the compilation process. In fact, invalid imports can have the same consequences.

However, there is no need for developers to worry about importing. Go provides an official automatic import tool, which will be automatically called by ides such as VSC to import required packages or delete unnecessary packages.

In this regard, Go language is quite extreme, and its advantages and disadvantages are equally obvious.

The advantage is to ensure the cleanness and efficiency of the code, eliminate unnecessary invalid code, find some unnecessary bug s in the compilation stage, and maintain a consistent code style, so that the Go language source code has high readability. The disadvantage is that it stifles the personality of the code, and some programmers may not be used to it after the first contact.

variable

statement

In Go language, there are many ways to declare variables, and the most common form is the following:

package main

func main() {
	var varInt int               //integer
	var varFloat float64         //float 
	var varArray [3]int          //integer array 
	var varMap map[string]bool   //mapping
	var varBool bool             //Boolean
	var varSlice []int           //section
	var varStruct struct{}       //structural morphology
	var varInterface interface{} //Interface
	var varChannel chan int      //passageway
}

This code does not really execute because the Go language stipulates that variables that have never been used cannot be declared.

As shown above, you can declare a variable using var + variable name + variable type.

Like some modern languages, Go will "intelligently" automatically initialize after variable declaration, which I call "default initialization". The initialization results will vary depending on the variable type. For details, please read the relevant contents in the next section "type".

Of course, if you need to give an initial value while creating a variable, you can initialize it while declaring:

package main

import "fmt"

type people struct {
	Name string
	Age  int
}

func (*people) string() string {
	return fmt.Sprintf("%s is %d years old", people.Name, people.Age)
}

type stringer interface {
	string() string
}

func main() {
	var varInt int = 1                                                          //integer
	var varFloat float64 = 1.5                                                  //float 
	var varArray [3]int = [...]int{1, 2, 3}                                     //integer array 
	var varMap map[string]bool = map[string]bool{"apple": true, "xiaomi": true} //mapping
	var varBool bool = true                                                     //Boolean
	var varSlice []int = []int{1, 2, 3}                                         //section
	var varStruct people = people{Name: "lalala", Age: 11}                      //structural morphology
	var varInterface stringer = &people{Name: "lalala", Age: 11}                //Interface
	var varChannel chan int = make(chan int)                                    //passageway
}

If you initialize while declaring, you can omit the type after the variable, such as:

var varInt = 1  

At this time, the compile time will automatically create variables according to the type of the expression result on the right of the assignment symbol = this mechanism is called "type inference".

In addition, on the basis of type inference, there is a common way to write "short assignment" in Go language:

varInt := 1

In this way, even the var keyword is omitted, but the assignment symbol = needs to be replaced with: =.

: = in Python, there is a very unique name - "walrus operator", because the symbol looks like a walrus's small eyes + long teeth.

Although Go programming uses a lot of short assignment to declare variables, especially when you don't need to care about variable types. However, it should be noted that if the exact type is required, it is still better to declare variables in the traditional way.

type

Like other languages, the variable types in Go language can be roughly divided into two categories. One is the basic type, or it can also be called "atomic type", and the other is the composite type composed of atomic types.

The basic types include int, float64, string, run, etc., and the composite types include array, structure, slice, etc.

But in fact, this distinction method is of little use, because in Go language, all types can add methods, even basic types. So there may not be much difference in the way they are used. Instead, another classification method is more important: value passing type and reference type.

Value passing type refers to the type of variable passed by copying the value of the variable when assigning and calling the function to pass parameters. The reference type is easy to understand, that is, when assigning and passing parameters, what is passed is not a value, but a "variable reference", more accurately, a pointer to a real variable.

These two kinds of variables are very different in initialization and function parameter call.

Value transfer types include integer, floating point, string, bool, array, structure, etc.

Reference types include: slice, map, channel, interface, etc.

The edge slice here is actually very special. Although it appears to be a reference type, it is actually different from other reference types, which is determined by its special structure. This will be described in detail later when introducing slices.

The first difference between the value transfer type and the reference type is the default initialization. When only declaring no initialization, the value transfer type will be initialized to 0 by default. For example, integer type will be initialized to 0, floating point type will be initialized to 0.0, bool will be initialized to false, etc.

Here's a practical demonstration:

package main

import "fmt"

func main() {
	var varInt int
	var varFloat float64
	var varString string
	var varBool bool
	var varArray [3]int
	var varStruct struct{}
	fmt.Println(varInt)
	fmt.Println(varFloat)
	fmt.Println(varString)
	fmt.Println(varBool)
	fmt.Println(varArray)
	fmt.Println(varStruct)
}

// 0
// 0

// false
// [0 0 0]
// {}

The output here illustrates the above point. In addition, the writing of var varStruct struct {} looks a little strange. In fact, it declares a structure variable with an anonymous empty structure struct {}, and the initialization value of the structure is an empty structure, with the corresponding literal amount of {}.

The reference type is different from the value passing type. The default initialization value is nil, which is not difficult to understand. People familiar with C + + should know that the value of null pointer is null, and nil is written in Go language.

Let's look at the actual test results:

package main

import (
	"fmt"
	"go-notebook/ch2/formater"
)

func main() {
	var varSlice []int
	formater.PrintVariable(varSlice)
	fmt.Println(varSlice == nil)
	var varMap map[string]bool
	formater.PrintVariable(varMap)
	fmt.Println(varMap == nil)
	var varChannel chan int
	formater.PrintVariable(varChannel)
	fmt.Println(varChannel == nil)
	var varInterface interface{}
	formater.PrintVariable(varInterface)
	fmt.Println(varInterface == nil)
}

// []int(nil) []int []
// true
// map[string]bool(nil) map[string]bool map[]
// true
// (chan int)(nil) chan int <nil>
// true
// <nil> <nil> <nil>
// true

In order to correctly observe the nil value of the reference type, I write an auxiliary function here:

package formater

import "fmt"

func PrintVariable(variable interface{}) {
	fmt.Printf("%#v %T %v\n", variable, variable, variable)
}

Here, with the help of formatted output, format parameters%#v,% T,% v are used to output the real value, variable type and string output value in Go language.

For more formatting parameters that can be used by the Printf function, you can read Go fmt.Sprintf format string.

It can be seen that the default initialized values of these reference variables are nil, but the manifestations are different. For example, [] int(nil) can be regarded as converting nil into slice type.

In addition, for the same reason, reference variables can naturally be compared with nil. In the above example, I also tested and the results are true.

In addition to the difference in default initialization, the more important difference between the two is the difference after passing as a parameter:

package main

import "fmt"

func main() {
	var varArray = [...]int{1, 2, 3}
	var varSlice = []int{1, 2, 3}
	changeArray(varArray)
	changeSlice(varSlice)
	fmt.Println(varArray)
	fmt.Println(varSlice)
}

func changeArray(array [3]int) {
	array[0] = 99
	fmt.Println("after change:", array)
}

func changeSlice(slice []int) {
	slice[0] = 99
}

// after change: [99 2 3]
// [1 2 3]
// [99 2 3]

In the above example, the changeArray function cannot change the value in the external varArray variable, but the changeSlice function can, because the former passes an array, which is a value passing type, and the latter passes a slice, which is a reference type. Of course, you can also modify it to pass the pointer of the array rather than the array itself, so you can also modify the contents of the array.

In fact, the slice contains an array pointer.

I hope I can clarify the difference between value type and reference type through these examples, which is a place that programmers who turn to other languages will be confused.

Custom type

In Go language, you can customize a new variable type by using the type keyword:

package main

import "fmt"

type Celsius float64 //Centigrade temperature
func (c Celsius) String() string {
	return fmt.Sprintf("%.1fC", c)
}

type Fahrenheit float64 //fahrenheit
func (f Fahrenheit) String() string {
	return fmt.Sprintf("%.1fF", f)
}

func main() {
	zero := Celsius(0)
	fmt.Println(zero)
	fmt.Println(changeC2F(zero))
	boil := Celsius(100)
	fmt.Println(boil)
	fmt.Println(changeC2F(boil))
}

func changeC2F(c Celsius) Fahrenheit {
	return Fahrenheit(9*c/5 + 32)
}

func changeF2C(f Fahrenheit) Celsius {
	c := Celsius(5 * (f - 32) / 9)
	return c
}

In this example, two new types are defined: Celsius and Fahrenheit. The underlying type of both is float64. In addition, we also define two conversion functions changeC2F and changeF2C to convert two types of variables.

In addition, in order to format the output conveniently, a String method is defined to meet the fmt.Stringer interface. The contents of the method and interface will be described in the following notes.

For more information about Celsius and Fahrenheit temperatures, read How to convert Fahrenheit temperature to Celsius temperature.

More than basic types can be used to define new types. In fact, we define structures and interfaces in a similar way, but we define more content.

Scope

The scope of a variable is very important in the Go language, because when accessing a variable with a variable name, the Go compiler will first retrieve whether there is a declaration of the variable with that name in the current scope. If not, it will retrieve whether there is a declaration of the variable in the scope of the outer layer. If not, it will continue to retrieve up to the outermost package scope. The only limitation of declaring a variable is that no variable with the same name has been declared in the current scope.

In general, developers with strong programming experience in typed languages should not encounter scope related problems, but some cases are subtle and special, such as the following code:

package main

import "fmt"

func main() {
	var numbers []*int
	for i := 0; i < 10; i++ {
		func() {
			numbers = append(numbers, &i)
		}()
	}
	for _, number := range numbers {
		fmt.Printf("%d ", *number)
	}
}

// 10 10 10 10 10 10 10 10 10 10

func() {...} () is written to define and execute an anonymous function, which can access the outer variables. Therefore, the pointer of the loop variable i is added to the slice numbers through the anonymous function, and the final output result is 10 10. In fact, the 10 int pointers in the slice point to a loop variable i, which is 10 after the execution of the for loop, So the output is 10.

If you modify it slightly here:

package main

import "fmt"

func main() {
	var numbers []*int
	for i := 0; i < 10; i++ {
		i := i
		func() {
			numbers = append(numbers, &i)
		}()
	}
	for _, number := range numbers {
		fmt.Printf("%d ", *number)
	}
}

// 0 1 2 3 4 5 6 7 8 9

Here, just add a line I: = I to the loop body, and the result is very different.

I believe that many developers will be confused at first sight when they see a similar writing method. Why does the same variable assign its own writing method? Moreover, this variable is a declared circular variable.

In fact, the underlying reason here is that the for statement will be divided into three different variable scopes in Go language, namely:

  1. Scope outside the for statement.
  2. The scope of the for statement itself, i.e. for I: = 0; i < 10; I + + is the scope of this code. In this example, the loop variable I is in this scope. Of course, the scope is included in the external scope.
  3. The scope of the loop body, that is, the part contained in {...} in the for exp {...} statement, which is contained in the scope of the for statement itself.

If you understand the scope distinction of for, you can understand the statement i:=i above. The right side of the assignment statement is actually the loop variable i in the scope of the for statement itself, while the left side is the local variable i in the loop body, which is a newly declared variable.

The rule for anonymous functions to obtain external variable i adopts the "proximity principle". In this example, it is natural to obtain variable i in the loop body. In each iteration, the compiler will press the code in the loop body into the code stack of different levels, and the loop variables in the loop body are naturally in different areas. In short, in each iteration, the loop body variable i is newly created, with different variables, and the final result will be 0,1,2,3... So.

In C or C + +, the above writing method is not feasible, because local variables are static, created in the stack with the code, and will be released from the stack with the end of the iteration. Therefore, it is impossible to record the output after the pointer of the local variable in the loop body, because all variables have been released at that time. However, the Go language has a feature that it creates local variables in the heap or stack as needed, such as the loop body variable i in the above example. Because it is used by anonymous functions and associated with external numbers slices, the compiler will create this variable in the heap rather than the stack during compilation, so there will be no problem of being released after the loop ends. This phenomenon is called variable escape in Go language. The escaped variable is located in the heap. Like the garbage collection mechanism in Java, it uses technologies such as reference counting to track whether it can be garbage collected. If it should be collected, the garbage collector of Go language will automatically collect it.

In addition to the for statement, the if statement is similar. It will be divided into three different scopes, which will not be explained here.

The above is the whole content of this note. Originally, I planned to introduce the contents of functions together. I found that there are a lot of contents introduced. That's it. Thank you for reading.

Previous contents

Tags: Go Back-end

Posted on Sat, 06 Nov 2021 10:11:47 -0400 by seanrock