字节后端训练营笔记(一)

go的历史与语言优势

go语言本身非常年轻,发布于2009年,C语言是上世纪70年代诞生的语言,C++是80年代诞生的,python是1991年诞生的,java是1995年诞生的,相比而言,go是目前流行语言中最年轻的,是在这些众多语言肩膀上诞生的语言,是由google支持的开源语言,属于静态的编译型语言,由三位语言专家共同编写。

go语言的优势

  • 简单易学:只有25个关键字

  • 开发速度快:比java好多了,和python差不多

  • 性能好:是编译型语言,编译完是字节码,“一处编译,到处运行”;同时可以很方便地引用C语言

  • 支持高并发:使用协程,考虑了很多并发场景和资源消耗的问题。

  • 生态环境好:很多优秀开源项目是用go做的,例如docker,K8S,ETCD,console,codis,TaiDB,都是基于golang开发的。

go的容器

数组(Array)

  • 数组的类型,取决于数组中的元素类型和数组的长度,其中数组的长度不可变

  • [N]T,N表示数组的长度,T表示数组的元素类型,比如[10]int

  • 注意数组的长度和类型,只要有一项不一样,就不能算作一样的数组,属于不同的类型。

  • 数组类型是值类型,不是引用类型,故函数传递数组参数的时候是值拷贝,函数内部使用的是数组的备份,不会影响初始的数组。

  • 数据声明时未显式初始化的元素会隐式初始化为类型对应的0(如数字0、空字符串、false等)。

切片(Slice)

  • []T,T表示切片中的元素类型

  • 切片的结构:

type slice struct{

    Data unsafe.Pointer

    Len int

    Cap int

}
  • 切片包含了对一段底层数组的动态引用,以及长度和容量

  • 长度表示已经有几个元素,容量表示一共能放多少元素

  • 定义的一般方法

    • a := make([]int,4,6)

    • 第一个是切片类型,第二个数是len,第三个数是capacity,再append 只能在len之后append。

  • 定义的其他方法

    • var num = [10]int{0,1,2,3,4,5,6,7,8,9}

    • b := num[2:5:8]

    • 第一个是start,第二个数是end,第三个数是capacity

  • 越容量访问会报panic

  • 注意slice赋值时底层数组是否相同,若直接将sliceA的一部分赋给sliceB,则修改一方时另一方会跟着改动。

  • 为避免这种浅拷贝,可以使用Go的内置函数copy,可以做到深拷贝。

    • slice1 := []int{1,2,3,4,5,6}

    • slice2 := make([]int,3)

    • copy(slice2,slice[1:4])

    • 前者是destination,后者是source

  • 还有个常用函数是append,用于向切片后面加元素

    • slice1 := make([]int,0,10)

    • slice1 = append(slice,1,2,3)

    • 注意如果加元素后超出容量后就会造成切片的扩容:整个容量在1024以下时,每次扩容*2;超过1024后,每次增加四分之一的容量。还要注意每次扩容go会新开辟一块空间,将原来的内容拷贝过来,所以这个操作还是会占据一定性能的。

  • 注意slice[i]是引用改动,但是for-range(for _,n := range slice{用n进行操作})循环是值改动,不会对slice产生实际影响。

映射(Map)

  • 映射的定义:map[k]T

  • key,value结构:students["Jack"] = 11

  • 可用delete删除元素:delete(students,"Jack")

  • 也用make生成:students := make(map[string]int)

  • 注意遍历顺序不是插入顺序(指for-range方法 for k,v := range students {})

  • golang没有内置集合(Set)类型,用Map来实现Set

  • 数组、切片、映射这几个容器都不是并发安全的。

结构体与接口

结构体(Struct)

  • struct 用来自定义复杂数据结构,可以包含多个字段(属性),可以嵌套其他结构体,也可以定义方法。

  • 封装性:字段首字母大写包外可访问、小写包外不可访问

  • 继承:用聚合实现继承(把父类放到子类里)

  • tag:定义特定格式中的形式或约束条件,书写:` `

  • 结构体的方法:收参是A结构体就是A结构体的方法。

  • 结构体的初始化:

    • 直接初始化:person := Person{}

    • 使用new:返回的是指针

  • 类似数组,也有值传递和引用传递的区别,不传指针就没法改

  • 注意,指针和非指针对字段的访问都是用.:person.Age =

接口(Interface)

  • 接口描述了某个类型有哪些方法,或者说一个接口类型,定义了一个方法集。

  • 通过接口可以实现多:

    type Animal interface {
    
        Shout()
    
    }
    
    
    
    type Dog struct{}
    
    type Cat struct{}
    
    
    
    func (d Dog) Shout(){
    
        fmt.PrintIn("汪汪")
    
    }
    
    
    
    func (c Cat) Shout(){
    
        fmt.PrintIn("喵")
    
    }
  • 空接口可以被认为是很多其他语言中的any类型;空接口中没有任何方法,所以任何类型都实现了空接口==》

  • 为什么PrintIn什么都能打印?

    • func Println(a ... interface{}) (n int,err error)

    • 因为里面入参是一堆空接口,而go中万物都是空接口

补充:类型断言

var x interface{} = 123

n,ok := x.(int)

fmt.Println(n,ok) //123 ,true

a = x.(float64) //将产生一个恐慌

协程与管道

协程(Goroutine)

  • go不直接支持创建系统线程,协程是Go程序内部唯一的并发实现方式。

  • 起协程的语句:go func(){......}()

  • 注意主协程(main)结束后,此程序也就退出了,即使还有一些其他协程在运行。

  • 从古至今,多线程的优化有好多种方式,比较常见的一种是reactor模型:线程池中存储大量线程,需要多创建一个线程来执行任务时就从线程池中选取一个线程来用。

  • 而Go语言采用的是另一个思路:有几个核就跑几个线程,只是某个线程上面有很多协程;协程的切换是不会像线程切换那样有操作系统层面上的开销的,例如线程切换需要切换虚拟地址空间、切换内核栈、切换硬件上下文、CPUcache需要失效,而切换协程完全没有这些开销。

  • 协程底层的实现原理:基于GMP模型:

  • G:goroutines 表示一个协程

  • M:machine 表示一个线程

  • P:processor 管理器,通过队列管理协程

  • 基于GMP模型,协程运行在线程上

  • 一个协程中的信息:运行栈+寄存器数值(PC,BP,SP)

  • 协程的切换,仅仅需要改变寄存器的数值,cpu便会从需要切换的协程指定位置继续运行

  • 协程与线程的比例关系:N:M

协程:线程含义优点缺点
1:1一个协程在一个线程上运行(其实就是传统的多线程)利用多核上下文切换比较慢(reactor模型,代价较大)
N:1多个协程在一个线程上运行上下文切换较快1.无法充分利用多核2.饥饿,如果一个协程不结束,其余协程阻塞
N:M多个协程在多个线程上运行充分利用多核,上下文切换快对实现要求更高
  • 协程调度器的设计策略(减少开销、兼顾公平):

    • 复用线程(避免频繁的创建、销毁线程,而是对线程的复用)

      • work stealing机制:当本线程无可运行的协程时,尝试从其他线程绑定的P偷取G,而不是销毁线程。

      • hand off机制:当本线程因为G进行系统调用阻塞时,线程释放绑定的P,把P转移给其他空闲的线程执行。

    • 利用并行:GOMAXPROCS设置P的数量

    • 抢占:限制协程执行时长,不会出现饿死现象

    • 全局协程队列:多个线程全满时可以塞入全局协程队列,它是链表实现的,可以塞很多协程,不用怕没地方放协程。

  • 常用的同步控制机制:WaitGroup

    • 开发过程中,经常遇到多task之间的同步问题。例如,多个子task并发完成一部分任务,主task等待他们最后结束。

var wg sync.WaitGroup ;

for i := 0;i<3;i++ {

    wg.Add(1)

    go func(i int){

        wg.Done()

    }(i)

}

wg.Wait()

管道(Channel)

  • 并发模型CSP,全称Communicationg Sequential Processes。它的核心观念是将两个并发执行的实体通过管道连接起来,所有的消息都通过管道传输。

  • 管道(通道),也是一种Go的数据同步技术。它可以被看作是在一个程序内部的一个先进先出(FIFO:first in first out) 数据队列。

  • 管道的操作有读、写和关闭。

    • 定义:ch := make(chan string)

    • 读:a = <- ch

    • 写: ch <- "hello"

    • 写一个已经关闭的channel会引发panic

  • 管道分类:无缓冲管道&缓冲管道

  • 无缓冲管道:长度为0的channel,为不带buffer的channel

    • ch := make(chan int)

    • 不会发生额外的拷贝

    • 读在写前

  • 有缓冲管道:长度大于0的channel,为带buffer的channel

    • ch := make(chan int,10)

    • 会发生额外的拷贝

    • 写在读前 ch<- 1

    • 缓冲区最大为65535

  • 管道元素的传递,是复制,非缓冲区管道复制了1次,缓冲区管道复制了2次

  • 例如面试常考的:请用管道实现交替打印AB:

func main() {
	ch1 := make(chan string)
	ch2 := make(chan string)
	ch3 := make(chan string)
	go printA(ch1, ch2)
	go printB(ch1, ch2, ch3)
	<-ch3
}

func printA(ch1, ch2 chan string) {
	for i := 0; i < 100; i++ {
		<-ch2
		fmt.Println(i, "A")
		ch1 <- "print A"
	}
}

func printB(ch1, ch2, ch3 chan string) {
	ch2 <- "begin"
	for i := 0; i < 100; i++ {
		<-ch1
		fmt.Println(i, "B")
		if i != 99 {
			ch2 <- "print B"
		} else {
			ch3 <- "end"
		}
	}
}

几项最佳实践

1.关于init()函数

Golang中init()函数是个特殊的函数,它的执行晚于变量的初始化而早于main()函数

  • 同一个包中的同一个文件中,可以有多个init()函数,按照顺序执行。

  • 而不同包的init函数按照包导入的依赖关系决定执行顺序。

乱使用init()函数,会使得代码的可阅读性和可理解性变差。建议不使用init(),或者克制使用。

2.关于切片追加

当使用切片追加时,总是将append的结果保存在相同切片中。如果需要新切片,先copy再追加

例如下述代码,初学者很难判断出来x,y,z都是0124

func main() {
	x := []int{}
	x = append(x, 0)
	x = append(x, 1)
	x = append(x, 2)
	y := append(x, 3)
	z := append(x, 4)
	fmt.Println(x, y, z)
}

3.关于切片构建

一般切片的构建,使用make()函数来完成,如果预知切片的大小,最好直接指定容量,否则一开始写个容量0,后面疯狂append,每次append都要新开空间然后复制,性能极差;所以一开就开够。

注意一个小陷阱:使用make构造切片时,如果只写一个参数的话,默认这个数既是len,也是capacity,所以感觉使用make指定好了容量,但是其实一append还是整个开空间复制,所以用make一定要写中间的0,不要偷懒。

当不知道cap时,要赋值一个适当的cap,既不能太大(占用内存空间),也不能太小(不断拷贝和调用gc)。

4.关于指针

Go中有两种指针:

1.限制型指针,类似 var ptr *int,这种指针只能寻址,但不能通过加减地址来任意读写内存;

2.无限制指针,unsafe.pointer,这种指针类似于C语言中的void*,可以突破Go类型系统的限制,任意读写内存。

对于第一种指针的用法:

  • Go的参数传递是值传递的,需要有结构体内部进行修改,使用指针。

  • 注意,如果不是修改的情况,建议不要使用指针,好处是避免结构体内的field被任意或者不小心修改。

  • 当结构体或者容器数据很大时,可以考虑用指针来提高性能

  • 但是不那么大的话,尽量用值。因为传指针Go要进行逃逸分析,所以传递指针的性能并不一定比传值性能更好,因为要进行逃逸分析。

5.关于恐慌(Panic)

Go中,当有运行时异常时,会报恐慌;也可以用panic()函数来直接触发恐慌。

发生panic后,后面的代码将停止运行,当前函数或方法会直接return。

Go中,延迟(defer)函数可以在函数return前执行,一般会将panic的recover逻辑放在defer函数中。

如果启子协程,如果想防止因为子协程中发生panic而导致整个程序终止,建议在启动子协程的开始增加恢复panic的延迟函数;延迟函数就是前有defer修饰的函数,是在主函数return前一定会执行的方法,所以一旦panic了就会立即执行defer函数,可以在不知道哪里可能会panic时用,并且一旦起协程就写defer也是一个好的编程习惯。

func main() {
	go some()
	go func() {
		defer func() {
			if err := recover(); err != nil {
				fmt.Println("panic is recovered")
			}
		}()
		fmt.Println("Do something!")
		panic("this is a panic")
	}()
	time.Sleep(2 * time.Second)
	fmt.Println("end")
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值