Go基础知识入门

发布时间 2023-10-07 20:59:23作者: 独断万古荒天帝

Go基础入门和并发编程

第一章、Go基础知识入门

一、变量和常量

1、变量定义与使用

image-20230825161805584

2、常量定义与使用

image-20230825161556925

3、iota的使用

iota,特殊常量,可以认为是一个可以被编译器修改的常量
每次运行一行iota自动递增,自增类型默认为int类型
iota能简化const常量的定义
每次出现const时,iota归零

image-20230825162813594

4、匿名变量与变量作用域

image-20230825163817930

二、go的基础数据类型

1、整形

类型名称 有无符号 bit数
int8 Yes 8
int16 Yes 16
int32 Yes 32
int64 Yes 64
uint8 No 8
uint16 No 16
uint32 No 32
uint64 No 64
int Yes 等于cpu位数
uint No 等于cpu位数
rune Yes 与 int32 等价
byte No 与 uint8 等价
uintptr No -

rune 类型是 Unicode 字符类型,和 int32 类型等价,通常用于表示一个 Unicode 码点。rune 和 int32 可以互换使用。

byte 是uint8类型的等价类型,byte类型一般用于强调数值是一个原始的数据而不是 一个小的整数。

uintptr 是一种无符号的整数类型,没有指定具体的bit大小但是足以容纳指针。 uintptr类型只有在底层编程是才需要,特别是Go语言和C语言函数库或操作系统接口相交互的地方。

不管它们的具体大小,int、uint和uintptr是不同类型的兄弟类型。其中int和int32也是 不同的类型, 即使int的大小也是32bit,在需要将int当作int32类型的地方需要一个显式 的类型转换操作,反之亦然。

有符号整数采用 2 的补码形式表示,也就是最高 bit 位用作表示符号位,一个 n bit 的有 符号数的值域是从 -2^{n-1} 到 2^{n-1}−1。例如,int8类型整数的值域是从-128 到 127, 而uint8类型整数的值域是从0到255。

2、字符串和数字的转换

除了字符串、字符、字节之间的转换,字符串和数值之间的转换也比较常见。由strconv包提供这类转换功能。

几个常用函数:

函数名 功能
strconv.Itoa() 整数到ASCII
strconv.FormatInt() 用不同的进制格式化数字
strconv.FormatUint() 用不同的进制格式化数字
strconv.Atoi() 将一个字符串解析为整数
strconv.ParseInt() 将一个字符串解析为整数

ParseInt函数的第三个参数是用于指定整型数的大小;例如16表示int16,0则表示int。在任何情况下, 返回的结果y总是int64类型,你可以通过强制类型转换将它转为更小的整数类型。

package main

import (
   "fmt"
   "strconv"
)

func main() {

   var a int = 12
   fmt.Println(a)

   //给类型取别名
   type IT int
   /*
      因为这里取了别名,所以这里的IT与int类型是不一样的
      因此无法赋值
      go语言类型要求很严格
   */
   //var b IT = a
   //var c IT = (int)a
   var d IT = IT(a)
   fmt.Println(d)

   // 字符串转数字
   var istr = "12"
   myint, err := strconv.Atoi(istr)
   //nil为空类型,<nil>
   if err != nil {
      fmt.Println("convert error")
   }
   fmt.Println(myint)

   // 数字转字符串
   var myi = 32
   fmt.Println(strconv.Itoa(myi))
   
   //FormatString进行格式化转化(其他类型转String)
   //parsexxx进行String转其他类型
}

有时候也会使用fmt.Scanf来解析输入的字符串和数字,特别是当字符串和数字混合在一行的时候,它可以灵活处理不完整或不规则的输入。

3、运算符

二元运算符:算术运算、逻辑运算和比较运算,运算符优先级从上到下递减顺序排列

 *      /     %     <<     >>     &     &^ 
 +      -     |     ^      
 ==     !=    <     <=     >      >=
 &&
 ||

bit位操作运算符

符号 操作 操作数是否区分符号
& 位运算 AND No
| 位运算 OR No
^ 位运算 XOR No
&^ 位清空 (AND NOT) No
<< 左移 Yes
>> 右移 Yes

注意 位操作运算符^作为二元运算符时是按位异或(XOR),当用作一元运算符时表示按位取反。

位操作运算符&^用于按位置零(AND NOT):对于表达式z = x &^ y, 如果对应y中某位bit位为 0 的话,结果z的对应的bit位等于x相应的bit位的值,否则 z 对应的bit位为0。

x << n 和x >> n 的右操作数(n)必须为无符号数。

操作 含义 --
<< 左移 左移运算用零填充右边空缺的bit位
>> 右移 无符号数的右移运算用0填充左边空缺的bit位,有符号数的右移运算用符号位的值填充左边空缺的bit位

一般来说,需要一个显式的转换将一个值从一种类型转化位另一种类型,并且算术和逻辑运算的二元操 作中必须是相同的类型。虽然这偶尔会导致需要很长的表达式,但是它消除了所有和类型相关的问题, 而且也使得程序容易理解。

许多整形数之间的相互转换并不会改变数值;它们只是告诉编译器如何解释这个值。但是对于将一个大尺寸的整数类型转为一个小尺寸的整数类型,或者是将一个浮点数转为整数,可能会改变数值或丢失精度。 浮点数到整数的转换将丢失任何小数部分,然后向数轴零方向截断。

任何大小的整数字面值都可以用以0开始的八进制格式书写,例如0666;或用以0x或0X开头的十六进制格式书写,例如0xdeadbeef。十六进制数字可以用大写或小写字母。

摘自Go 语言的基本数据类型

三、字符串基本操作

1、长度、转义符、输出格式化

image-20230827163514865

Go中的fmt几种输出的区别和格式化方式

2、高性能字符串拼接strings.builder

image-20230827164847722

3、字符串常用操作方法

image-20230827174609579

四、条件判断与for循环

if、goto、switch...

package main

import (
	"fmt"
)

func main() {
	for i := 1; i <= 9; i++ {
		for j := 1; j <= i; j++ {
			fmt.Printf("%d*%d = %d ", i, j, i*j)
		}
		fmt.Println()
	}

	//for range 主要对字符串、数组、切片、map、channel
	name := "xiaosage潇洒哥"
	nameRune := []rune(name)
	for _, value := range name {
		fmt.Printf("%c", value)
		fmt.Println()
	}
	for index := range nameRune {
		fmt.Printf("%c\r\n", nameRune[index])
	}
	for i := 0; i < len(nameRune); i++ {
		fmt.Printf("%c\r\n", nameRune[i])
	}
	/*
		字符串      字符串的索引(key) 字符串对应的索引的字符值的拷贝(value)  如果不写key,那么返回的是索引
		数组:      数组的索引        索引对应返回值的拷贝                  如果不写key,那么返回的是索引
		切片       切片的索引        索引对应返回值的拷贝                  如果不写key,那么返回的是索引
		map       map的key       value返回的是key对应值的拷贝           如果不写key,那么返回的是map对应的值
		channel                 value返回的是channel接收的数据
	*/
}

第二章、容器和Go语言编程思想

一、数组、切片(slice)、map、list

1、数组简单用法:

package main

import "fmt"

func main() {
	//数组
	var courses1 [3]string
	var courses2 [4]string
	//打印类型
	fmt.Printf("%T\r\n", courses1)
	fmt.Printf("%T\r", courses2)
	//这里的courses1与courses2是两种不同的类型
	//[]string 与 [3]string也是两种不同的类型,前者叫切片(slice)
	courses1[0] = "go"
	courses1[1] = "grpc"
	courses1[2] = "gin"
	for _, value := range courses1 {
		fmt.Println(value)
	}

	//数组的初始化
	subject1 := [3]string{"Go", "Java", "Python"}
	for _, value := range subject1 {
		fmt.Println(value)
	}
	subject2 := [3]string{2: "Go"}
	for _, value := range subject2 {
		fmt.Println(value)
	}
	//...的使用
	subject3 := [...]string{"Go", "Java", "Python"}
	for _, value := range subject3 {
		fmt.Println(value)
	}
	for i := 0; i < len(subject3); i++ {
		fmt.Println(subject3[i])
	}
	//多维数组
	var courseInfo [2][3]string
	courseInfo[0] = [3]string{"go", "1h", "Tom"}
	courseInfo[1] = [3]string{"python", "1.5h", "jack"}
	//遍历
	for i := 0; i < len(courseInfo); i++ {
		for j := 0; j < len(courseInfo[i]); j++ {
			fmt.Print(courseInfo[i][j] + " ")
		}
		fmt.Println()
	}
	for _, row := range courseInfo {
		for _, coulumn := range row {
			fmt.Print(coulumn + " ")
		}
		fmt.Println()
	}
	for _, row := range courseInfo {
		fmt.Println(row)
	}
}

2、切片(slice)

package main

import "fmt"

func main() {
	/*1
	初始化空切片,并使用append函数填充切片元素。
	这种声明与数组很像,但[]中没有内容,因为slice可变长。
	*/
	var courses []string //初始化声明空切片,切片中没有任何内容,连类型零值也没有
	fmt.Printf("%T\r\n", courses)
	courses = append(courses, "Go")
	courses = append(courses, "Java")
	courses = append(courses, "Python")
	/*2
	直接初始化切片,len=cap。在追加切片元素遇到
	容量cap不足时,将按原容量的2倍扩容cap。
	*/
	s1 := []int{1, 2, 3}
	fmt.Println(s1[0], s1[1], s1[2])
	/*3
	通过内置函数make()初始化切片(常用)
	s := make([]type, len, cap)
	[]type表示切片元素类型,len表示切片长度,
	cap表示切片容量,len小于等于cap,
	其中len个元素会被初始化为对应type的零值。
	*/
	/*
		初始化一个长度为3的切片,其中len个元素会被初始化为默认零值,
		切片元素[0 0 0]。可以使用append函数,向切片插入元素,
		使切片长度len+1。未初始化元素不可以访问,
		会引发panic: runtime error: index out of range。
	*/
	s2 := make([]int, 3, 4)
	s2 = append(s2, 1)
	fmt.Println(s2[0])
	/*4
	引用数组初始化切片
	*/
	arr := [5]int{1, 2, 3, 4, 5}
	s3 := arr[:]
	fmt.Println(s3)
	//访问从下标为2开始,下标为3之前的元素
	fmt.Println(s3[2:3])
}
type slice struct {
   array unsafe.Pointer //用来存储实际数据的数组指针,指向一块连续的内存
   len   int            //切片中元素的数量
   cap   int            //array数组的长度
}
slice的底层实现是结构体

slice是值传递但是效果又能表现出引用传递的效果(不完全是)

  • 当append元素没有造成扩容时,是值传递,但是又表现出引用传递的特点(传递的并不是实际的对象,而是对象的引用,外部对引用对象的改变也会反映到源对象上,因为引用传递的时候,实际上是将实参的地址值复制一份给形参。)
  • 当append元素造成扩容后(先开始是2倍扩容cap*2,后面到达一定程度时会减缓),表现为值传递(传递对象的一个副本,即使副本被改变,也不会影响源对象,因为值传递的时候,实际上是将实参的值复制一份给形参。),即切片指针不再指向原切片的array数组,而是指向新开辟的一块数组地址。

1、go中函数传递都是值传递

2、slice、map、channel都是引用类型,即便是值传递,结构内部还是指向原来的引用对象,所以函数体内可以直接修改元素。

3、如果slice触发扩容,data会指向新的底层数组,而不指向外部的底层数组了。所以之后再修改slice,不会对外部的slice造成影响。

3、map

package main

import (
   "fmt"
)

func main() {
   //map是key(索引)和value(值)结合的无序集合,主要是查询方便时间复杂度O(1)
   var courseMap = map[string]string{}
   courseMap["mysql"] = "mysql原理"
   fmt.Println(courseMap["mysql"])
   //map类型设置值必须要先初始化
   var subject = make(map[string]string, 3) //make是内置函数,主要用于初始化slice、map、channel
   subject["Go语言"] = "Go语言入门到精通"
   subject["Java语言"] = "Java语言入门到精通"
   subject["C语言"] = "C语言入门到精通"
   fmt.Println(subject)
   //遍历
   for key, value := range subject {
      fmt.Println(key, value)
   }
   //判断map中是否存在元素,下面是if语句在Go语言中的另一种用法
   if _, ok := subject["Go语言"]; ok {
      fmt.Println("exist")
   } else {
      fmt.Println("not exist")
   }
   //删除,如果元素不存在也不会报错
   delete(subject, "C语言")
   fmt.Println(subject)
   /*
      map不是线程安全的
      var syncMap sync.Map
   */
}

4、list

package main

import (
   "container/list"
   "fmt"
)

func main() {
   //var myList list.List
   //初始化
   myList := list.New()
   //尾插入
   myList.PushBack("Java")
   myList.PushBack("Grpc")
   myList.PushBack("MySQL")
   //头插入
   myList.PushFront("C")
   //正向遍历打印值
   for i := myList.Front(); i != nil; i = i.Next() {
      fmt.Println(i.Value)
   }
   fmt.Println("-----------------------------")
   //反向遍历打印值
   for i := myList.Back(); i != nil; i = i.Prev() {
      fmt.Println(i.Value)
   }
   //Grpc前插入Go
   i := myList.Front()
   for ; i != nil; i = i.Next() {
      if i.Value == "Grpc" {
         break
      }
   }
   fmt.Println("-----------------------------")
   myList.InsertBefore("Go", i)
   for i := myList.Front(); i != nil; i = i.Next() {
      fmt.Println(i.Value)
   }
   //删除Grpc
   j := myList.Front()
   for ; j != nil; j = j.Next() {
      if j.Value == "Grpc" {
         break
      }
   }
   myList.Remove(i)
   fmt.Println("-----------------------------")
   myList.InsertBefore("Go", i)
   for i := myList.Front(); i != nil; i = i.Next() {
      fmt.Println(i.Value)
   }
}
运行结果:
C
Java                         
Grpc                         
MySQL                        
-----------------------------
MySQL                        
Grpc                         
Java                         
C                            
-----------------------------
C                            
Java                         
Go                           
Grpc                         
MySQL                        
-----------------------------
C                            
Java                         
Go                           
MySQL                        

集合类型4种:

1、数组 - 不同长度的数组类型不一样

2、切片- 动态数组,用起来很方便,而且性能高,我们要尽量使用

3、map

4、list:用得少

二、函数

1、Go中函数是“一等公民”

package main

import (
	"fmt"
)

//函数作为一等公民作为返回值或参数都是非常常见的

/*
返回值可以是多个(与其他语言不同),下面的入参表示不确定int类型入参的个数
而如果在items ...int前加上其他入参,则该入参是必须要传递的
这里的return没有返回任何值则是要直接返回已经定义好的返回值:sun与err
*/
func add1(items ...int) (sum int, err error) {
	for i := range items {
		sum += i
	}
	return
}

func add2(a, b int) {
	fmt.Printf("The sum is:%d\r\n", a+b)
}

// 函数的返回值数函数
func cal(op string) func() {
	switch op {
	case "+":
		return func() {
			fmt.Println("这是加法")
		}
	case "-":
		return func() {
			fmt.Println("这是减法")
		}
	default:
		return func() {
			fmt.Println("这不是加减法")
		}
	}
}

// 函数作为参数
func callback(x, y int, f func(int, int)) {
	f(x, y)
}

// 函数的闭包特性
func autoIncrease() func() int {
	local := 0 //一个函数中访问另一个函数的局部变量是不行的,这就是闭包。但是这里的匿名函数可以访问
	return func() int {
		local++
		return local
	}
}

func main() {
	//函数的普通调用
	sum1, _ := add1(1, 2, 3, 4, 5)
	fmt.Println(sum1)
	fmt.Println("---------------")
	//函数作为变量传递
	funcVar := add1
	sum2, _ := funcVar(1, 2, 3, 4, 5)
	fmt.Println(sum2)
	fmt.Println("---------------")
	//调用返回值为函数的函数
	cal("+")()
	fmt.Println("---------------")
	//函数作为参数,普通用法
	callback(1, 2, add2)
	//函数作为参数,匿名函数
	callback(1, 2, func(a, b int) {
		fmt.Printf("The total is:%d\r\n", a+b)
	})
	fmt.Println("---------------")
	//匿名函数的定义与使用
	localFunc := func(a, b int) {
		fmt.Printf("The total is:%d\r\n", a+b)
	}
	callback(1, 2, localFunc)
	fmt.Println("---------------")
	//函数的闭包特性
	nextFunc := autoIncrease()
	for i := 0; i < 5; i++ {
		fmt.Println(nextFunc())
	}
}

运行结果:
10
---------------
10             
---------------
这是加法       
---------------
The sum is:3   
The total is:3 
---------------
The total is:3 
---------------
1              
2              
3              
4              
5  

2、defer

与java中的try-catch-finally中的finally一样,Go语言为了让打开与关闭操作代码不相隔太远使用defer定义一些关闭操作。

多个defer存在时,根据的先定义后执行的顺序执行

package main

import "fmt"

func main() {
   //连接数据库、打开文件、开始锁,无论如何,最后都要记得去关闭数据库、关闭文件、解锁
   defer fmt.Println("1")
   defer fmt.Println("2")
   defer fmt.Println("main")
   return
}
运行结果:
main
2
1

defer也可以对函数使用,在return之前执行某一函数,例如以下代码的返回值不再是10,而是经过一次+1操作输出11

package main

import "fmt"

func deferReturn() (ret int) {
   defer func() {
      ret++
   }()
   return 10
}

func main() {
   ret := deferReturn()
   fmt.Printf("ret = %d\r\n", ret)
}

运行结果:
ret = 11

3、error、panic、recover

Go语言错误处理的理念,一个函数可能出错,像Java语言中会使用try-catch去包住这个函数,将异常上抛。但是在Go中开发函数的人需要有一个返回值去告诉调用者是否成功,Go设计者要求我们必须要处理这个error,代码中大量地会出现if err != nil。Go设计者认为必须要处理这个error,防御编程

package main

import (
   "errors"
   "fmt"
)

func A() (int, error) {
   return 0, errors.New("This is a error")
}

func main() {
   if _, err := A(); err != nil {
      fmt.Println(err)
   }
}

运行结果:
This is a error

panic会导致程序的退出,平时开发中不要随使用,一般我们在哪里用到:我们一个服务启动的过程中比如我的服务想要启动,必须有些依赖服务准备好,日志文件存在、mysql能连接通、比如配置文件没有问题,这个时候服务才能启动的时候如果我们的服务启动检查中发现了这些任何一个不满足那就调用panic主动调用。但是你的服务一旦启动了,这个时候你的某行代码中不小心panic,那么不好意思你的程序挂了,这是重大事故。但是架不住有些地方我们的代码写的不小心会导致被动触发panic。recover这个函数能捕获到panic

  1. defer需要放在panic之前定义,另外recover只有在defer调用的函数中才会生效
  2. recover处理异常后,逻辑并不会恢复到panic的那个点去

三、结构体

1、基本使用

package main

import "fmt"

type person struct {
	name    string
	age     int
	sex     string
	address string
}

func main() {
	//初始化,所有字段赋值
	p1 := person{"xxx", 20, "man", "安徽省合肥市xxx区xxx街道"}
	fmt.Println(p1)
	//初始化,可部分字段不赋值
	p2 := person{
		name: "xxx",
		age:  20,
	}
	//取值
	fmt.Println(p2.age)
	//以结构体类型为基本元素的切片初始化
	var persons []person
	//通过已有append
	persons = append(persons, p1)
	//直接定义append
	persons = append(persons, person{
		name: "xxx",
		age:  20,
	})
	//直接定义使初始化
	persons1 := []person{
		{"xxx", 20, "man", "安徽省合肥市xxx区xxx街道"},
		{
			name: "xxx",
			age:  20,
		},
	}
	fmt.Println(persons1)
	//匿名结构体,比如说上面的结构体的地址,我们需要将它进行划分,但是只是使用一次
	address := struct {
		province  string
		city      string
		p_address string
	}{
		"安徽省",
		"合肥市",
		"xxx区xxx街道",
	}
	fmt.Println(address.p_address)
}

运行结果:
{xxx 20 man 安徽省合肥市xxx区xxx街道}
20
[{xxx 20 man 安徽省合肥市xxx区xxx街道} {xxx 20  
}]
xxx区xxx街道

2、结构体的嵌套

package main

import "fmt"

type Person struct {
	name string
	age  int
}

type TempStruct struct {
	a int
	b int
}

type Student struct {
	//第一种嵌套方式
	p     Person
	score float32
	name  string
	//第一种嵌套方式,匿名嵌套
	TempStruct
}

// 在结构体上定义方法
// 两种形态:值传递(p Person)
// 当person对象很大,引用传递(p *Person)(使用指针)--->更能节约空间
func (p *Person) print() {
	p.age = 10
	fmt.Printf("name:%s,age:%d\r\n", p.name, p.age)
}

func main() {
	s := Student{
		Person{
			name: "里面的name",
			age:  20,
		},
		78.5,
		"Jack",
		TempStruct{1, 2},
	}
	fmt.Println(s.name)
	s.name = "Tom"
	fmt.Println(s.name)
	fmt.Println(s)
	fmt.Println(s.p.name)
	//测试在结构体上定义的方法
	p := Person{name: "Mary", age: 18}
	p.print()
    s.p.print()
	/*
		s.print()这里调用会报错,但是当Person使用匿名嵌套到Student结构体中时不会报错
	*/
}

运行结果:
Jack
Tom                             
{{里面的name 20} 78.5 Tom {1 2}}
里面的name 
name:Mary,age:10  
name:里面的name,age:10 

为何go语言中结构体A普通地嵌套另一个结构体B时,当结构体B上定义某一方法时,结构体A的实例化对象不能调用这一方法,而如果使用A结构体匿名嵌套B结构体时结构体A的实例化对象能调用这一方法?

在普通嵌套情况下,结构体A嵌套结构体B时,结构体B的方法不会自动继承到结构体A上。这是因为普通嵌套意味着结构体B变成了结构体A的一个字段,而不是结构体A的匿名字段。由于结构体字段的访问是通过选择器(.)进行的,所以调用结构体B的方法需要通过结构体A的字段选择器先访问到结构体B,然后再调用方法。然而,在匿名嵌套情况下,结构体A嵌套结构体B时,结构体B的方法会被自动继承到结构体A上。这是因为匿名嵌套允许内部结构体的方法直接在外部结构体上调用,这样就不需要通过字段选择器来访问嵌套结构体的方法。因此,要想在普通嵌套的结构体中调用内部结构体的方法,需要明确通过结构体字段选择器来进行访问;而在匿名嵌套的结构体中,则可以直接在外部结构体上调用内部结构体的方法。

四、指针

Go语言限制了指针的运算,在C语言中拿到一个指针之后可以进行+1,在Go语言中不行,不能参与运算。Go中的指针是一个阉割版,unsafe包里面,所以说一般我们不会使用unsafe包,但是当你需要使用的时候可以使用

package main

import "fmt"

type Person struct {
   name string
   age  int
}

func (p *Person) changeName() {
   p.name = "Tom"
}

func main() {
   //指针的定义方式1
   p := Person{"Jack", 20}
   var pi *Person = &p
   pi.changeName()
   fmt.Println(pi)
   //指针的定义方式2
   po := &Person{
      name: "Mary",
   }
   po.name = "bilibili"
   fmt.Println(po)
   //指针的定义方式3
   var pp *Person = new(Person)
   pp.name = "Jim"
   pp.age = 20
   fmt.Println(pp)
   /*
      这里的fmt.Println底层是做了优化的,所以打印出的东西并非是指针所指向的地址
      如果要打印地址需要使用printf
   */
   fmt.Printf("%p\n", pi)
   fmt.Printf("%p\n", po)
   /*
      指针需要初始化,例如如果定义了一个结构体类型的指针,
      但是没有初始化之前访问其中的属性,那么是会抛异常的
      此时的指针是nil
   */
   var a *int
   fmt.Println(a)
}

运行结果:
&{Tom 20}
&{bilibili 0}
&{Jim 20}    
0xc000008078 
0xc0000080a8 
<nil>

初始化两个关键字,map、channel、slice初始化推荐使用make方法

指针初始化推荐使用new函数,指针要初始化否则会出现nil pointer

map必须初始化

五、接口

1、接口基本使用、接口嵌套(暂无具体使用)、结构体中嵌入interface的实现

package main

import "fmt"

type MyWriter interface {
   Writer(string) error
}

type MyClose interface {
   Close(string) error
}

// 接口嵌套
type WriterClose interface {
   MyWriter
   MyClose
   DaAiXianZun()
}

type writerClose struct {
   MyWriter //interface也是一个类型 比如说这里我想放入一个写文件的实现、放一个写入数据库的实现
   MyClose
}

type fileWriter struct {
   filePath string
}

type databaseWriter struct {
   host   string
   port   string
   dbname string
}

type fileClose struct {
}

type databaseClose struct {
}

func (fw *fileWriter) Writer(x string) error {
   fmt.Println("This is fileWriter")
   fmt.Println("参数如下:")
   fmt.Println("文件路径:" + fw.filePath)
   return nil
}

func (dw *databaseWriter) Writer(x string) error {
   fmt.Println("This is databaseWriter")
   fmt.Println("参数如下:")
   fmt.Println("主机名称:" + dw.host)
   fmt.Println("端口号:" + dw.port)
   fmt.Println("数据库名称:" + dw.dbname)
   fmt.Println("数据库" + dw.dbname + "使用完毕")
   return nil
}

func (fc *fileClose) Close(filePath string) error {
   fmt.Println(filePath + "处写入文件内容成功,现已关闭文件")
   return nil
}

func (dc *databaseClose) Close(dbname string) error {
   fmt.Println("数据库:" + dbname + "关闭成功")
   return nil
}

func main() {
   //接口在代码解耦上是十分重要的
   var mw MyWriter = &writerClose{
      &fileWriter{
         "D:\\GolandProjects",
      },
      &fileClose{},
   }
   mw.Writer("Test")
   fmt.Println("------------------------------")
   var mw1 MyWriter = &writerClose{
      &databaseWriter{
         "localhost",
         "6371",
         "GoDB",
      },
      &databaseClose{},
   }
   mw1.Writer("Test")
}

2、接口遇到slice的常见错误、error类型解析

package main

import "fmt"

func mPrint(datas ...interface{}) {
   for _, value := range datas {
      fmt.Println(value)
   }
}

func mPrint1(data interface{}) {
   fmt.Println(data)
}

type myInfo struct {
}

func (mi *myInfo) Error() string {
   fmt.Println("我自己实现了Error接口")
   return "无法选中"
}

func main() {
   var data = []interface{}{
      "C", "Java", "Go", 666,
   }
   mPrint(data...) //将slice打散
   fmt.Println("------------------")
   var data1 = []string{
      "C", "Java", "Go",
   }
   //mPrint(data1...)这里会报错,接口遇到slice的常见错误
   mPrint1("C") //这里又是可以的
   fmt.Println("------------------")
   //显式地将他转化为interface
   var data1i []interface{}
   for _, value := range data1 {
      data1i = append(data1i, value)
   }
   mPrint(data1i...)
   fmt.Println("------------------")
   var err = &myInfo{}
   err.Error() //因为error接口的规范在那里,这里其实有一个string类型的返回值
}

第三章、Go并发编程和工程管理

一、package和gomodules

1、基本的包管理、导入操作

目录结构:

image-20230923162926212

代码:

image-20230923162959315

image-20230923162745775

image-20230923163036833

image-20230923163058943

2、配置GoProxy国内镜像

七牛云:

`go env -w GO111MODULE=on`
`go env -w GOPROXY=https://goproxy.cn,direct`

阿里云:

go env -w GO111MODULE=on
go env -w GOPROXY=https://mirrors.aliyun.com/goproxy/,direct

3、go get、go mod相关命令

1、go get

image-20230923154826100

2、go mod tidy(整理依赖)

image-20230923160223040

go install --->升级到最新的次要版本或者修订版本

go get -u=patch --->升级到最新的修订版本

go get github.com/redis/go-redis/v9@version --->会修改go.mod文件

4、replace命令

场景:一开始的时候我写了一个A项目,仓库是project-A,但是我的代码仓库的go.mod中设置的是github.com/xiaosage/A。B项目由于依赖了A项目,import github.com/xiaosage/project-A,go get命令的时候由于package和代码仓库的名称不一样。如何正确导入?

image-20230923162043733

image-20230923162457776

5、go编码规范

参考:Go编码规范

二、单元测试

1、单元测试 go test

go test命令是一个按照一定约定和组织的测试代码驱动程序,在包目录中,所有以_test.go为后缀的源码文件都会go test运行到。我们写的_test.go源码文件不用担心内容过多,因为go build命令不会将这些测试文件打包到最后的执行文件中

test文件有4类:Test开头的:代码功能测试、Benchmark开头的:性能测试、example、模糊测试

image-20230924112819468

image-20230924112856477

2、如何跳过耗时的测试用例

image-20230924113921033

3、基于表格驱动测试

image-20230924135511696

4、benchmark性能测试

测试每种字符拼接的性能

测试代码:

func BenchmarkAdd(bb *testing.B) {
	var a, b, c int
	a = 123
	b = 456
	c = 579
	for i := 0; i < bb.N; i++ {
		if actual := add(a, b); actual != c {
			fmt.Printf("%d + %d,expect:%d,actual:%d", a, b, c, actual)
		}
	}
}

const numbers = 10000

func BenchmarkStringSprintf(b *testing.B) {
	b.ResetTimer()

	for i := 0; i < b.N; i++ {
		var str string
		for j := 0; j < numbers; j++ {
			str = fmt.Sprintf("%s%d", str, j)
		}
	}

	b.StopTimer()
}

func BenchmarkStringAdd(b *testing.B) {
	b.ResetTimer()

	for i := 0; i < b.N; i++ {
		var str string
		for j := 0; j < numbers; j++ {
			str = str + strconv.Itoa(j)
		}
	}

	b.StopTimer()
}

func BenchmarkStringBuilder(b *testing.B) {
	b.ResetTimer()

	for i := 0; i < b.N; i++ {
		var builder strings.Builder
		for j := 0; j < numbers; j++ {
			builder.WriteString(strconv.Itoa(j))
		}
		_ = builder.String()
	}

	b.StopTimer()
}

image-20230924200123768

可见StringBuilder的性能是最好的,一般我们就使用Sprintf就行了,追求更高性能使用StringBuilder

三、并发编程

1、基础
package main

import (
	"fmt"
	"time"
)

/*
python,java,php 多线程编程,多线程和多进程存在的问题就是耗费内存
内存、线程切换,web2.0促进用户级线程,绿程、轻量级线程、协程 python-asyncio php-swoole java-netty
go语言中协程内存占用少(2k)、切换快,go语言的协程,go语言诞生之后就只有协程可用-goroutine,非常方便
*/

/*func main() {
	//主死随从
	//1、闭包 2、for循环的问题:for循环的时候,每一个变量会重用
	//每次for循环的时候,i变量会被重用,当我进行到第二轮的for循环的时候这个i就变了
	for i := 0; i < 100; i++ {
		//匿名函数启动goroutine
		go func() {
			fmt.Println(i)
		}()
	}
	//让主协程休眠10秒,执行随从协程
	time.Sleep(10 * time.Second)
}*/

func main() {
	for i := 0; i < 100; i++ {
		//第一种解决 tmp := i
		go func(i int) {
			fmt.Println(i)
		}(i) //值传递
	}
	time.Sleep(10 * time.Second)
}
2、Go的GMP调度原理

image-20230927104547307

当goroutine中出现例如死循环的代码时,会将该goroutine挂起,让下一个goroutine执行,P处理器会选择一个空闲的线程,将goroutine加入到该线程中执行,当下一个goroutine使用P处理器寻找线程时又是如此操作。这样使得goroutine在一个线程上反复地调用,线程切换时造成的线程准备、一些寄存器的保存等等这些过程都可以省略掉了。因此远比线程的切换性能高。

3、WaitGroup
package main

import (
   "fmt"
   "sync"
)

//子goroutine如何通知主的goroutine自己已经执行结束了?

func main() {
   var wg sync.WaitGroup

   //我要监控多少个goroutine执行结束
   wg.Add(100)
   for i := 0; i < 100; i++ {
      go func(i int) {
         defer wg.Done()
         fmt.Println(i)
      }(i)
   }

   //等到
   wg.Wait()
   fmt.Println("All done")

   //WairGroup主要用于goroutine的执行等待,Add方法要和Done方法配套使用
}
4、Mutex互斥锁与atomic加减1原子操作
package main

import (
   "sync"
   "sync/atomic"
)

var total int32
var wg sync.WaitGroup

//var lock sync.Mutex

/*
锁是不能复制的,复制后就失去了锁的作用
*/

func add() {
   defer wg.Done()
   for i := 0; i < 1000000; i++ {
      atomic.AddInt32(&total, 1)
      //lock.Lock()
      //total += 1
      //lock.Unlock()
   }
}

func reduce() {
   defer wg.Done()
   for i := 0; i < 1000000; i++ {
      atomic.AddInt32(&total, -1)
      //lock.Lock()
      //total -= 1
      //lock.Unlock()
   }
}

func main() {
   wg.Add(2)
   go add()
   go reduce()
   wg.Wait()
   println(total)
}
5、RWMutex读写锁
package main

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

/*
锁的本质是将并行的代码串行化
即使是设计锁,也要尽量保证并行
两组协程:一组负责读数据、一组负责写数据,web系统中大多是读多写少
虽然有多个goroutine,读协程之间应该并发,读和写协程之间应该串行,读和读之间也不应该并行
读写锁
*/

func main() {
   var rwlock sync.RWMutex
   var wg sync.WaitGroup

   wg.Add(6)

   //写的goroutine
   go func() {
      time.Sleep(3 * time.Second)
      defer wg.Done()
      rwlock.Lock()
      defer rwlock.Unlock()
      fmt.Println("get write lock")
      time.Sleep(6 * time.Second)
   }()

   //读的goroutine
   for i := 0; i < 5; i++ {
      go func() {
         defer wg.Done()
         for {
            rwlock.RLock()//加读锁,读锁不会影响别人的读
            time.Sleep(500 * time.Millisecond)
            fmt.Println("get read lock")
            rwlock.RUnlock()
         }
      }()
   }

   wg.Wait()
}

image-20230927173858130

6、通过channel进行goroutine之间的通信
package main

import (
   "fmt"
   "sync"
)

func main() {
   /*
      不要通过共享内存来通信,而是要通过通信来共享内存
      提供消息队列的机制,消费者与生产者之间的关系
      channel再加上语法糖让使用channel更加简单
   */
   var wg sync.WaitGroup
   wg.Add(1)
   var msg chan string

   //有缓冲区与无缓冲区的channel
   msg = make(chan string, 0) //channel的初始化值(缓存空间)如果为0的话,放值进去会阻塞

   go func(msg chan string) { //go中有一种happen-before的机制,可以保障
      defer wg.Done()
      data := <-msg
      fmt.Println(data)
   }(msg)

   msg <- "xiaosage" //语法糖,放值到channel中
   wg.Wait()
   //WaitGroup如果少了Done的调用,容易出现deadlock,无缓冲的channel也容易出现deadlock
}

无缓冲的时候需要启动一个goroutine区去消费它,否则会出现deadlock报错

7、有无缓冲区的channel的应用场景

无缓冲channel适用于通知,B要第一时间知道A是否完成

有缓冲channel适用于生产者与消费者之间的通信

go中channel的应用场景

  1. 消息传递、信息过滤

  2. 信号广播

  3. 信号订阅和广播

  4. 任务分发

  5. 结果汇总

  6. 并发控制

  7. 同步与异步

    ...

8、for-range取channel值
package main

import (
   "fmt"
)

func main() {
   var msg chan int
   msg = make(chan int, 0)

   go func(msg chan int) {
      for data := range msg {
         fmt.Println(data)
      }
      fmt.Println("all done")
   }(msg)
   msg <- 1 //语法糖,放值到channel中
   msg <- 2
   close(msg)
   //已经关闭了channel,不能再放值了,但是可以继续取值
   d := <-msg
   fmt.Println(d)
   //msg <- 3 报错panic
}
9、单向channel的应用场景

默认情况下channel是双向的

但是我们经常一个channel作为参数进行传递,希望对方是单向使用

1、channel的单向控制案例
package main

import (
   "fmt"
   "time"
)

// recv-only
func producer(out chan<- int) {
   for i := 0; i < 10; i++ {
      out <- i * i
   }
   close(out)
}

// read-only
func consumer(in <-chan int) {
   for num := range in {
      fmt.Printf("num=%d\r\n", num)
   }
}

func main() {
   c := make(chan int)
   go producer(c)

   go consumer(c)

   time.Sleep(5 * time.Second)
}
2、案例

交叉输出12AB34CD56EF78GH910IJ1112KL1314MN1516OP1718QR1920ST2122UV2324WX2526YZ2728

package main

import (
   "fmt"
   "time"
)

var number, letter = make(chan bool), make(chan bool)

func ptn() {
   i := 1
   for {
      <-number
      fmt.Printf("%d%d", i, i+1)
      i += 2
      letter <- true
   }
}

func ptl() {
   i := 0
   str := "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
   for {
      <-letter
      if i >= len(str) {
         return
      }
      fmt.Print(str[i : i+2])
      i += 2

      number <- true
   }
}

func main() {
   go ptn()
   go ptl()
   number <- true
   time.Sleep(1 * time.Second)
}
10、监控goroutine的执行
1、select完成对多个channel的监控
package main

import (
	"fmt"
	"time"
)

//channel是多线程安全
//很多时候我们并不会多个goroutine使用同一个channel。

func g1(ch chan struct{}) {
	time.Sleep(1 * time.Second)
	ch <- struct{}{}
}

func g2(ch chan struct{}) {
	time.Sleep(2 * time.Second)
	ch <- struct{}{}
}

func main() {
	//select 类似于 switch case语句,但是select的功能是我们操作linux里面提供的io的select。poil、epoil
	//select主要作用于多个channel
	//现在有个需求,我们现在有两个goroutine都在执行,但是在主的goroutine中,当某一执行完成以后,这个时候我会立马知道
	g1Channel := make(chan struct{})
	go g1(g1Channel)
	g2Channel := make(chan struct{})
	go g2(g2Channel)
	for {
		//我们监控多个channel,任何一个channel有数据,就执行对应的goroutine。
		//1.某一个分支就绪了就执行该分支 2.如果两个都就绪了,就随机执行一个,防止饥饿
		//应用场景
		timer := time.NewTimer(3 * time.Second)
		select {
		case <-g1Channel:
			fmt.Println("g1 done")
		case <-g2Channel:
			fmt.Println("g2 done")
		case <-timer.C:
			fmt.Println("timeout")
			return
		}
	}
}
11、WithValue、WithCancel、WithTimeout的应用场景
package main

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

var wg sync.WaitGroup

func cpuInfo(ctx context.Context) {
   //比如说这里拿可以拿到一个请求的id
   fmt.Printf("tid:%s\r\n", ctx.Value("traceid"))
   //这里还可以记录一些日志,比如说这次请求时哪个traceid发起的
   defer wg.Done()
   for {
      select {
      case <-ctx.Done():
         fmt.Println("退出cpu监控")
         return
      default:
         time.Sleep(2 * time.Second)
         fmt.Println("cpu的信息")
      }
   }
}

func main() {
   wg.Add(1)
   //context包提供了三种函数,WithTimeout,WithValue,WithCancel。
   //如果你的goroutine函数中,如果希望被控制,超时,或者取消,那么你可以使用这些函数。但是我不希望我原来的接口信息受到影响,函数参数中第一个参数尽量地要加上一个ctx
   //1. ctx1, cancle1 := context.WithCancel(context.Background())
   //ctx2, _ := context.WithCancel(ctx1)

   //2. timeout主动超时
   ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
   //3. WithDeadLine在时间点cancel
   //4. WithValue 传递信息
   valueCtx := context.WithValue(ctx, "traceid", "xiaosage")
   go cpuInfo(valueCtx)
   wg.Wait()
   fmt.Println("监控完成")
}

image-20231006234931041

context结构本身不是树形结构,而是一个非树形结构。但是,context结构可以被视为一个树形结构的节点,其中每个节点都有一个父节点(context.Background())和一个或多个子节点(通过context.WithTimeout()context.WithCancel()等方法创建)。

树形结构通常用于表示具有层次结构的数据结构,其中每个节点都有一个父节点和一个或多个子节点。然而,context结构主要用于实现异步编程和取消操作,而不是表示数据结构。因此,它不是树形结构。