A preliminary study of Go language: from basics to practical use

1.Go overview

A program is an ordered combination of computer instructions. Program = algorithm + data structure. Any program can be implemented by combining modules through three basic control structures (sequence, branch, loop).

Go (also known as Golang) is an open source programming language developed by Google. The design goal is to make programming easier, more efficient and more reliable. Go aims to provide high performance, concise and easy-to-understand syntax. It combines the speed and performance of traditional compiled languages ​​with the ease of use and convenience of dynamically typed languages.
Go language features:

1. Static compilation

2. Less is more, the syntax is concise and simple, and it is highly readable.

3. Native support for concurrent programming

4. Non-intrusive interface to Duck model

5. Emphasize combination, which is better than inheritance

6. Supports cross-compilation of multiple operating systems and architectures. HeretargetOS is the target operating system (such as windows, linux, darwin, etc.), targetArchitecture is the target architecture (such as amd64, arm, 386, etc.).

GOOS=targetOS GOARCH=targetArchitecture go build
GOOS=windows GOARCH=amd64 go build

7. Make extensive use of interfaces and built-in functions to improve code reuse

8. Supports the mutual calling mechanism (CGO) with C language. Go uses the C package to call C functions, and uses special types to handle the communication between Go and C. The data passed. Here's a simple example showing how to call a C function in Go:

#include <stdio.h>

void helloFromC() {
    
    
    printf("Hello from C!\n");
}
package main

/*
#cgo CFLAGS: -g -Wall
void helloFromC();
*/
import "C"

func main() {
    
    
    C.helloFromC()
}

9. Precise dependencies and shorten compilation time through incremental compilation, parallel compilation and caching compilation results

Basic commands of Go language:

2.Go basic syntax

There are 25 keywords in Go language, which is a statically strongly typed language.

Strong type: The compiler will confirm the type of each variable, and incorrect use will cause an error;

static: only supports automatic type inference at compile time

Insert image description here

For conditional statements in Go language, () is not required after the if judgment. At the same time, if can be used with an initialization substatement; it is separated from the condition. At the same time, Go does not support the ternary operator

/*
if SimpleStmt;Expression {
	statement
	......
}
*/
if i:=10;i>8 {
    
    
    //条件语句
}

Switch statement description:

Note: The math.Floor(num) function is used to return the smallest integer less than num

At the same time, switch is a lazy evaluation, and the expression is calculated only when evaluation is needed, thereby reducing consumption and improving performance.
Insert image description here

for loop:

1. The loop statement of GO only has for, not while/do while
2. There is no need to add ( ) after the for statement
3.for When omitting any of the three parts of the statement, the semicolon cannot be omitted
4. When only conditional judgment is left, the semicolon is not needed (equivalent to the while statement)
5. Omit everything and it becomes an infinite loop

//while:
for experssion {
    
    
    
}
//无限循环
for {
    
    
    if state {
    
    
        break
    }
}

goto can work more closely with labels and can replace break to jump out of multiple loops

Manual sorting

Handwritten implementation of bubble sort, the Go code is as follows:

It should be noted here that passing slices as parameters is passed by reference!

func bubbleSort(nums []int){
    
    
    n:=len(nums)
    // 这里i是定义排序好的数量
    for i:=0;i<n-1;i++ {
    
    
        // 每次排序都是从第一个元素开始冒泡
        for j:=0;j<n-1-i;j++ {
    
    
            if nums[j]>nums[j+1] {
    
    
                nums[j+1],nums[j]=nums[j],nums[j+1]
            }
        }
    }
}

To implement insertion sort by hand, the Go code is as follows:

func insertSort(nums []int){
    
    
    n:=len(nums)
    // 从无序组第二个元素开始依次插入有序组中
    for i:=1;i<n;i++{
    
    
        key:=nums[i]
        j:=i-1
        for j>=0 && key<nums[j]{
    
    
            nums[j+1]=nums[j]
            j--
        }
        nums[j+1]=key
    }
}

Manually implement quick sorting, the specific Go code is as follows

func quickSort(nums []int){
    
    
    n:=len(nums)
    if n<2 {
    
    
        return 
    }
    // 定义基准线
    pivot:=nums[0]
    low,high:=0,n-1
    for low<=high {
    
    
        if nums[low]<=pivot{
    
    
            low++
        }else{
    
    
            nums[low],nums[high]=nums[high],nums[low]
            high--
        }
    }
    // 交换基准元素位置
    nums[0],nums[high]=nums[high],nums[0]
    // 递归排序左右子数组
    quickSort(nums[:high])
    quickSort(nums[high+1:])
}

3.Basic data types and operations

The basic data types in Go language are: integer, floating point, complex number, Boolean, character, string and error type.

You can use the reflect.TypeOf function to view the type name

1.Basic type

1. Integer type

Integers can be divided into:signed bits and unsigned bits;

Integers can be divided according to the number of digits: int int8 int 16 int32 int32 int64

Note here:Different integer types cannot be directly compared or operated directly

2. Floating point numbers

Floating point numbers mainly include float32 and float64

All functions in the standard library math package use float64
Insert image description here

3.Plural

Complex numbers are represented by two floating point numbers, a real part and an imaginary part.

There are two complex number types, complex64 (composed of two float32) and complex128 (composed of two float64)

There are three built-in complex number handling functions

complex(float,float) creates a complex number

real() gets the real part

image() gets the imaginary part

package main

import (
	"fmt"
)

func main() {
    
    
	// 创建复数
	var comp1 complex64 = complex(2, 3)     // 实部为2,虚部为3
	comp2 := complex(4.5, 7.1)             // 使用默认类型complex128

	// 输出复数
	fmt.Println("Complex 1:", comp1)
	fmt.Println("Complex 2:", comp2)

	// 访问实部和虚部
	fmt.Println("Real part of Complex 1:", real(comp1))     // 输出实部
	fmt.Println("Imaginary part of Complex 1:", imag(comp1)) // 输出虚部
}
4. Boolean type

Boolean values ​​mainly include true and false, and the type length is 1byte

Boolean types cannot be assigned to other types and do not support type conversion.

The Boolean type here does not support using 0 and 1 to represent true and false.

The conditional part of if and for statements must be a boolean value or expression

2.Operator

Operators mainly include arithmetic operators, relational operators, logical operators, assignment operators and bitwise operators

1. Arithmetic operators

Arithmetic operators mainly include addition, subtraction, multiplication and division, modulo, auto-increment, and auto-decrement.

**Note: **Go language only supports variable ++ for auto-increment and does not support ++ variables. The same goes for auto-decrement.

2. Relational operators

Relational operators mainly include

==
!=
>
<
>=
<=

Note: Since the Boolean type does not support conversion to integer types, the syntax of continuous inequalities, such as x<y<z, is wrong!

3. Logical operators

Logical operators mainly include negation! , and &&, or ||

4. Assignment operator

Insert image description here

5. Bit operators

Bit operators include

<< 左移 相当于乘以2
>> 右移 相当于除以2
&  位与
|  位或
^ 异或

Bitwise operations only work on integers, which is a low-level operation and has high efficiency!
Insert image description here

4. Collection data type

There are three main types of collections in Go language, namely Array, Slice and Map.

1.Array

An array is a collection of similar elements. After an array variable is declared, its element type and array length are immutable.

Array declaration:

// 只声明未赋值H
var arr1 [5]int
// 直接赋值
arr2:=[3]int{
    
    1,2,3}
// 数组长度由初始化数量确定
arr3:=[...]int{
    
    1,2,3} //...不可省略
// 对含有下标的元素赋初值 其余元素保持零值
arr4:=[4]{
    
    0:99,3:100}

Array copy:

Copying between array variables will copy the entire array (value copy)

	a := [...]string{
    
    "USA", "China", "India", "Germany"}
    b := a 
    b[0] = "Singapore"
    fmt.Println("a is ", a)
    fmt.Println("b is ", b)   
    //a is [USA China India Germany]  
    //b is [Singapore China India Germany]

Array parameter passing:

With the array copy type, only the actual parameters are copied to the formal parameters, and they are destroyed when the function call is completed. The two are independent of each other, and the efficiency is low when passing large arrays!

func changeLocal(num [5]int) {
    
    
    num[0] = 55
    fmt.Println("inside function ", num)
}

func main() {
    
    
    num := [...]int{
    
    5, 6, 7, 8, 8}
    fmt.Println("before passing to function ", num)
    changeLocal(num) //num is passed by value
    fmt.Println("after passing to function ", num)
}
//before passing to function  [5 6 7 8 8]  
//inside function  [55 6 7 8 8]  
//after passing to function  [5 6 7 8 8] 

Array traversal:

Array traversal can use for loop traversal or range traversal

// for循环
 	a := [...]float64{
    
    67.7, 89.8, 21, 78}
    for i := 0; i < len(a); i++ {
    
     
        fmt.Printf("%d th element of a is %.2f\n", i, a[i])
    }
// range遍历
	a := [...]float64{
    
    67.7, 89.8, 21, 78}
    for i, v := range a {
    
     // 第一个参数为序号,第二个为变量
        fmt.Printf("%d the element of a is %.2f\n", i, v)
    }
    //0 the element of a is 67.70  
    //1 the element of a is 89.80  
    //2 the element of a is 21.00  
    //3 the element of a is 78.00

In addition, if only one of the two parameters of range traversal is used, an error will be reported. You can use _ placeholder to indicate that only one parameter is used.

Multidimensional Arrays:

	a := [3][2]string{
    
    
        {
    
    "lion", "tiger"},
        {
    
    "cat", "dog"},
        {
    
    "pigeon", "peacock"}, //此处,不可忽略,否则报错
    }
    for _, v1 := range a {
    
    
        for _, v2 := range v1 {
    
    
            fmt.Printf("%s ", v2)
        }
        fmt.Printf("\n")
    }

2. Slice

Since the fixed-length nature of arrays and value copying limit their use, slicing is provided, that is, array references with variable lengths are provided.

The size of the underlying array cannot be given when declaring a slice, otherwise it becomes an array declaration. At the same time, you can use the built-in function make to declare and initialize!

Slices are reference types and do not support the == operation

Create slices:

It should be noted here that if you specify the underlying array when creating a slice, once the slice changes, the underlying array elements will also change, because the slice is a reference to the original array. Therefore, multiple slices can share the same underlying array.

// 指定底层数组创建
 	a := [5]int{
    
    76, 77, 78, 79, 80}//底层数组
    s1 := a[0:4] // from a[0] to a[3]
    s2 := a[:4]  // from a[0] to a[3]
    s3 := a[2:5] // from a[2] to a[4]
    s4 := a[2:]  // from a[2] to a[4]
    fmt.Printf("%v\n%v\n%v\n%v", s1, s2, s3, s4)
    //[76 77 78 79]
    //[76 77 78 79]
    //[78 79 80]
    //[78 79 80]
// 同时创建数组和切片
//指定数组大小,只创建数组    
c := [3]int{
    
    6, 7, 8}
//不指定数组大小,返回切片引用,底层数组匿名 
d := []int{
    
    6, 7, 8}  
//用...推断数组大小,只创建数组 
e := [...]int{
    
    6, 7, 8} 

The built-in function len() returns the current length of the slice
The built-in function cap() returns the capacity of the underlying array of the slice

Slices are dynamically added:

The built-in function append() dynamically expands the slice and directly overwrites the underlying array elements within the capacity of the underlying array.

package main

import "fmt"

func main() {
    
    
    arr := [7]int{
    
    9, 8, 7, 6, 5, 4, 3}
    sli := arr[1:3]
    sli = append(sli, 20) // 增加一个20,切片容量扩展一倍
    fmt.Printf("%v\n", arr) //[9 8 7 20 5 4 3]
    fmt.Printf("%v\n", sli) //[8 7 20]
}

When the slice is dynamically increased, when the capacity of the underlying array is exceeded, the underlying array will be re-created and the data will be transferred
The slice will grow exponentially when the elements are less than 1000 and exceed 1000. , the growth rate is about 1.25

	cars := []string{
    
    "Ferrari", "Honda", "Ford"}
    fmt.Println("cars:", cars, "length", len(cars), "capacity", cap(cars))  
    //cars: [Ferrari Honda Ford] length 3 capacity 3
    fmt.Printf("%x\n", &cars[0])
    //c000080330
    cars = append(cars, "Toyota")
    fmt.Println("cars:", cars, "length", len(cars), "capacity", cap(cars))
    //cars: [Ferrari Honda Ford Toyota] length 4  capacity 6 //why 6
    fmt.Printf("%x\n", &cars[0])    
    //c0000a4000   //Why

Slice merging:

The built-in function append() also supports the merging of slices. Use the... operator to take out all elements of the corresponding slice.

	veggies := []string{
    
    "potatoes", "tomatoes", "brinjal"}
    fruits := []string{
    
    "oranges", "apples"}
    food := append(veggies, fruits...) //... 不可忽略
    fmt.Println("food:", food)
    //food: [potatoes tomatoes brinjal oranges apples]

Slice passing parameters:

When passing parameters to a function, what is copied is a copy of the structure, realizing transfer by reference.

func subtactOne(numbers []int) {
    
    
    for i := range numbers {
    
    
        numbers[i] -= 2
    }
}
func main() {
    
    
    nos := []int{
    
    8, 7, 6}
    fmt.Println("slice before function call", nos)
    //slice before function call [8 7 6]
    subtactOne(nos)
    fmt.Println("slice after function call", nos)
    //slice after function call [6 5 4]
}

Multidimensional slices:

Multidimensional slices are more flexible than multidimensional arrays, and the number of elements in each row does not have to be the same

 	pls := [][]string{
    
    
        {
    
    "C", "C++", "C#"},
        {
    
    "JavaScript"},
        {
    
    "Go", "Rust"},
    }
    for _, v1 := range pls {
    
    
        for _, v2 := range v1 {
    
    
            fmt.Printf("%s ", v2)
        }
        fmt.Printf("\n")
    }
    //C C++ C# 
    //JavaScript 
    //Go Rust 

3.Map

Map is used to store a series of unordered key-value pairs. It is a reference type and does not support == operations (except nil).

Map is not thread-safe and does not support concurrent writing

1.Initialization

Map zero value is not available, it only declares that it will not be initialized to nil value, no underlying storage space is allocated, and elements cannot be added
Elements can be added after initialization with literals or make function< /span>

	var m1 map[string]int
    fmt.Println(m1 == nil)
    //true
    //m1["a"] = 1 //error

    m2 := map[string]int{
    
    }
    fmt.Println(m2 == nil)
    //false
    m2["a"] = 1 //ok

    m3 := make(map[string]int)
    fmt.Println(m3 == nil)
    //false
    m3["a"] = 1 //ok
2. Assignment

Element assignment can be performed after Map initialization, or element assignment can be performed directly during Map initialization.

	personSalary := make(map[string]int)
    personSalary["steve"] = 12000
    personSalary["jamie"] = 15000
    personSalary["mike"] = 9000
    //初始化时,直接赋值
    personSalary := map[string]int{
    
    
        "steve": 12000,
        "jamie": 15000,
    }

3. Element search

Map elements can actually return two values ​​when accessed through subscripts (the bottom layer is actually a function, Comma-ok method)
1. The corresponding value
2. Boolean value indicating whether the corresponding key exists

	personSalary := map[string]int{
    
    
        "steve": 12000,
        "jamie": 15000,
    }
    value, ok := personSalary["joe"]
    if  ok  == true {
    
    
        fmt.Println("Salary of joe is", value)
    } else {
    
    
        fmt.Println("joe not found")
    }
4.Map element traversal

Map elements can be traversed using range, butthe order is not guaranteed

	personSalary := map[string]int{
    
    
        "steve": 12000,
        "jamie": 15000,
    }
    personSalary["mike"] = 9000
    for key, value := range personSalary {
    
    
        fmt.Printf("personSalary[%s] = %d\n", key, value)
    }
    //personSalary[mike] = 9000
    //personSalary[steve] = 12000
    // personSalary[jamie] = 15000
5.Map element deletion

Use the built-in function delete() to delete Map elements
1. The key exists and the corresponding element is deleted
2. The key does not exist and there is nothing happen

	personSalary := map[string]int{
    
    
        "steve": 12000,
        "jamie": 15000,
    }
    personSalary["mike"] = 9000
    fmt.Println("map before deletion", personSalary)
    delete(personSalary, "steve")
    fmt.Println("map after deletion", personSalary)
    //map before deletion map[jamie:15000 mike:9000 steve:12000]
    //map after deletion map[jamie:15000 mike:9000]

5.Go function

High function efficiency means high program efficiency. It is recommended to use more standard library functions.

1. Function definition

//语法格式
func funcName(paramList)(resultList) {
    
    
	coding ……
}
//paramList = input1 type1,input2 type2 ……
//resultList = output1 type1,output2 type2 ……
//多个相邻相同类型参数可以使用简写
func add(a, b int) int {
    
    
    return a + b
}

There can be multiple return values ​​in a Go function, and the return values ​​can have variable names and be visible in the function body.

**NOTE:** Function overloading is not supported as overloading is only occasionally useful but in practice leads to unsolvable problems and brittleness

2. Parameters (indefinite parameters)

Indefinite parameters, formal parameters are variable and uncertain
Indefinite parameter declaration syntax format: param … type

The formal parameters of indefinite parameters are slices within the function

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

The above-mentioned variable parameters have an indefinite number, but the types are the same. If you want to implement an indefinite number and an indefinite type at the same time, you need to implement it through the interface type interface{} as a parameter.

func printAll(vals ...interface{
    
    }) {
    
    
    for _, val := range vals {
    
    
        fmt.Println(val)
    }
}

3.Anonymous functions

Anonymous functions are equivalent to function literals. Anonymous functions can be used wherever functions can be used.

//匿名函数直接调用
func(a,b int )int{
    
    
    return a-b
}(5,4)
//匿名函数赋值给函数变量
var sum = func(a,b int )int{
    
    
    return a+b
}
//函数作为返回值
func getFun(op string) func(a,b int )int {
    
    
    return func(a,b int )int{
    
    
        return a+b
    }
}

4. Closure

Closure = function + reference environment, it is common to define an anonymous function inside a function, and the anonymous function accesses the scope of the external function that defines it

package main
import "fmt"
func main() {
    
    
    // 外部函数外的变量
    outsideVar := 10
    // 内部函数,形成闭包
    closureFunc := func() {
    
    
        fmt.Println(outsideVar) // 闭包函数内部访问外部变量
    }
    closureFunc() // 调用闭包函数
}

Function currying:

Function currying is to transform a function that receives multiple parameters into a function that receives a single parameter.

Function currying is the process of converting a multi-parameter function into a series of single-parameter functions. The result of this transformation is that the original function can be called through a series of functions with fewer parameters.

5. Delayed call (defer)

Go functions support defer for delayed calling

defer is similar to the final clause in OOP language exception handling, and is often used to ensure the recovery and release of system resources.

	defer Println("last")
    Println("main body")
    Println("first")
    //main body
    //first
    //last

When using the defer function, the current actual parameter values ​​will be passed to the formal parameters. Even if the subsequent actual parameters change, the function result will not be affected!

	a := 5
    defer fmt.Println(defer 注册函数时的a值", a)
    a = 10
    fmt.Println(“普通函数的a值", a)
    //普通函数的a值 10
    //defer 注册函数时的a值 5

In addition, when using multiple defers, these defer calls are executed in first-in-last-out (FILO) order before the function returns!

    name := "Naveen"
    fmt.Printf("Original String: %s\n", string(name))
    fmt.Printf("Reversed String: ")
    for _, v := range []rune(name) {
    
    
        defer fmt.Printf("%c", v)
    }
    //Original String: Naveen
    //Reversed String: neevaN

6. Recursive functions

Formally: an executing function calls itself (direct recursion).

Recursion cannot be called without limit because the stack space is limited
There must be a statement to complete the ultimate task in the recursion
The recursive call parameters gradually approach the end condition< /span>
The purpose of recursion is to simplify the design and make the program easier to read, but it is usually less efficient

6. Structure and methods

1. Structure

1. Structure definition

Structures unify different types of data that are intrinsically related into a whole and make them related to each other.

A structure is a collection of variables, which looks like an entity from the outside.

type Employee struct{
    
    
    firstName string
    lastName  string
    age       int
    salary    int
}
2. Labeled structure

In addition to the name and type, the fields in the structure can also have an optional tag.

The tag is a string attached to the field, used to describe the field information
The tag can also be modified according to key1:“value1” key2:“value2” key-value pairs to provide Encoding, decoding, ORM and other conversion assistance

You can use reflection to obtain each key-value pair in the structure tag.

package main

import (
	"fmt"
	"reflect"
)

type Person struct {
    
    
	Name string `json:"name" validate:"required"`
	Age  int    `json:"age" validate:"min=18"`
}

func main() {
    
    
	p := Person{
    
    Name: "Alice", Age: 25}
	// 获取结构体字段的标签信息
	t := reflect.TypeOf(p)
	for i := 0; i < t.NumField(); i++ {
    
    
		field := t.Field(i)
		fmt.Printf("Field: %s, Tag: %s\n", field.Name, field.Tag)
	}
}
3. Initialization of structure variables

1. You can use field names to initialize, so there is no need to order, and unspecified fields have zero values.

    emp1 := Employee{
    
    
        firstName: "Sam",
        age:       25,
        salary:    500,
        lastName:  “Anderson”, //逗号不能忽略
    }

2. Initialize with literals, set them all in the order in which field types are declared. If the order is incorrect or fields are missing, an error will be reported.

emp2 := Employee{
    
    "Thomas", "Paul", 29, 800}
4. Access and modify field values

1. Use structure variable.field

emp := Employee{
    
    "Thomas", "Paul", 29, 800}
fmt.Println(emp.age)

2. Use (*structure variable pointer).field

emp := &Employee{
    
    "Sam", "Anderson", 55, 6000}
fmt.Println("First Name:", (*emp).firstName)

3. Use structure variable pointer.field, not supported ->

emp := &Employee{
    
    "Sam", "Anderson", 55, 6000}
fmt.Println("First Name:", emp.firstName)
4.Anonymous fields

The field name can also be omitted in the structure field.The field name defaults to the corresponding data type name (the data type cannot be repeated)

	type Person struct {
    
    
        string
        int
    }
    p := Person{
    
    "Naveen", 50}
    p.int =60

2.Method

A method is an encapsulation of the behavior of a specific type, essentially a function bound to that type.

Methods in OO languages ​​usually have a hidden this or self pointer pointing to the object. Go exposes this hidden pointer, which is called a receiver.

func (t Type) funcName(paramList)(resultList)
func (t *Type) funcName(paramList)(resultList)
1. Method examples
type Employee struct {
    
    
    name     string
    salary   int
    currency string
}
//定义方法
func (e Employee) displaySalary() {
    
    
    fmt.Printf("Salary of %s is %s%d", e.name, e.currency, e.salary)
}
func main() {
    
    
    emp1 := Employee{
    
    
        name:     "Sam Adolf",
        salary:   5000,
        currency: "$",
    }
    emp1.displaySalary() 
}

In fact, the method can be implemented using equivalent functions, as shown below:

type Employee struct {
    
      
    name     string
    salary   int
    currency string
}
func displaySalary(e Employee) {
    
      
    fmt.Printf("Salary of %s is %s%d", e.name, e.currency, e.salary)
}
func main() {
    
      
    emp1 := Employee{
    
    
        name:     "Sam Adolf",
        salary:   5000,
        currency: "$",
    }
    displaySalary(emp1)
}

Since functions can do it, why do we need methods?

In the Go language, methods and equivalent functions can accomplish similar tasks. Although they can accomplish the same task, there are some differences and applicable scenarios between methods and functions,where methods are more suitable for specific types of operations and object-oriented programming.

GO functions cannot be overloaded, so different types cannot use functions with the same name, but different types of methods can have the same name
GO does not support classes, use structures instead of classes, and structure fields are used Encapsulate object properties, methods are used to encapsulate the object's behavior

In addition, methods are not exclusive to structures, all custom types can define methods

type myInt int //自定义类型

func (a *myInt) add(b myInt) myInt {
    
    
    return *a + b
}

num1 := myInt(5)
num2 := myInt(10)
sum := num1.add(num2)

7.Interface

The interface in Go language is a non-intrusive interface of the Duck model. Unlike traditional interfaces, the specific type of non-intrusive interface does not need to be explicitly declared. As long as its method set is a superset of the interface, it will be corresponding during compilation. check!

The GO interface only has method signatures, no data fields, and no function body code.

If the method set of a type is a superset of multiple interfaces, it implements multiple interfaces

1.Interface class definition

// 命令接口类型
type interfaceName interface{
    
    //接口类型命名通常以er为后缀
    methodName(paramList)(resultList)
    otherInterfaceName
}
// 匿名接口类型
interface{
    
    
    methodName(paramList)(resultList)
    otherInterfaceName
}

And if the method set in the anonymous interface is empty, that is, interface{} is an empty interface. All types implement the empty interface and can be assigned or passed to the empty interface.

2.Interface initialization

Only declare unassigned interface variables as nil
Initialization of interface variables requires binding the interface to a specific type instance
Uninitialized interface variables cannot Only the receiver of the method can assign a value to the interface variable by calling its method
. The value of the interface variable includes the value of the underlying type and the specific type

package main

import (
	"fmt"
)

// 接口定义
type Speaker interface {
    
    
	Speak() string
}

// 实现接口的结构体
type Dog struct{
    
    }

// Dog 结构体实现 Speak 方法
func (d Dog) Speak() string {
    
    
	return "Woof!"
}

// 创建接口的实例
func NewSpeaker() Speaker {
    
    
	return Dog{
    
    } // 返回一个 Dog 类型,它满足了 Speaker 接口
}

func main() {
    
    
	// 初始化接口并调用方法
	speaker := NewSpeaker()
	fmt.Println(speaker.Speak())
}

In addition, an interface can contain one or more interfaces, that is, nested interfaces

type ReadWrite interface {
    
    
    Read(b Buffer) bool
    Write(b Buffer) bool
}
type File interface{
    
    
    ReadWrite
    close() bool
}

3. Interface type assertion

Interface type assertion is used to determine whether a variable that implements an interface is of a certain type

If so, return the value of that type and true

If not, return zero value and false of this type

// interfaceName.(typeName)
    var a interface{
    
    } = 56
    v, ok := a.(int)
    fmt.Println(v, ok)
    //56 true
    var b interface{
    
    } = true
    v, ok = b.(int)
    fmt.Println(v, ok)
    //0 false

4. Interface type query

Interface type query uses the switch statement to determine the underlying type of the interface variable.

.(type) can only be used in switch expressions because the underlying type of a variable can only be determined by interface type assertion. Go can only determine whether the variable memory format matches a certain type and parse the value according to a certain type.

func findType(i interface{
    
    }) {
    
    
    switch i.(type) {
    
    //.(type)只能用于switch表达式
    case string:
        fmt.Printf("string and value is %s\n", i.(string))
    case int:
        fmt.Printf("int and value is %d\n", i.(int))
    default:
        fmt.Printf("Unknown type\n")
    }
}
findType(77)
findType(89.98)
// int and value is 77 
// Unknown typ

5.Stringer interface

StringerThe interface is an interface in the Go language. It contains only one method: String(), is used to return the string representation of this type< /span>. This interface is usually used to customize the type of string output format.

In Go language, if a type implements the Stringer interface, then you can use the printing method in the fmt package (such as ) to customize the output method of this type. Println or Sprintf

The following is the definition of the Stringer interface:

type Stringer interface {
    
    
    String() string
}

String()The method of the in order to customize the string output of this type. methods, Stringer interface can define their own interface returns a string. Types that implement the String()

The following is a simple example demonstrating how to use the Stringer interface:

package main
import (
    "fmt"
)
type Person struct {
    
    
    Name string
    Age  int
}
// 实现 Stringer 接口
func (p Person) String() string {
    
    
    return fmt.Sprintf("%s is %d years old", p.Name, p.Age)
}
func main() {
    
    
    person := Person{
    
    "Alice", 30}
    fmt.Println(person) // 输出: Alice is 30 years old
}

In this example, the Person type implements the Stringer interface and overrides the String() method, so that when we When using fmt.Println to print a variable of type Person, the String() method is called and the custom string format of the type is output.

Summary implements the type variable of the Stringer interface. When using the fmt.Println method to print the object, it can be output in a specified format, similar to overriding the toString method in Java. .

6.Sorter interface

The sort package of the standard library implements three methods to define sorting:

//Len()   反映元素个数的方法
//Less(i, j)  比较第 i 和 j 个元素
//Swap(i, j) 交换第 i 和 j 个元素
// 具体Sorter接口定义如下
type Sorter interface {
    
    
    Len() int
    Less(i, j int) bool
    Swap(i, j int)
}

Please implement bubble sorting based on the Sorter interface:

package main

import (
	"fmt"
)

type Sorter interface {
    
    
	Len() int
	Less(i, j int) bool
	Swap(i, j int)
}

type IntArray []int

func (arr IntArray) Len() int {
    
    
	return len(arr)
}

func (arr IntArray) Less(i, j int) bool {
    
    
	return arr[i] < arr[j]
}

func (arr IntArray) Swap(i, j int) {
    
    
	arr[i], arr[j] = arr[j], arr[i]
}

func BubbleSort(data Sorter) {
    
    
	n := data.Len()
	for i := 0; i < n-1; i++ {
    
    
		for j := 0; j < n-i-1; j++ {
    
    
			if data.Less(j+1, j) {
    
    
				data.Swap(j, j+1)
			}
		}
	}
}

func main() {
    
    
	array := IntArray{
    
    64, 34, 25, 12, 22, 11, 90}
	fmt.Println("Unsorted array:", array)

	BubbleSort(array)
	fmt.Println("Sorted array:", array)
}

7. Interface characteristics

The interface feature is referred to as dynamic and static combination.

Interface static characteristics:

Supports type checking at the compilation stage: when an interface type variable is assigned, the compiler will check whether the rvalue type implements all methods in the interface method set.

Interface dynamic characteristics:

That is: using empty interface variables can use different types of variable assignments

The true type of the value stored in the interface type variable at run time. For example: the dynamic type of interface variable i in var i interface{} = 13 is int.
can be assigned to different dynamic type variables at runtime, thus supporting runtime polymorphism.

8.Reflection

The most basic information of a variable is its type and value. Reflection can check the type and value of the variable whenthe program is running

Through reflection, you can obtain the field information of the structure variable, even the tag information of the structure field.

package main
import (
	"fmt"
	"reflect"
)
type Person struct {
    
    
	Id   int //首字母大写表示公开字段
	Name string
	Sex  string
}
func (this Person) Call() {
    
    
	fmt.Println("我正在打电话")
}
func getTypeAndValue(object interface{
    
    }) {
    
    
	//动态获取对象object的类型信息
	objectType := reflect.TypeOf(object)
	objectValue := reflect.ValueOf(object)
	fmt.Println("type =", objectType.Name())
	fmt.Println("type =", objectType, "value =", objectValue)
	// objectType.NumField() 获取字段的总数
	for i := 0; i < objectType.NumField(); i++ {
    
    
		field := objectType.Field(i)
		value := objectValue.Field(i)
		fmt.Printf("type %d = %v\n", i, field.Type)
		fmt.Printf("name %d = %v\n", i, field.Name)
		fmt.Printf("value %d = %v\n", i, value.Interface())
	}
	for i := 0; i < objectValue.NumMethod(); i++ {
    
    
		method := objectValue.Method(i)
		method.Call(nil)
	}
}
func main() {
    
    
	person := Person{
    
    1, "nancy", "mail"}
	getTypeAndValue(person)
}

8.Error handling

There is no exception mechanism in Go language, only error handling. Errors are handled through multiple return values ​​of functions.

Go language errors mainly include: compile-time errors, run-time errors and logic errors

Error handling in Go language

1.Can be processed, processed by returning an error from the function

2.cannot be processed, throw an error through panic, and exit the program

1.error interface

Implement the standard mode of error handling through the error interface, and automatically call the Error() function when printing an error.

type error interface{
    
    
    Error() string
}

The last return value of a function that may go wrong is an error type. Check whether the return value is nil. If so, handle the error. Otherwise, call it normally.

 	f, err := os.Open("/test.txt")
    if err != nil {
    
    
        fmt.Println(err)
        return
    }
    fmt.Println(f.Name(), "opened successfully")

2.if happy path principle

The "happy path" principle is a design concept in programming. Its guiding idea is to keep the main path of the function as "happy" as possible, that is, the main work or logic of the function can be completed as quickly as possible without being disturbed by unexpected situations.

An example is to pass reasonable error checking and returns,put the error handling logic at the beginning of the function, and put the main logic and processing Place it in the main part of the function. This allows you to exit the function early and return an error, but at the same time keep the main logic inside the function body, making the main logic as happy as possible.

func PerformTask(param int) (result int, err error) {
    
    
    // 错误检查放在前面
    if param < 0 {
    
    
        return 0, errors.New("param cannot be negative")
    }

    // 主逻辑放在主体内部
    // 这里是函数的主要逻辑,称为快乐路径
    result = param * 2
    return result, nil
}

3. Custom errors

error is a built-in interface type in the Go language. It has only one method Error(), which is used to return a string representation of error information.

The standard libraryerrors package provides functions for creating simple error messages.

package main

import (
	"errors"
	"fmt"
)

func divide(a, b int) (int, error) {
    
    
	if b == 0 {
    
    
		return 0, errors.New("division by zero")
	}
	return a / b, nil
}

func main() {
    
    
	result, err := divide(6, 2)
	if err != nil {
    
    
		fmt.Println("Error:", err)
	} else {
    
    
		fmt.Println("Result:", result)
	}

	result, err = divide(3, 0)
	if err != nil {
    
    
		fmt.Println("Error:", err)
	} else {
    
    
		fmt.Println("Result:", result)
	}
}

Additionally, you can create custom errors using the Errorf function of the fmt package!

package main

import (
	"fmt"
)
func someFunction() error {
    
    
	return fmt.Errorf("This is a more detailed error: %s", "specific error message")
}

func main() {
    
    
	err := someFunction()
	if err != nil {
    
    
		fmt.Println("Error:", err)
	}
}

When code duplication exists in multiple error handling locations, can use goto to centrally handle errors!

err := firstCheckError()
if err != nil {
    
    
    goto onExit
}
err = secondCheckError()
if err != nil {
    
    
    goto onExit
}
// 正常处理代码
onExit:
fmt.Println(err)
exitProcess()

4.panic

panic is one of the built-in functions in the Go language that is used to cause the program to abort when an unrecoverable error occurs. panic will stop the execution of the current function and propagate a panic signal to the caller, and then the program will be terminated.

Normally, panic is used to handle serious errors, such as array out-of-bounds, null pointer reference, etc. When it is called, the program will stop executing the current function and start executing the defer function, and then the program will crash with the error message generated by panic. When encountering panic, the normal flow of the program will be broken and the current task will not continue.

During the development process, try to avoid usingpanic. Instead, use error returns or other appropriate handling methods when it can be predicted and handled, because a>panic is unrecoverable and can easily cause program instability.

package main
import "fmt"
func someFunc() {
    
    
    // 模拟一个无法处理的错误
    err := someErrorOccurred()
    if err != nil {
    
    
        panic("An unexpected error occurred: " + err.Error())
    }
}
func main() {
    
    
    fmt.Println("Starting the program.")
    someFunc()
    fmt.Println("End of the program.")
}

Insert image description here

5.recover

In Go language, recover function is used to resume the execution of the program and recover from panic state (panic). recover will only take effect when called internally by the defer function.

Normally, recover is used in conjunction with defer to resume program execution if the program enters a panic state.

Here is an example of using recover to capture and handle panic conditions:

package main

import (
	"fmt"
)

func recoverDemo() {
    
    
	if r := recover(); r != nil {
    
    
		fmt.Println("Recovered:", r)
	}
}

func someFunc() {
    
    
	defer recoverDemo()

	// 模拟一个恐慌状态
	panic("Something went wrong!")
}

func main() {
    
    
	fmt.Println("Starting the program.")
	someFunc()
	fmt.Println("End of the program.")
}

In this example, the recoverDemo function is executed as a defer function in someFunc. When the someFunc function triggers a panic state, the function in recoverDemo captures the panic and prints out the error message. recover

Please note that recover functions are only valid within delayed functions. Calling recover in a non-deferred function has no effect, and the error message can only be captured when a panic condition occurs.

9. Concurrency

1. Process, thread, coroutine

A process is an independent unit for resource allocation and scheduling by the operating system when a program is running in memory
A thread is an execution entity of the process and an execution within the process. Path is the basic unit of CPU scheduling and dispatch. It is a basic unit that is smaller than a process and can run independently
Each process includes at least one thread, and the initial thread of each process is called Main thread, main thread terminates, process terminates
Coroutines are lightweight threads, and one thread can have multiple coroutines
Processes and threads are a>. Coroutines are not managed by the operating system kernel, but are completely controlled by the program, so there is no thread switching overhead. Compared with multi-threads, the greater the number, the more obvious the performance advantage of coroutines. The biggest advantage of coroutines is that they are lightweight and can easily create tens of thousands without causing system resource exhaustioncompiler level, coroutine is operating system level

2.goroutine

In the Go language, Goroutine is the basic unit of concurrent execution. They are lightweight threads in the Go runtime environment and are assigned to logical processors for execution by the Go scheduler. Goroutine operation does not depend on physical processors or operating system threads. Each logical processor (P) is responsible for running Goroutine, and multiple Ps can run on one physical processor (CPU).

3.Coroutine communication

Don't communicate via shared memory, share memory via communication

There are two common ways of communicating between coroutines:

1. Shared data: Many languages ​​use shared memory to synchronize program data and ensure that the program is executed in a logical manner. During program execution, a process or thread may lock shared data to prevent other processes or threads from modifying it. Overall programming complexity is high

2. Message mechanism: Each concurrent unit is an independent individual, and the data of multiple concurrent units is not shared. Data is synchronized through message communication.

4.channel channel

Channel is a special type, and only one goroutine can access the channel to send and obtain data at the same time.

Channel writing and reading use the <- operator
Writing: Channel<-Variable
Reading: Variable<- channel

5. Buffer channel

Channels include unbuffered channels and buffered channels
Unbuffered channel make(chan datatype)
Buffered channel make(chan datatype, capacity)

The unbuffered channel can only store one message. The buffered channel can store n messages according to the capacity parameter of the make function and read them out according to FIFO.

func receiver(c chan string) {
    
    
    for msg := range c {
    
    
        fmt.Println(msg)
    }
}
func main() {
    
    
    messages := make(chan string, 2)
    messages <- "hello"
    messages <- "world"
    go receiver(messages)   
    time.Sleep(time.Second * 1)
}//hello world	

Additionally, you can use built-in functions to return buffer channel status

​ len() gets the current buffer number of the channel
​ cap() gets the channel buffer capacity

    ch := make(chan string, 3)
    ch <- "naveen"
    ch <- "paul"
    fmt.Println("capacity is", cap(ch))
    fmt.Println("length is", len(ch))
    fmt.Println("read value", <-ch)
    fmt.Println("new length is", len(ch))
    //capacity is 3
    //length is 2
    //read value naveen
    //new length is 1

Unbuffered channel, writes wait for reads, reads wait for writes, and are blocked until both parties are ready

There is a buffered channel, writes will wait when the channel is full, and reads will wait when the channel is empty.

6. Close the channel

To close the channel, use the built-in function close(), which actually closes writing, that is, the sender tells the receiver that no more data will be sent to the channel.

The receiver can obtain the parameters of whether the channel is closed while the channel is receiving data.

func producer(chnl chan int) {
    
      
    for i := 0; i < 10; i++ {
    
    
        chnl <- i
    }
    close(chnl)
}
func main() {
    
      
    ch := make(chan int)
    go producer(ch)
    for {
    
    
        v, ok := <-ch
        if ok == false {
    
    
            break
        }
        fmt.Println("Received ", v, ok)
    }
}

In addition,for range can automatically determine whether the channel is closed. The specific code is as follows:

func producer(chnl chan int) {
    
      
    for i := 0; i < 10; i++ {
    
    
        chnl <- i
    }
    close(chnl)
}
func main() {
    
      
    ch := make(chan int)
    go producer(ch)
    for v := range ch {
    
    
        fmt.Println("Received ",v)
    }
}

7.WaitGroup

sync.WaitGroupMethod used to wait for a set of Go coroutines to complete before executing the main program. It provides a simple mechanism so that the main program knows when all other coroutines have completed execution.

When using sync.WaitGroup, there are three main functions:

  • Add(int): Increase the number of coroutines to wait for.
  • Done(): Marks the completed coroutine.
  • Wait(): Wait for all coroutines to complete.

Usually, the Add function is used to count the number of coroutines to wait, and then use Done in the function of the coroutine to mark that it has been executed, and finally Use Wait to block the main program until all coroutines are executed.

image-20231107161709866

The following is an example demonstrating the use of sync.WaitGroup:

package main

import (
	"fmt"
	"sync"
	"time"
)

func worker(id int, wg *sync.WaitGroup) {
    
    
	defer wg.Done() // 标志协程完成
	fmt.Printf("Worker %d starting\n", id)
	time.Sleep(time.Second) // 模拟工作
	fmt.Printf("Worker %d done\n", id)
}

func main() {
    
    
	var wg sync.WaitGroup

	for i := 1; i <= 5; i++ {
    
    
		wg.Add(1) // 增加等待的协程数量
		go worker(i, &wg)
	}

	wg.Wait() // 等待协程执行完成
	fmt.Println("All workers have finished")
}

In this example, we start five coroutines, each goroutine simulates some work (simulated by time.Sleep). Add is used to increase the number of coroutines to wait, Done marks that the coroutine has been executed, and Wait blocks the main program until All coroutines have been executed.

8. Guess the number example

Here is an example of using coroutines. The specific example is as follows: Interested friends can try it
Insert image description here

9. Timer

Communication between coroutines requires setting up auxiliary mechanisms such as timeout

One-time timer: The timer only counts once and stops when it is finished.

package main

import (
	"fmt"
	"time"
)

func main() {
    
    
	timer1 := time.NewTimer(2 * time.Second)

	<-timer1.C // 阻塞等待定时器信号
	fmt.Println("Timer 1 expired")

	timer2 := time.NewTimer(1 * time.Second)
	go func() {
    
    
		<-timer2.C
		fmt.Println("Timer 2 expired")
	}()

	stop2 := timer2.Stop() // 停止定时器2
	if stop2 {
    
    
		fmt.Println("Timer 2 stopped")
	}
}

Insert image description here

10.Timer Ticker

Periodic timer: The timer counts periodically and will run permanently unless it is actively stopped.

In Go language, time.Ticker is a tool for repeating interval trigger operations. Unlike time.Timer, time.Ticker repeatedly sends time events to the channel at certain intervals.

The main methods are as follows:

func NewTicker(d Duration) *Ticker 指定一个时间创建一个Ticker , Ticker一经创建便开始计时,不需要额外的启动命令
func (t *Ticker) Stop() 停止计时,但管道不会被关闭

Sample code looks like this:

package main

import (
	"fmt"
	"time"
)

func main() {
    
    
	ticker := time.NewTicker(1 * time.Second)
	defer ticker.Stop()

	for {
    
    
		select {
    
    
		case <-ticker.C:
			fmt.Println("Ticker ticked")
		}
	}
}

11.select

Multiplexing is the transmission of multiple signals or data streams on one channel, such as network cables

select borrows the concept of network multiplexing, is used to monitor multiple channels and respond to multiple channels at the same time

If multiple channels are not writable or readable, select will block
If one channel is writable or readable, select will execute the channel statement
There are multiple channels that are writable or readable, select will randomly select one of them for execution

selectThe statement is a key tool in the Go language for handling channel operations. It can monitor multiple channel operations at the same time. Once a channel is operable (a message can be received or sent), the corresponding case statement will be executed. The select statement is somewhat similar to the switch statement, but is specifically used for channel operations.

Here is an example that demonstrates the use of the select statement:

package main

import (
	"fmt"
	"time"
)

func main() {
    
    
	ch1 := make(chan string)
	ch2 := make(chan string)

	go func() {
    
    
		time.Sleep(2 * time.Second)
		ch1 <- "one"
	}()

	go func() {
    
    
		time.Sleep(1 * time.Second)
		ch2 <- "two"
	}()

	for i := 0; i < 2; i++ {
    
    
		select {
    
    
		case msg1 := <-ch1:
			fmt.Println("Received", msg1)
		case msg2 := <-ch2:
			fmt.Println("Received", msg2)
		}
	}
}

In this example, messages are sent through two coroutines to two different channels ch1 and ch2. The select statement will monitor the status of these two channels. Once there is data to receive, the corresponding case statement will be executed and the received message will eventually be output.

12.Mutex

Multiple threads competing to use a variable at the same time may lead to out-of-control results

mutex, a mutex lock, is used to ensure that a certain variable can only be accessed by one thread at any time; mutex uses Lock() and Unlock() to create a critical section of resources. The code in this section is thread-safe. At any point in time, there can only be one goroutine executing the code in this range.

Mutex can also be replaced by a channel. The bottom layer of the channel is based on mutex, that is, mutex has higher performance. Generally, mutex is used if thread interaction data is not involved. Channels are used if other performance requirements are not sensitive.

package main

import (
	"fmt"
	"sync"
)

var count = 0
var mutex sync.Mutex

func increment() {
    
    
	mutex.Lock()         // 通过 Lock() 方法锁住共享资源
	count++
	mutex.Unlock()       // 通过 Unlock() 方法解锁共享资源
}

func main() {
    
    
	var wg sync.WaitGroup

	for i := 0; i < 1000; i++ {
    
    
		wg.Add(1)
		go func() {
    
    
			defer wg.Done()
			increment()
		}()
	}

	wg.Wait()

	fmt.Println("Count:", count)
}

13.RWMutex

Mutex When there is a lot of concurrency, only one coroutine holds the lock at the same time, and the others are blocked and waiting, and the performance decreases
RWMutex adds read and write signals based on Mutex The amount, and the number of read locks similar to reference counting is used, so that multiple coroutines can hold read locks, which is suitable for applications with a certain amount of concurrency and more reads and less writes.

Notice:

You can apply for multiple read locks in RWMutex. If there is a read lock, applying for a write lock will be blocked.

As long as there is a write lock, subsequent applications for read locks and write locks will be blocked.

The main methods are as follows:

func (rw *RWMutex) Lock() //申请写锁
func (rw *RWMutex) Unlock() //释放写锁
func (rw *RWMutex) RLock() //申请读锁
func (rw *RWMutex) RUnlock()//释放读锁
package main
import (
	"fmt"
	"sync"
)

var sharedData int
var rwMutex sync.RWMutex

func readData() {
    
    
	rwMutex.RLock() // 读取共享资源时使用 RLock() 方法
	defer rwMutex.RUnlock()
	fmt.Println("Read Data:", sharedData)
}

func writeData(value int) {
    
    
	rwMutex.Lock() // 写入共享资源时使用 Lock() 方法
	defer rwMutex.Unlock()
	sharedData = value
	fmt.Println("Write Data:", value)
}

func main() {
    
    
	// 读取数据
	for i := 0; i < 5; i++ {
    
    
		go readData()
	}

	// 写入数据
	for i := 0; i < 5; i++ {
    
    
		go writeData(i)
	}

	// 等待所有协程执行完毕
	fmt.Scanln()
}

Guess you like

Origin blog.csdn.net/qq_51447436/article/details/134295134