Life is short, Let‘s go

一. Hello Go

每个语言入门的第一行代码莫过于hello world
编写如下代码并执行 go run 文件名.go

package main // 包,代表代码所在的模块
import "fmt" // 引入代码依赖,就是可以用别的模块(包)里面的代码内容

// 程序的入口
func main() {
	fmt.Println("hello world!")
}

这段代码需要注意的点:

  1. 所在包必须是main包,在java中是没有这个必要的
  2. 方法也必须是func main(),不能加参数返回值这些东西
  3. 文件名可以随便,只要是xxx.go即可,所在的目录名称也不需要是包名

这里如果想获取到返回状态怎么办呢?类似于C语言中return 0;,可以使用os.Exit(0)方法,与shell等一样,传0代表真,其它数字代表假。在命令行中可以使用$?获取运行状态,当然程序运行结束也会展示出来。
在这里插入图片描述
这里代码最后就是os.Exit(1)


那么如果获取到main参数呢,类似于java的public static void main(String[] args),在go中想获取到类似的参数不能通过main函数中加参数的办法,需要通过os.Args来获取,表示一个string类型的切片(切片是一种数组的封装,具有更好的灵活性)。如下:

package main

import (
	"fmt"
	"os"
)

func main() {
	if len(os.Args) > 1 {
		fmt.Println("Hello World", os.Args[1])
	}
}

在这里插入图片描述
os.Args[0]是该文件生成的可执行文件的全路径

二. 变量与常量

变量的使用方式:

variable := 5 // 短变量不能放在函数外面,这是错的

func main() {
	var a int = 1 // 声明一个int型变量 (如果只是要声明,后续赋值,必须要加类型)
	var b = 1 // 直接赋值的时候不用声明类型,可以自动类型推断,也不建议加类型(看个人习惯了)
	var (
		c int = 2
		d = 3 // 也可以像这样一次声明多个
	)
	e := 1 // 短变量声明法,只能在代码快中使用,在外面是不行的
}

这里go语言是支持一个赋值语句给多个变量同时赋值的,如下:

func main() {
	a := 1
	b := 2
	/*
	如果是c语言,就只能这样写
	int tmp = a
	a = b
	b = tmp
	*/
	// go可以这样做,相同的python也可以这样
	a, b = b, a
}

常量的使用方式:

const cst = "这是个常量字符串"

const (
	multi1 = "这是一次声明多个常量的方式"
	multi2 = "第二个常量"
)

const (
	sameVal1 = "这是一段话"
	sameVal2 // 这里如果不写,那么默认和上面一个的value相同
)

这里有个好用的常量iota,它的默认值为0,但是遇到了声明在一起的常量他会自增,具体实例如下:

const (
	a = 1
	b = iota
	c
)

const (
	v1, v2 = iota, iota
	v3, v4
)
func main() {
	fmt.Println(a, b, c) 
	/*会输出1, 1, 2 
	原因是它们三个在一个const声明中
	b的上面有1个常量,那么b就等于1
	
	c没写,根据上面的说法就应该和b的值一样,也是iota,
	此时c上面有两个常量,所以iota等于2
	*/
	fmt.Println(v1, v2, v3, v4) // 只跟上面行数有关,所以输出0, 0, 1, 1
}


三. 数据类型

基本的数据类型有如下:

类型表示
布尔型bool
字符串string (go的字符串并不像java一样属于引用,默认为""而不是nil
整型int int8 int16 int32 int6 (int是int32还是int64取决于运行的机器位数)
无符号整型uint uint8 uint16 uint32 uint64 uintptr(这是个指针类型)
比特byte (是uint8的别名)
runerune(是int32的别名,每一个rune代表了一个unicode coed point )
浮点型float32 float64)
复数complex64 complex128

注意点:

  1. go语言不允许隐式类型转换
  2. 即使这两个类型的实现完全相同也不可以
  3. 指针类型是不支持运算的
#include <iostream>
using namespace std;

int main() {
	int a = 100;
	long long int b = a;
	return 0;
}

在C语言里面是可以上面的操作的,但是go语言不可以

package main

import "fmt"

type myInt int // 这里的type相当于c的typedef,创造一个新的类型,实现与int一致

type aliasInt = int // 这样写,多一个等于号,含义是不一样的,代表了int的别名,仍然是个int

func main() {
	a := 100 // 默认int
	var b myint
	b = a // 这里仍然会报错,即使它们底层实现一致
	fmt.Println(b)

	var c aliasInt
	c = a // 这里不属于隐式转换,因为aliasInt是个别名,他们确实是一种类型
	fmt.Println(c)
}

func main() {
	a := 1
	c := &1
	fmt.Printf("c的类型是%T\n", c) // 会输出 *int即为指针类型
	/*
	在C中可以这样使用
	#include <iostream>
	using namespace std;
	
	int main() {
	    int array[] = {1, 2, 3};
		// 因为是个数组,所以直接使用array就可以拿到这个数组的首地址,那么*int就会找到该地址的元素 —> 1
	    cout << *array << endl;
	    // c中可以进行指针运算,将该地址+1即为向后移动一位,在去取出元素,就是 -> 2
	    cout << *(array + 1) << endl;
	}
	*/
	// 类似于C中写
	var array = []int{1, 2, 3} // go数组声明
	arrayAddress := &array // 这里是它的指针
	fmt.Printf("%v\n", unsafe.Pointer(arrayAddress)) // 这里可以打印出他的地址,但是却无法进行指针运算
}

小tips:
    类型都存在一些预定义的最大值最小值,在math模块中
    math.MaxInt64
    math.MaxFloat64
    等等…


四. 运算符

基本与c++ java等语言一致,几点区别如下:

  1. 无前自增和前自减(++a --a)
  2. 可以用==比较数组,但是要求数组长度完全相同才能比较(每个位置的元素都一样返回true)
  3. 多了一个&^运算符(按位 - 置零),规则如下:
func main() {
	// a&^b操作: b在二进制的某一位上为1,那么得到的结果该位置就为0
	// 否则该位置为a在此处的表示
	a := 3 // 二进制表示-> 11
	b := 1 // 二进制表示-> 01
	c := a &^ b // 结果为 2
	/*
				1	 					 			1
				0									1
		此处b的位为0,结果为a的该位		此处b的位为1,结果为0
				1									0

		二进制下10 为十进制下 2
		
	*/
}

五. 结构化程序

5.1 循环

go语言不支持while循环,只支持for循环

func main() {
	for i := 0; i < 5; i++ {
		fmt.Println(i)
	}
	
	start, end := 1, 5
	for start <= end {
		fmt.Println(start)
		start++
	}

	for {
		fmt.Println("死循环....")
	}
}

5.2 分支

func main()  {
	var a int
	fmt.Scan(&a) // 从控制台输入 os.stdin
	
	// 这里的condition条件和java一样必须是一个bool值,而不能像c++一样可以用数字表示
	if a >= 1 && a <= 5 {
		fmt.Println("输入的数字在1~5之间")
	} else if a >= 6 && a <= 10 {
		fmt.Println("输入的数字在6~10之间")
	} else {
		fmt.Println("输入的数字不在1~10之间")
	}
}

go语言的if可以使用两段表达式,缩小了一些不必要变量的作用域,非常方便

func main() {
	if i := 5; i < 10 {
		// 第一段声明,第二段判断
		fmt.Println("i是小于10的")
	}
}

switch语句使用如下:

func main() {
	// 第一段也可以像if一样不要
	switch os := runtime.GOOS; os {
	case "darwin":
		fmt.Println("OS X.")
	case "linux":
		fmt.Println("linux")
	default:
		fmt.Printf("other os, it's name is %s\n", os)
	}

	switch { // 也可以这样写,case里面就和if的一样了
	case 1 <= 3:
		fmt.Println("恒真")
	case 1 > 2:
		fmt.Println("恒假")
	}
}

注意点:

  1. 这里判断的可以不只是 常量和整数
  2. 单个case可以逗号分隔多个选项,不需要像c一样不写break让语句落下去
  3. go是不需要break掉case的,自动退出

六. 数组和切片

声明数组:

func main() {
	var array [5]int
	array[0] = 100
	/*
	声明一个长度为5的int数组
	(( 并且!! 会初始化成零值 ))
	这个地方不像c一样分配完内存数字可能是乱的
	如果声明的是个结构体,也不像java一样只声明了数组的空间没有声明类的空间导致nep
	《非常好用》
	*/

	// 声明的同时初始化
	b := []int{1, 2, 3}
	c := [2][3]int{{1, 2}, {3, 4, 5}} // 不需要完全初始化
	d := [...]int{1, 2, 3} // 如果不想数有多少个,用三个点即可
}

数组遍历:

func main() {
	a := [...]int{1, 2, 3, 4}
	// len函数可以获取数组长度
	for i := 0; i < len(a); i++ {
		fmt.Println(a[i])
	}

	// 也可以使用特殊的for range遍历
	for idx, elem : range a {
		// idx 是下标 就是上面的i
		// elem 是元素,就是上面的a[i]
		fmt.Println(idx, elem)
	}
}

数组截取:

func main() {
	a := [...]int{1, 2, 3, 4, 5}
	part := a[1:3] // 获取到的位置是从下标1开始,到下标3-1=2的这一段元素的切片
	fmt.Println(part)

	/*
		c = a[0:0] 相当于清空了,也可以直接c = nil,二者仍有区别,下面会对比
		c = a[:] // 复制了一个切片出来
		c = a[:5] // 从0~4
		c = a[3:] // 从3到最后
	*/
}

切片的内部结构:
切片的底层仍然维护了一个数组,维护了数组的头指针地址 + 切片的长度 + 底层数组的长度
在这里插入图片描述

这就会发现 (c = a[0:0] 相当于清空了,也可以直接c = nil,二者仍有区别,下面会对比) 这句话,
如果c = nil 那么我操作c,a是不会发生任何变化的,但是c = a[0:0],它们的底层共享同一个数组,我改了c的elem,a也会变化


声明一个切片:

func main() {
	var slice0 []int // [] 里面不写长度和...即为声明切片
	slice1 := []int{}
	// make(type, len, cap) -> len: 切片长度(初始化长度)
	// cap: 底层数组长度,未初始化的位置无法访问,下标[len, cap-1]无法访问
	slice2 := make([]int, 2, 5)

	// 添加一个元素
	slice2 = append(slice2, 99)
	/*
		为什么需要在赋值回去?
		一个理解就是当append容量超过底层数组的时候,数组的指针就会更换(创建一个新的数组)
		由于append进去的是一个值拷贝(!!go里面都是值拷贝)
		所以外面真正的这个slice2是无法更换指针的,也无法改变len的大小扩容
		所以只能返回回来一个新的slice
		
		为什么不传&slice呢....
	*/
}

这里需要注意的地方是:cap()函数可以求切片底层数组的长度,但不是整个数组的长度,如图:
在这里插入图片描述


数组与切片对比:

  1. 切片的容量可伸缩,数组不行(动态扩容的时候,会变成原来cap的二倍,能知道该声明多大时尽量固定容量)
  2. 数组可以比较,切片只能和nil进行比较

七. Map

7.1 Map基础

map的声明:

func main() {
	m := map[int]string{
		1: "一月",
		2: "二月",
	}
	m1 := map[string]int{}
	m1["lalala"] = 1
	m2 := make(map[int[string, 10) // 10 是capacity,没有必要初始化len,因为初始化了它也不知道key value, 只能填空
}

map的访问:

func main() {
	m := map[int]string{
		1: "一月",
		2: "二月",
	}
	v := m[1]
	fmt.Println(v) // 一月

	v := m[3] // 不存在会返回"" , 而不是java一样的nil
	// 判断是否存在key的时候如下:
	if val, ok := m[3]; ok {
		fmt.Println(val)
	} else {
		fmt.Println("key不存在")
	}
}

map的遍历:

func main() {
	m := map[int]string{
		1: "一月",
		2: "二月",
	}
	for key, val := m {
		fmt.Println(key, val)
		/*
			1 一月
			2 二月
		*/
	}
}

7.2 Map扩展

map的value部分可以是个函数,所以可以轻松实现一个简单的工厂模式:

func main() {
	myFactory := map[string]func(){} // 最后这个{}是map的空实现,别看错了
	myFactory["矿泉水"] = func() {
		// do something
	}
	myFactory["饮料"] = func() {
		// do something
	}
}

实现set

func mai() {
	mySet := map[string]bool{}
	// 插入
	mySet["myString"] = true;
	// 判断是否存在
	_, ok := mySet["myString"]
	if ok {
		// 存在
	}
	// 删除元素
	delete(mySet, "myString")
	// 元素个数
	fmt.Println(len(mySet)
}

八. 字符串相关

  1. string是基本类型
  2. string是只读的byte slicelen()可以求byte数,但是一个汉字占三个字节,所以无法直接求字符串长度
  3. string只读,用+=操作比较耗时
  4. 常用的字符串操作包: strings strconv
func main() {
	s := "A,B,C,D"
	strSlices := strings.Split(s, ",")
	for _, slice := range strSlices {
		fmt.Println(slice)
	}
	
	toS := strings.Join(strSlices, "!")
	fmt.Println(toS) // A!B!C!D
}

九. 函数

函数的基本格式:

func functionName(argA typeA, argB typeB) (resultA typeC, resultB typeD) {
	// do
	return xx, xx
}

注意点:

  1. go语言的函数允许多返回值
  2. 所有的参数都是值传递,slice等也是,因为底层的数组指向没变,才有引用的错觉
  3. 函数可以作为参数传入和返回,当然也可以当成变量值

可变参数:

func variableArgsFunc(args ...int) int {
	// 实现一个累加操作
	sum := 0
	for _, num := args {
		sum += num
	}
	return sum
}

// 也可以这样提前声明返回值
func variableArgsFunc(args ...int) (sum int) {
	// 实现一个累加操作
	for _, num := args {
		sum += num
	}
	return sum
}

defer函数: 可以延迟函数的调用时机

func main() {
	defer func() {
		fmt.Println("final execute...")
	}()
	fmt.Println("第一个输出...")
}

如果有返回值,defer的时机是这样的:

func hasReturnFunc() int {
	var n int
	defer func() {
		n = 9
	}()
	n = 10
	return n
}

在这里插入图片描述
这个地方可能会有坑,这种形式的返回,其实返回的并不是n本身,而是它的一个副本。
按照上图的过程,坑产生在第一步,第一步其实是在复制一个副本出来,第二步改了n也没什么用,第三步返回的时候是defer执行前的副本,即答案为10 (千万不要认为他是在return n 这句话执行前执行defer,那样得到的应该是9)

当然还有另一个办法能让它变成9,写法如下:

func hasReturnFunc() (n int) {
	defer func() {
		n = 9
	}()
	n = 10
	return n
}

只要在返回值的地方声明了次变量,那返回就是本身而不是副本


十. 面向对象编程

10.1 封装数据和行为

定义结构体:

type MyStruct struct { // 首字母大写别的模块也可以访问到,小写则不能
	elem string // 属性也是,大写可别的模块直接访问到,小写可以通过函数传出去
	name string
}

创建实例以及初始化:

func main() {
	e := MyStruct{"elementVal", "jerry"}
	e1 := MyStruct{elem: "elementVal", name: "jerry"}
	e2 := &MyStruct{elem: "elementVal", name: "jerry"} // 获取指针
	e3 := new(MyStruct) // return a pointer
	// 注意指针并不需要像C那样使用->,仍然使用点即可
}

方法的定义:

// 和函数的定义只差一个地方
func (m MyStruct) introduce() string { // 在函数名前加上是哪个结构的方法即可
	return "hello my name is " + m.name
}

上面的写法是m MyStruct,实际上是一个实例副本,通常为了避免内存拷贝,用下面的方式:

func (m *MyStruct) introduce() string {
	return "hello my name is " + m.name
}

10.2 Duck Type 式接口实现

接口的定义:

type myInterface interface {
	Method() int
}

接口的实现:

var ok = 1
type myStruct struct {}
func (ms *MyStruct) Method() int {
	fmt.Println("这是个接口的方法")
	return ok
}

go语言并不像java一样,如果实现一个接口就需要求implements,只要实现了接口定义的所有方法即可。这种方式是非入侵性的,并且不用先定义一堆接口。

注意: 空接口相当于java中的Object类的作用,形参写空接口,实参可以传任意对象

type emptyInterface interface {}

10.3 扩展与复用

go是不支持继承的!!但是可以用复合的方式做到复用
用法如下:

type Pet struct {
}
type Dog struct {
	Pet
}
func (p *Pet) say() {
	fmt.Print("huhuhu~")
}

func main() {
	dog := new(Dog)
	dog.say() // 这里dog是没有实现say的,但是上面的写法Pet实现了,就会调用
}

多态的实现:只需要定义一个接口,让不同的结构体去实现这些方法,然后函数形参写接口即可

type sayInterface interface {
	say()
}
type Pig struct {
}
type Dog struct {
	Pet
}
func (p *Pig) say() {
	fmt.Println("huhuhu~")
}

func (d *Dog) say() {
	fmt.Println("wangwangwang")
}

func letPetSay(pet sayInterface) {
	pet.say()
}

func main() {
	dog := new(Dog)
	letPetSay(dog)
}

10.4 空接口断言

当一个变量是接口类型的时候,我们可以通过断言将它转换为指定的类型:

type mInterface interface {} // 空接口,任何类型都实现了它
type mStruct struct {}

func assertIt(i mInterface) {
	if v, ok := i.(mStruct); ok {
		fmt.Println("是mStruct类型")
		// v是该类型的值
	}

}
func main() {
	ms := new(mStruct)
	assertIt(ms)	
}

十一. 错误处理

11.1 error

go语言是没有java的那种try catch异常机制的,主要通过errror接口的传递来判断(error类型实现了error接口)

// 我们可以通过errors.New() 快速创建出一个错误的实例
var numIsTooLargerError = errors.New("传入的数字过大")
func computeInTen(a int, b int) (int, error) {
	if a > 10 || b > 10 {
		return nil, numIsTooLargerError 
	}
	return a + b, nil
}

这里错误处理我们应该尽早处理,防止过于多的错误情况出现嵌套,即:

func testError() {
	if error1 {
		return
	}
	if error2 {
		return
	}
}

而不是如下:

func testError() {
	if !error1 {
		if !error2 {

		} else {
			return
		}
	} else {
		return
	}
}

11.2 panic 和 recover

panic是用于不可恢复的错误,让程序crash掉,但是panic后仍然会执行defer函数,我们可以尝试恢复一些已知的错误。常见的写法如下:

func main() {
	defer func() {
		if err := recover(); err != nil {
			fmt.Println("recovered from ", err)
		}
	}()
	fmt.Println("Start")
	panic(errors.New("Something wrong!"))
}

注意:当不知道错误是什么的时候,最好让程序挂掉,否则形成僵尸进程,让健康检查失效很严重。


十三. 构建可用模块(包)

注意点:

  1. 首字母大写代表包外代码可直接访问
  2. 代码的package可以和目录名不一致
  3. 同一目录的package要一致
  4. 下载第三方模块:go get src,强制更新:go get -u src

init函数:在main函数执行之前,所有的init都会按照包导入的顺序执行,每个包可以有多个init函数,每个源文件也可以有多个init函数


go的依赖问题:在新版本中可以使用go mod来组织:在写完import之后,在console中执行go mod tidy即可自动整理下载包。


十四. Go的并发

14.1 协程机制

首先对比一下线程与协程:

  1. java thread stack 默认为1M
  2. Groutine的stack初始化为2K
  3. 和kernel space entity的对应关系:Java thread是1:1 Groutine是M:N

很明显可以看出协程更加轻量级,并且写成的切换不像java那样涉及到内核态,所以开销更小。

启动一个协程的方式:

func main() {
	for i := 0; i < 10; i++ {
		fmt.Println("main: ", i)
		time.Sleep(time.Second * 1) // 休眠1秒
	}
	go func() {
		for i := 0; i < 10; i++ {
			fmt.Println("协程: ", i)
			time.Sleep(time.Second * 1) // 休眠1秒
		}
	}()
	time.Sleep(time.Second * 5)
}

14.2 GMP模型

  1. G是goroutine,基于协程建立的用户态线程
  2. M是machine,它直接关联一个os内核线程,用于执行G。
  3. P是processor,P里面一般会存当前goroutine运行的上下文环境(函数指针,堆栈地址及地址边界),P会对自己管理的goroutine队列做一些调度

在go中启动一个协程之后,会经历如下步骤:
在这里插入图片描述


14.3 共享内存并发机制

go语言中可以利用sync包内的一些组件,例如:sync.Mutex, sync.WaitGroup

func main() {
	sum := 0
	for i := 0; i < 10000; i++ {
		go func(i int) {
			sum ++
		}(i)
	}
	fmt.Println(sum) // 这样写是不安全的->  sum != 10000
}

// 改成下面的写法
func main() {
	// 声明一个锁
	var mutex sync.Mutex
	sum := 0
	for i := 0; i < 10000; i++ {
		go func(i int) {
			defer func() {
				mutex.Unlock()
			}()
			mutex.lock()
			sum ++
		}(i) // 这里为什么要这么传?这样是复制一个副本,加的就是当时的i,如果直接在里面用闭包的形式,加的i可能是9999
	}
	time.Sleep(time.Second) // 睡一秒,以防上面没执行完
	fmt.Println(sum) // 这样写是不安全的->  sum != 10000
}

WaitGroup类似于java中的CountDownLatch,会有一个计数,然后每次Done一下,等数字Done到0了就放行。
上面那个例子中,我们在最后睡了一秒,但是这是非常不优雅的,可以修改如下:

func main() {
	var mutex sync.Mutex
	var wg sync.WaitGroup // 声明一个WaitGroup
	sum := 0
	for i := 0; i < 10000; i++ {
		wg.Add(1) // 到了下面的tag位置这个地方是10000
		go func(i int) {
			defer func() {
				mutex.Unlock()
			}()
			mutex.lock()
			sum++
			wg.Done() // 数字-1
		}(i)
	}
	// tag位置
	wg.Wait() // 这里等待,只要数字没有减到0,就会等,到0就放行
	fmt.Println(sum) // 这样写是不安全的->  sum != 10000
}

14.4 CSP并发机制

交换消息的循序程序(communicating sequential processes),通过一个channel来进行通讯,go的channel是有容量的并且独立于协程。demo如下:

func taskOne() string {
	time.Sleep(time.Second)
	return "任务一的结果"
}

func taskTwo() {
	fmt.Println("do something start")
	time.Sleep(time.Second * 2)
	fmt.Println("do something end")
}

// 返回值即为一个channel类型,这个channel里面可以传递string类型
func asyncTaskOne() chan string { 
	retCh := make(chan string) // 创建一个空长度的channel,那么往里面放东西,因为没有多余的长度就会阻塞住
	go func() { // 开协程异步执行
		retCh <- taskOne() // 将得到的结果放入channel中
	}()
	return retCh
}

func main() {
	// 这是同步写法,总耗时3s+
	// ret := tashOne()
	// taskTwo()
	// fmt.Println(ret)
	ch := asyncTaskOne()
	taskTwo()
	fmt.Println(<-ch) // 从传回的通道中拿出来值
}

注意点:如果channel的声明不给一个长度,那在没有人从channel中拿元素的时候,协程里面的那句话就会一直阻塞,可能导致协程泄露。改为如下写法即可:

go func() {
	// retCh := make(chan string)
	retCh := make(chan string, 1)
}

14.5 多路选择和超时控制

多路选择依靠关键字select实现:

func service1() chan string {
	retCh := make(chan string 1)
	time.Sleep(time.Second * 1)
	retCh <- "service1's result"
	return retCh
}
func service2() chan string {
	retCh := make(chan string 1)
	time.Sleep(time.Second * 2)
	retCh <- "service2's result"
	return retCh
}

func main() {
	select {
	case ret1 :=  <-service1(): // 因为service1先返回,所以会执行这个case
		fmt.Println("先接受到了service1的返回")
	case ret2 :=  <-service2():
		fmt.Println("先接受到了service2的返回")
	}
}

注意点:

  1. 如果同时有返回值,并不是按照定义顺序去判断的,可能是随机的
  2. 如果加了default,在当前select没有chan返回的情况下,就会执行default,而不是去等待某一个chan返回

超时控制,利用select没chan返回会阻塞的原理:

func service() chan string {
	retCh := make(chan string 1)
	time.Sleep(time.Second * 1)
	retCh <- "service's result"
	return retCh
}
func main() {
	select {
	case ret := <-service():
		t.Log(ret)
	case <-time.After(time.Millisecond * 100): // 100ms过去还没有返回,那么就会执行这个case,以此达到超时控制而不是无限等待下去
		t.Error("time out")
	}
}

14.6 channel的关闭

关闭一个channel的方法入下:

func main() {
	ch := make(chan string, 1)
	close(ch)
}

注意:

  1. 关闭channel之后再次向该通道发送数据将会触发panic异常
  2. 关闭channel之后仍然可以正常取出剩下的元素,全部取完再取会得到零值

可以通过接受的第二个参数获取channel是否仍然开启中:

	ch := make(chan string, 1)
	ch <- "hello"
	
	if v, ok := <-ch; ok {
		fmt.Println("channel未关闭")
	} else {
		fmt.Println("channel已关闭")
	}

我们可以利用这个关闭机制做到广播:

func main() {
	ch := make(chan int, 5)
	// 模拟一个生产者
	go func() {
		for i := 0; i < 10; i++ {
			ch <- i // 生产数据
		}
	}()
	
	// 模拟一个消费者
	go func() {
		for i := 0; i < 10; i++ {
			fmt.Println(<-ch) // 消费数据
		}
	}
	
	// 这里需要sleep一下或者waitgroup防止程序没跑完
}

上面的情况当我们知道具体需要生产多少个数据的时候是可以的,但是如果生产数据的个数是个未知数就无法操作了。或者有多个消费者也没办法,就需要依靠close广播,代码如下:

func main() {
	ch := make(chan int, 5)
	// 模拟一个生产者
	go func() {
		for i := 0; i < 10; i++ {
			ch <- i // 生产数据
		}
		close(ch)
	}()
	
	// 模拟一个消费者
	go func() {
		// 这里一直消费,直到通道关闭
		for { 
			if v, ok := <-ch; ok {
				fmt.Println() // 消费数据
			} else {
				break
			}
		}
	}
}

14.7 任务的取消

利用通道,和select语句,上面提到:当select中存在default语句时,如果第一时间没有值就会走default,我们可以利用这一点,判断是否其他协程发出了任务取消的消息。

func isCancelled(cancelChan chan struct{}) bool {
	select {
	case <-cancelChan: // 只要没有人传取消指令(往chan里面放东西),就会返回false
		return true
	default:
		return false
	}
}
// 通过往同一个chan里面放入元素,以达到取消的效果
func cancel(cancelChan chan struct{}) {
	cancelChan <- struct{}{}
}

func main() {
	cancelChan := make(chan struct{})
	go func(cancelChan chan struct{}) {
		for{
			if isCancelled(cancelChan) { // 如果取消任务了就退出
				break
			}
			time.Sleep(time.Millisecond * 5) // 否则可以继续执行
		}
		fmt.Println(i, "Cancelled")
	}(cancelChan)
	
	time.Sleep(time.Second)
	cancel(cancelChan) // 1s后取消任务
}

但是上述代码有一个问题:如果有多个任务执行怎么办?难不成要发N个消息给channel吗?可以直接把channel关掉,这样它就不会走default了,会直接拿到零值。如下:

/*
原来的
func cancel(cancelChan chan struct{}) {
	cancelChan <- struct{}{}
}
*/
func cancel(cancelChan chan struct{}) {
	close(cancelChan)
}

14.8 Context与任务取消

可以通过go语言提供的context来做超时,任务取消,任务截至时间等操作:
在这里插入图片描述
以任务取消为例改造14.7的代码:

// 通过context自带的chan来判断
func isCancelled(ctx context.Context) bool {
	select {
	case <-ctx.Done():
		return true
	default:
		return false
	}
}

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	go func(cancelChan chan struct{}) {
		for{
			if isCancelled(ctx) { // 如果取消任务了就退出
				break
			}
			time.Sleep(time.Millisecond * 5) // 否则可以继续执行
		}
		fmt.Println(i, "Cancelled")
	}(cancelChan)
	
	time.Sleep(time.Second)
	cancel() // 这里不用自己写取消函数了,通过context返回的函数即可
}

该代码仍可以完成同样的功能,有几点如下:

  1. context.WithCancel()需要的参数是一个parent,同样为context类型,代表这个环境是由哪个环境生成出来的,后续有用。如果自己就是根,那么就可以像上面那样写BackGround()
  2. 当调用返回的cancel()函数时,所有由当前context衍生出来的context都会收到一个信号,即ctx.Done能接到东西。

十五. 常见的并发任务(sync的使用)

15.1 多个协程只执行一次某个方法

以单例模式为例,java的代码是这样的(懒汉式的DCL写法):

public class Singleton {  
    private volatile static Singleton instance = null;
    private Singleton(){}  
    public static Singleton getInstance(){  
        if(instance == null){  
            synchronized (Singleton.class) {  
                if(instance == null)  
                    instance = new Singleton();                 
            }             
        }  
        return instance;  
    }  
}  

很麻烦,又要私有化构造方法,还有用synchronized关键字做双重锁检验,还要用volatile防止指令重排序。如果用go的sync.Once代码将如下:

var sync.Once	
type Singleton struct {}
var singleInstance *Singleton
func getInstance() *Singleton {
	once.Do(func() {
		singleInstance = new(Singleton)
	})
	return singleInstance
}

这样就保证了只初始化一次,实现了单例模式。


15.2 仅需任意任务完成即可返回

在很多场景下是只需要多个执行路径下其中一条执行完毕就可以返回的,示例如下:

func getResponse(responseChannel chan string) string {
	for i := 0; i < 10; i++ {
		go func() {
			response := doSomething()
			responseChannel <- response
		}()
	}
	return <-responseChannel // 这里会阻塞住,直到channel里面有值,那就意味着第一个返回到了,就可以return了
}

func main() {
	responseChannel := make(chan string, 10) // 这里不能开0,不然第二个任务没有地方<-出来,后面的都会阻塞住,协程就泄露了
	firsteReponse := getResponse(responseChannel)
	fmt.Println(firstReponse) // 第一个返回结果
}

15.3 必须所有任务都完成

对上面的代码加以改造即可,将阻塞一次返回改成循环,阻塞N次返回。

func getResponse(responseChannel chan string) string {
	for i := 0; i < 10; i++ {
		go func() {
			response := doSomething()
			responseChannel <- response
		}()
	}
	result := ""
	for i := 0; i < 10; i++ {
		result += <-responseChannel + "\n"
	}
	return result
}

func main() {
	responseChannel := make(chan string, 10) // 这里不能开0,不然第二个任务没有地方<-出来,后面的都会阻塞住,协程就泄露了
	alleReponse := getResponse(responseChannel)
	fmt.Println(alleReponse) // 第一个返回结果
}

15.4 对象池

利用buffered channel 的空间存储对象:(这里不使用sync.Pool存储)

type Obj struct {}
type ObjPool struct {
	bufChan chan *Ob // 存指针,不然返回副本就没有pool的意义了
}
func NewObjPool(objNums int) *ObjPool {
	objPool := ObjPool{}
	objPool.bufChan = make(chan *ReusableObj, objNums)
	for i := 0; i < objNums; i++ {
		objPool.bufChan <- &Obj{}
	}
	return &objPool
}

func (pool *ObjPool) GetObj(timeout time.Duration) (*Obj, error) (
	select {
	case obj := <-pool:
		return obj, nil
	case <-time.After(timeout):
		return nil, errors.New("超时了") // 或者做一些超时控制,比如java线程池中的拒绝策略
	}
}

func (pool *ObjPool) ReleaseObj(obj *Obj) error {
	select {
	case p.bufChan <- obj:
		return nil
	default:
		return errors.New("overflow")
	}
}

十六. 单元测试

16.1 go test

  1. 创建测试文件xxx_test.go,必须以_test.go结尾
  2. 编写测试函数TestXXX(t *testing.T),要以Test开头,参数固定
  3. 执行命令go test

示例如下:

// filenmae: func_test.go
func TestDemo(t *testing.T) {
	fmt.Println("hello")
}

在该文件所在目录执行go test结果如下:
在这里插入图片描述
也可以不使用fmt.Println输出,使用t.Log输出:

func TestDemo(t *testing.T) {
	t.Log("hello")
}

此时执行go test无法看到输出结果,如果要看到t相关结果需要加参数-v,即go test -v


t参数还可以使用t.Error()以及t.Fatal(),二者都是代表测试失败(case不通,不是程序crash),区别如下:
t.Error()``执行之后,该测试后续代码依然执行,t.Fatal()`之后后续代码将停止执行。


还可以指定某个测试函数运行,方法是加-run=regexp参数,regexp是正则表达式,这里可以测试某个前缀的方法。例如想要测试TestCode(t *testing.T),那就可以这么写:go test -v -run=Code


-cover参数可以测试代码覆盖率,就是该目录下哪些代码执行到了,哪些没有执行到(不包括_test.go文件)


16.2 Benchmark

可以通过benchmark来测试运行效率,使用方法:

  1. 将函数TestXXX(t *testing.T)改为BenchmarkXXX(b *testing.B)
  2. 测试命令加上参数go test -benchmark=.(这个点也是正则,现在代表所有的Benchmark函数,在windows中或者goland的terminal中注意加上双引号-benchmark="."
  3. b.resetTimer()重置测试开始时间
  4. b.stopTimer()停止测试时间
  5. b.startTimer()开始测试时间
  6. 一般通过345将要测试的代码片段包含起来,排除掉不想测的代码最后得到结果
func BenchmarkConcatStringByAdd(b *testing.B) {
	elems := []string{"1", "2", "3", "4", "5"}
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		ret := ""
		for _, elem := range elems {
			ret += elem
		}
	}
	b.StopTimer()
}

func BenchmarkConcatStringByBytesBuffer(b *testing.B) {
	elems := []string{"1", "2", "3", "4", "5"}
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		var buf bytes.Buffer

		for _, elem := range elems {
			buf.WriteString(elem)

		}
	}
	b.StopTimer()
}

在这里插入图片描述

也可以看到字符串直接+=是个很慢的操作

加上-benchmem参数可以在上面的基础上看到每次操作用掉的内存大小。


十七. 反射

17.1 简介

反射可以通过字符串的方式取操作一个对象的属性和方法。先了解一下两个类型:

  1. reflect.Type,通过reflect.TypeOf(object)获取
  2. reflect.Value,通过reflect.ValueOf(object)获取

我们可以通过上面两种类型的.Kind()方法获取到object的种类,还可以通过.Name()获取到它的类型,这两个是不一样的。类型是int, bool, type cat struct{}的cat 这些具体的类型。种类是大的范围,包括基本类型和切片、通道、指针这些。

func CheckType(v interface{}) {
	t := reflect.TypeOf(v)
	// 如果传入的是&number 那t.Kind() 就是ptr指针类型了
	switch t.Kind() { // 这就是它的种类,是个int类型的,会匹配到第二个case
	case reflect.Float32, reflect.Float64:
		fmt.Println("Float")
	case reflect.Int, reflect.Int32, reflect.Int64:
		fmt.Println("Integer")
	default:
		fmt.Println("Unknown", t)
	}
}

func TestBasicType(t *testing.T) {
	var number int = 12
	CheckType(number )
}

17.2 通过反射修改值&&调用方法

可以通过反射编写更加灵活通用的代码:

type People struct {
	Name string
	Sex bool
}

func TestReflect(t *testing.T) {
	p := &People{
		Name: "jerry",
		Sex: true,
	}
	val := reflect.ValueOf(p)
	// 按照名字获取属性
	t.Log(val.Elem().FieldByName("Name"))
}

上面的注意点有两个:

  1. NameSex属性必须首字母大写,否则根据包的知识它们是无法被reflect访问到的
  2. 由于.ValueOf()传入的是个指针,所以我们如果想拿到它的属性要先去Elem(),这相当于C中的*操作

也可以调用方法:

func (p *People) Do() {
	fmt.Println("wo......")
}

func TestReflect(t *testing.T) {
	p := &People{
		Name: "jerry",
		Sex: true,
	}
	val := reflect.ValueOf(p) // 这里传p或者*p都可以,或者.Elem() 但是如果方法实现是非指针类型的,当前value也必须是指针类型
	val.MethodByName("Do").Call(nil)
}

注意点:有的方法在TypeValue中都有,有的是一样的作用,例如Kind(),有的就是不一样的,例如TypeFieldByName()无法修改值,还是要加以区分。如果想要获取该对象的某些属性类型有关的,最好用Type,和获取修改值有关的最好用Value


17.3 Struct Tag

结构体标记类似于java中的注解,反射中可以获取到这个tag

type People struct {
	Name string `format:"name"` // <- 这个``反引号括起来的就是tag,里面还要再加双引号,是kv形式
	Sex bool
}

这里可以用Type类型获取到这个tag(只能用Type,它不属于值的概念)

func TestReflect(t *testing.T) {
	p := &People{
		Name: "jerry",
		Sex: true,
	}
	//val := reflect.ValueOf(p) // 这里传p或者*p都可以,或者.Elem() 但是如果方法实现是非指针类型的,当前value也必须是指针类型
	//byName := val.Elem().FieldByName("Name")
	//byName.Tag 根本调用不了

	ty := reflect.TypeOf(p) // 这里传p或者*p都可以,或者.Elem() 但是如果方法实现是非指针类型的,当前value也必须是指针类型
	name, _ := ty.Elem().FieldByName("Name")
	t.Log(name.Tag.Get("format"))
}

17.4 万能程序

反射极大的提高了便利性,但是可读性和程序性能会降低。例如自带的json解析就是通过反射实现的,如果针对某个结构单独写一个json序列化,那么性能优于反射。


十八. 构建高可扩展的软件架构

18.1 Pipe-Filter

这种架构更像是线性的结构,通过一个pipe将多个过滤器串接在一起,当一个事件通过这个pipe的时候要把串起来的filter任务全都走一遍。
在这里插入图片描述

在实现上需要定义一个filter接口,有输入流和输出流,让每个filter去实现它的process方法。然后定义pipeline,将需要的filter都注册进去。一个大概的结构如下:
在这里插入图片描述


18.2 Micro-Kernel

微内核架构保持了一个核心,其他的功能都以可插拔的插件方式加入。这样的好处就是当其中一个功能(插件)崩掉之后,不会影响其他功能。并且非常容易拓展。
在这里插入图片描述

实现上需要以下几个关键点:

  1. 核心系统,主要用于注册插件和交互事件队列内容的存储:Agent
  2. 插件接口,用于实现功能:Collector
  3. 事件,如果没有事件,核心系统和各个插件无法交互:Event
type Event struct {
	Source  string // 事件源,由谁发出
	Content string // 发出的消息
}

type Collector interface {
	Init(evtReceiver EventReceiver) error // 初始化插件的必要内容
	Start(agtCtx context.Context) error // 开始工作
	Stop() error
	Destory() error
}

type Agent struct {
	collectors map[string]Collector // 可插拔的入口
	evtBuf     chan Event // 事件存储的地方
	cancel     context.CancelFunc // 如果Agent核心关闭,需要调用的context函数
	ctx        context.Context
	state      int // 当前核心的状态,是否还在工作
}

func (agt *Agent) RegisterCollector(name string, collector Collector) error {
	// 注册插件的接口
	// core code
	if agt.state != Waiting {
		return err // 核心run的时候别插入
	}
	agt.collectors[name] = collector // 插入
	err := collector.Init(collector) // 初始化必要资源
	return err
}

// stopCollectors和destroy同理
func (agt *Agent) startCollectors() error {
	// 真正实现需要注意并发问题
	// core code
	for _, collector := range agt.collectors {
		err := collector.Start(agt) // 启动各个插件,将agt环境注入进去
	}
}

// 核心接收事件
func (agt *Agent) OnEvent(evt Event) {
	agt.evtBuf <- evt
}

// agent处理各个插件的消息
func (agt *Agent) EventProcessGroutine() {
	var evtSeg [10]Event
	for {
		for i := 0; i < 10; i++ {
			select {
			case evtSeg[i] = <-agt.evtBuf:
			case <-agt.ctx.Done(): // 收到取消指令就停止处理
				return
			}
		}
		fmt.Println(evtSeg)
	}
}

/* --------------------------------------- */

// 实现一个插件
type DemoCollector struct {
	evtReceiver EventReceiver // agent核心
	agtCtx      context.Context // 接受停止的环境,收到停止指令后可以先处理现在的任务,然后发出可以stop的指令
	stopChan    chan struct{} // 
	name        string
	content     string
}


// 当agent调用stop的时候,会发出cancel() 并且调用所有插件的stop
func (c *DemoCollector) Start(agtCtx context.Context) error {
	fmt.Println("start collector", c.name)
	for {
		select {
		case <-agtCtx.Done(): // cancel的时候这里就会生效,可以先把最后的部分做完
			c.stopChan <- struct{}{} // 然后发出可以停止的信息
			break
		default:
			time.Sleep(time.Millisecond * 50)
			c.evtReceiver.OnEvent(Event{c.name, c.content})
		}
	}
}

/*
	这里为什么不直接用cancel()控制全部内容而去再用一个stopChan?
		假设我们的stop中存在一些插件依靠的引擎,如果用问题的方法去解决
		那么是无法保证停止start和stop的先后顺序的,这就会产生依靠的引擎关了,然后start方法中最最后还在处理事情,就会出现问题
*/
func (c *DemoCollector) Stop() error {
	fmt.Println("stop collector", c.name)
	select {
	case <-c.stopChan: // 在agent依次调用collector的stop时候,就会走到这,下面利用超时控制
		return nil
	case <-time.After(time.Second * 1):
		return errors.New("failed to stop for timeout")
	}
}

// 释放资源放在destroy中

上面就是这个架构的核心实现了。


十九. 常见的一些任务

19.1 JSON解析

并不像java一样需要引入第三方库,go内置了json解析,通过反射实现:
核心方法就两个:
1.json.Marshal(obj),序列化,将对象序列化成字符串
2.json.Unmarshal([]byte(str). &Object),返回错误

使用方法如下:

type MyJson struct {
	// !! 属性要大写,不然json模块访问不到的
	FirstField string `json:"first_field"` // 打tag即可
	// web服务这里如果返回的时候不想序列化,可以在第一个位置加 减号
	// 如果想要以别的类型读入可以加上类型名,用逗号分隔开它们即可
	// 如果像忽略控制加omitempty
	SecondField string `json:"second_field"`
}

func TestJson(t *testing.T) {
	myJsonObj := &MyJson{
		FirstField: "第一个属性",
		SecondField: "第二个属性",
	}
	if jsonBytes, err := json.Marshal(myJsonObj); err != nil {
		fmt.Println("序列化错误")
	} else {
		fmt.Println(string(jsonBytes))
	}
	
	createObj := new(MyJson)
	jsonStr := "{\"first_field\":\"第一个属性\",\"second_field\":\"第二个属性\"}"
	if err := json.Unmarshal([]byte(jsonStr), createObj); err != nil {
		fmt.Println("反序列化错误")
	} else {
		fmt.Println(createObj)
	}
}

在这里插入图片描述


19.2 Easyjson(更快的序列化)

下载方式:go get -u github.com/mailru/easyjson/...
或者用go mod的情况下import然后执行 go mod tidy

在命令行中使用easyjson -all models.gomodels.go是struct所在的文件,用法是对象.Marshal(),同原生json


19.3 Http服务

func main() {
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("Hello World!"))
		// r.Method 返回请求方式,可以进行不同的操作
	})
	http.ListenAndServe(":8080", nil) // 8080端口启动,第二个参数是个接口,不用框架暂时可以不写
}

路由判断的规则:(上面写的规则)

  1. 不以/结尾,是个固定路径,如果找不到就404了
  2. /结尾,那么它能匹配任何子路径(前面一样后面不管什么样都可以匹配),当然一个URL被多个规则匹配到了就会找最长的。

后面可以用gin等框架,开发更方便


二十. 性能分析

20.1 性能文件查看

通过下列方法可以生成分析文件:

  1. CPU占用分析:pprof.StartCPUProfile(file) 和 pprof.StopCPUProfile()(file可以用os.Create(filename)生成
  2. 内存占用分析:pprof.WriteHeapProfile(file)
  3. 协程分析:pprof.Lookup("goroutine")

运行结束后生成的文件用go tool pprof filename即可

测试的同时也可以生成文件:go test -bench . -cpuprofile=cpu.profgo test -bench . -memprofile=./mem.prof

性能调优可以通过以下步骤找到哪一部分耗时较多:

  1. go test -bench=. -cpuprofile=cpu.prof,利用b.N去跑
  2. go tool pprof cpu.prof生成的文件查看
  3. top命令查看哪个耗时较多
  4. list functionName 查看具体哪一步消耗过大
  5. 《经验调优》
  6. 吐槽:有些东西Mac的brew安装真的爽

20.2 sync.Map的锁问题

sync.Map的实现问题导致了它适用于读多写少的情况,一但读写差不多或者写多于读,那么sync.Map的性能甚至不如RWLock,这时候建议用其他开源的并发map解决。


二十一. 写代码的建议

21.1 复杂对象传递引用

如结构和数组等,它们如果复制一份的开销是很大的

21.2 避免掉内存的分配和复制

map和slice当底层数组大小不够时,会扩容到二倍,这是会重新申请内存,并且复制一份。所以建议初始化到合适的大小

21.2 次数很多的字符串操作不要用+=

string是不可变对象,同样也不建议在多次操作fmt.SPrintf()函数,建议使用strings.Builer,版本过低建议使用bytes.Buffer,用法相同如下:

func TestStringBuilder(t *testing.T) {
	var builder strings.Builder
	for i := 0; i < numbers; i++ {
		builder.WriteString(strconv.Itoa(i))

	}
	result = builder.String()
}

基础入门完结

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值