文章目录
一、经验
二、迷点
2.1、内存重排
- 内存重排是指程序在实际运行时对内存的访问顺序和代码编写时的顺序不一致,主要是为了提高运行效率。分别是硬件层面的 CPU重排 和软件层面的 编译器重排
2.2、逃逸分析
- 逃逸分析决定一个变量是分配在堆上还是分配在栈上,是经过编译器的逃逸分析之后得出的结论
- Go逃逸分析最基本的原则是:如果一个函数返回对一个变量的引用,那么它就会发生逃逸。
- 不要盲目使用变量的指针作为函数参数,虽然它会减少复制操作。但其实当参数为变量自身的时候,复制是在栈上完成的操作,开销远比变量逃逸后动态地在堆上分配内存少的多。
2.3、nil究竟是什么
- 明确一点:nil是值而非类型。nil值只能赋值给func、slice、map、chan、interface和指针
- 在Go中,任何类型都会有一个初始值。数值 类型的初始值为0,slice、map、chan、interface和指针类型的初始值为 nil, 对于nil值的变量,我们可以简化理解为初始状态变量
// nil 只是一个表示初始状态的值
var err error
e := &err
if e != nil {
fmt.Printf("&err is not nil:%p\n", e)
}
// 输出:&err is not nil:0xc0000301f0
// 对于slice map chan interface,当值为nil 时,可读不可写
// 只有通过make(new)创建的对象可写
// 1
var s []int
fmt.Printf("%v\n", s[0])
// 输出panic
// 2
var c chan int
val := <-c
fmt.Printf("%v\n", val)
// 输出panic
// 3
var m map[int]int
m[1] = 123
// 输出panic
package main
import "fmt"
func Foo(x interface{}) {
if x == nil {
fmt.Println("empty interface")
return
}
fmt.Println("non-empty interface")
}
func main() {
var x interface{} // 输出 empty interface
var x []int // 输出 non-empty interface 初始化值为nil,但 interface 接收时是带类型方法的
var x int // 输出 non-empty interface 因为初始化值为0
fmt.Println(x == nil)
Foo(x)
}
2.4、接口的本质
接口命名一般以“动作+er”,理解为可以怎么怎么样的“人”,比如Writer为可以理解为“会Write的人”
一旦某人(某个Struct)有能力Write了(实现Write方法了),我们就自动的认为他是个“会写的人”,
是个Writer 任何实现Write方法的Struct都可以当做io.Writer来传递给相关方法(如Fprintln方法)
- 以io.Writer为例看go中的interface{}
- interface 本身是引用类型,即指针类型
- 内部结构:空接口 和 带有方法的接口
package main
import ( "fmt")
type People interface {
Show()
}
type Student struct{}
func (stu *Student) Show() {
}
func live() People {
var stu *Student
return stu
}
func main() { if live() == nil
{
fmt.Println("AAAAAAA")
} else {
fmt.Println("BBBBBBB")
}
}
type eface struct { //空接口
_type *_type //类型信息
data unsafe.Pointer //指向数据的指针(go语言中特殊的指针类型unsafe.Pointer类似于c语言中的void*)}
type iface struct { //带有方法的接口
tab *itab //存储type信息还有结构实现方法的集合
data unsafe.Pointer //指向数据的指针(go语言中特殊的指针类型unsafe.Pointer类似于c语言中的void*)}
type _type struct {
size uintptr //类型大小
ptrdata uintptr //前缀持有所有指针的内存大小
hash uint32 //数据hash值
tflag tflag
align uint8 //对齐
fieldalign uint8 //嵌入结构体时的对齐
kind uint8 //kind 有些枚举值kind等于0是无效的
alg *typeAlg //函数指针数组,类型实现的所有方法
gcdata *byte str nameOff
ptrToThis typeOff
}
type itab struct {
inter *interfacetype //接口类型
_type *_type //结构类型
link *itab
bad int32
inhash int32
fun [1]uintptr //可变大小 方法集合
}
/*
iface比eface 中间多了一层itab结构。 itab 存储_type信息和[]fun方法集,从上面的结构我们就可得出,因为data指向了nil 并不代表interface 是nil, 所以返回值并不为空,这里的fun(方法集)定义了接口的接收规则,在编译的过程中需要验证是否实现接口
*/
- 当调用接口方法时,会自动对指针进行解引用
type Animal interface {
Barking()
}
type Cat struct {
}
func (c *Cat) Barking() {
fmt.Printf("Meow~~\n")
}
type Dog struct{}
func (d Dog) Barking() {
fmt.Printf("W~W~W~\n")
}
d := &Dog{}
d.Barking()
dd := Dog{}
dd.Barking()
c := &Cat{}
c.Barking()
cc := Cat{}
cc.Barking()
/* 输出:
W~W~W~
W~W~W~
Meow~~
Meow~~
*/
- 接口作为函数参数如何传递?
/*
由于接口会自动对传递的指针进行解引用,所以当接口类型作为函数参数传递时,有以下规则:
当以 指针 接收器实现接口方法时,传递AnimalBarking的参数**必须为**对象 指针。
当以 对象 接收器实现接口方法时,传递AnimalBarking的参数既可以是对象 指针(指针会自动解引用),也可以是对象 实例。
*/
func AnimalBarking(a Animal) {
a.Barking()
}
d1 := &Dog{}
AnimalBarking(d1)
d2 := Dog{}
AnimalBarking(d2)
c := Cat{}
AnimalBarking(c) //Cat does not implement Animal (Barking method has pointer receiver)
cc := &Cat{}
AnimalBarking(cc)
- type只能使用在interface
// 编译失败
func main() {
i := GetValue()
switch i.(type) {
case int:
println("int")
case string:
println("string")
case interface{}:
println("interface")
default:
println("unknown")
}
}
func GetValue() int {
return 1
}
2.5、defer 机制
- defer 对外部变量的引用是有两种方式的,分别是作为函数参数和作为闭包引用
1. 作为函数参数,则在defer定义时就把值传递给 defer,并被 cache 起来;如果此变量是一个“值”,那么就和定义的时候是一致的,如果此变量是一个“引用”,那么就可能和定义的时候不一致
2. 作为闭包引用的话,则会在 defer 函数真正调用时根据整个上下文确定当前的值
type number int
func (n number) print() { fmt.Println(n) }
func (n *number) pprint() { fmt.Println(*n) }
func main() {
var n number
defer n.print()
defer n.pprint()
defer func() { n.print() }()
defer func() { n.pprint() }()
n = 3
}
//输出 3 3 3 0
第四个defer语句是闭包,引用外部函数的n, 最终结果是3;
第三个defer语句同第四个;
第二个defer语句,n是引用,最终求值是3.
第一个defer语句,对n直接求值,开始的时候n=0, 所以最后是0;
- 出现 panic 恐慌时候,会先按照 defer 的后入先出的顺序执行,最后才会执行panic
- 利用 defer 原理,关闭文件
- defer 命令拆解
- 闭包=函数+引用环境,匿名函数 继承了函数声明时的作用域
可以把闭包看作是一个类,一个闭包函数调用就是实例化一个类。闭包在运行时可以有多个实例,它会将同一个作用域里的变量和常量捕获下来,无论闭包在什么地方被调用/实例化时,都可以使用这些变量和常量。而且闭包捕获的变量和常量是引用传递
func main() {
var a = Accumulator()
fmt.Printf("%d\n", a(1))
fmt.Printf("%d\n", a(10))
fmt.Printf("%d\n", a(100))
fmt.Println("------------------------")
var b = Accumulator()
fmt.Printf("%d\n", b(1))
fmt.Printf("%d\n", b(10))
fmt.Printf("%d\n", b(100))
}
func Accumulator() func(int) int {
var x int
return func(delta int) int {
fmt.Printf("(%+v, %+v) - ", &x, x)
x += delta
return x
}
}
/* 输出
(0xc0420080a8, 0) - 1
(0xc0420080a8, 1) - 11
(0xc0420080a8, 11) - 111
------------------------
(0xc042008110, 0) - 1
(0xc042008110, 1) - 11
(0xc042008110, 11) - 111
*/
- defer 配合 recover,产生panic的时候 我们至少可以在程序崩溃前做一些 “扫尾工作” 如,关闭客户端的链接
recover() 函数只在 defer 的上下文中才有效 且只有通过在 defer 中用匿名函数调用才有效
2.6、并发
- 【基本摘抄饶神的创作】
- 并发:数据竞争 原子性 内存访问同步 死锁 活锁 饥饿
并发是同一时间应对(dealing with)多间事情的能力;并行是同一时间动手(doing)做多件事情的能力。
并发是指逻辑上具备同时处理多个任务的能力;并行则是物理上同时执行多个任务。
Concurrency is a property of the code; parallelism is a property of the running program.
我们只能编写并发的代码,而不能编写并行的代码,而且只是希望并发的代码能够并行执行。
并发的代码能否并行,取决于抽象的层级:并发原语 runtime 操作系统,要指定上下文,也就是抽象的层级。 - go 的goroutine + (channel、mutex) === 其他语言的 线程 + 内存同步访问
- goroutine 和 channel 是 Go 语言并发编程的 两大基石。Goroutine 用于执行并发任务,channel 用于 goroutine 之间的同步、通信
- go的并发哲学:不要通过共享内存来通信,而要通过通信来实现内存共享,go的并发哲学 依赖于CSP模型 基于 channel 实现
2.7、chan
- 本质,底层就是通过 mutex 来控制并发的。只是 channel 是更高一层次的并发编程原语,封装了更多的功能
就是说 channel 的发送和接收操作本质上都是 “值的拷贝”,无论是从 sender goroutine 的栈到 chan buf,还是从 chan buf 到 receiver goroutine,或者是直接从 sender goroutine 到 receiver goroutine。
- channel 是一个引用类型
- channel 是线程安全的,所以用起来非常方便;channel 还提供“先进先出”的特性;它还能影响 goroutine 的阻塞和唤醒
- 关于是选择 sync 包里的底层并发编程原语还是 channel
- 读写chan是否应该加锁?
不需要,chan 的原始 struct 有 lock 字段,chan 的实现源代码中 读写内部均加了锁
chanel 的底层就是通过 mutex 来控制并发的,只是封装了更多的功能
所以实际应用中,多个goroutine 同时读写chan 时不需要加锁 - 使用Select+超时改善无阻塞读写
- 缓冲(异步)
- 发送和接收元素的本质都是 值的拷贝
- 非缓冲(同步),只有两者都 ready 时,数据才能在内存拷贝;否则都会被挂起,等待另一方的出现才能被唤醒
- send、send finished、receive、receive finished 的 happened-before 关系如下:(接收和发送都是相对于channel而言的)
1. 第 n 个 send 一定 happened before 第 n 个 receive finished
2. 对于容量为 m 的缓冲型 channel ,第 n 个 receive 一定 happened before 第 n+m 个 send finished
3. 对于非缓冲型的 channel , 第 n 个 receive 一定 happened before 第 n 个 send finished
4. channel close 一定 happened before receive 得到通知
// 第1条:done<-true 先于 <-done 发生
var done = make(chan bool)
var msg string
func aGoroutine() {
msg = "hello, world"
done <- true
}
func main() {
go aGoroutine()
<- done
println(msg)
}
// 第3条:在 done<-true 完成之前, <-done 就已经发生了
var done = make(chan bool)
var msg string
func aGoroutine() {
msg = "hello, world"
<-done
}
func main() {
go aGoroutine()
done <- true
println(msg)
}
- 不要从一个 receiver 侧关闭 channel,也不要在有多个 sender 时,关闭 channel,那怎么办呢?
非优雅:
1. 使用 defer - recover 机制,放心大胆地关闭 channel 或者向 channel 发送数据,即使发生了 panic,有 defer - recover 在兜底
2. 使用 sync.Once 来保证只关闭一次
优雅:
3. N 个sender , 一个 receiver : 增加一个传递关闭信号的 channel,receiver 通过信号 channel 下达关闭数据 channel 指令。senders 监听到关闭信号后,停止发送数据。代码没有明确关闭 channel,如果最终没有任何 goroutine 引用它,不管 channel 有没有关闭,最终都会被 gc 回收
// the only receive says "please stop sending more" by closing an additional signal channel
func main() {
rand.Seed(time.Now().UnixNano())
const Max = 10
const NumSenders = 100
dataCh := make(chan int, 100)
stopCh := make(chan struct{})
// 多个 senders
for i := 0; i < NumSenders; i++ {
go func() {
for {
select {
case <-stopCh:
return
case dataCh <- rand.Intn(Max):
}
}
}()
}
// 1 个 the receiver
go func() {
for value := range dataCh {
if value == Max-1 {
fmt.Println("send stop signal to senders.")
close(stopCh)
return
}
fmt.Println(value)
}
}()
// 阻塞主进程
select {
case <-time.After(time.Hour):
}
}
- N 个sender M个 receiver : 增加一个中间人,M 个 receiver 都向它发送关闭的 请求,中间人收到第一个请求后,就会直接下达关闭 指令,另外,N 个 sender 也可以向中间人发送关闭的请求
/*
这里将 toStop 声明成了一个 缓冲型的 channel。
假设 toStop 声明的是一个非缓冲型的 channel,那么第一个发送的关闭 dataCh 请求可能会丢失。因为无论是 sender 还是 receiver 都是通过 select 语句来发送请求,如果中间人所在的 goroutine 没有准备好,那 select 语句就不会选中,直接走 default 选项,什么也不做。这样,第一个关闭 dataCh 的请求就会丢失。
*/
func main() {
rand.Seed(time.Now().UnixNano())
const Max = 10
const NumReceivers = 3
const NumSenders = 3
dataCh := make(chan int, 100)
stopCh := make(chan struct{})
// It must be a buffered channel.
toStop := make(chan string, 1)
var stoppedBy string
// moderator
go func() {
stoppedBy = <-toStop
close(stopCh)
}()
// 多个 senders
for i := 0; i < NumSenders; i++ {
go func(id string) {
for {
value := rand.Intn(Max)
if value == 0 {
select {
case toStop <- "sender#" + id:
fmt.Println("sender#", id)
default:
}
return
}
select {
case value := <-stopCh:
fmt.Println("send stop signal to senders.", value)
return
case dataCh <- rand.Intn(Max):
}
}
}(strconv.Itoa(i))
}
// receivers
for i := 0; i < NumReceivers; i++ {
go func(id string) {
for {
select {
case value := <-stopCh:
fmt.Println("send stop signal to receivers.", value)
return
case value := <-dataCh:
if value == Max-1 {
select {
case toStop <- "receiver#" + id:
fmt.Println("receiver#", id)
default:
}
return
}
fmt.Println(value)
}
}
}(strconv.Itoa(i))
}
// 阻塞主进程
select {
case <-time.After(time.Hour):
}
}
- 关闭的 channel 仍能读出数据
- 定时任务
// 等待 100 ms后,如果 s.stopc 还没有读出数据或者被关闭 就直接结束
select{
case <-time.After(100 * time.Millisecond):
case<-s.stopc:
returnfalse
}
// 每隔1 秒 执行
func worker() {
ticker := time.Tick(1* time.Second)
for{
select{
case <- ticker:
// 执行定时任务
fmt.Println("执行 1s 定时任务")
}
}
}
- 解耦生产方和消费方
- 控制并发数
- 缓存池,可返回不同类型的数据
package main
import (
"sync"
"fmt")//下面的迭代会有什么问题?
type threadSafeSet struct {
sync.RWMutex
s []interface{}
}
func (set *threadSafeSet) Iter() <-chan interface{} {
// ch := make(chan interface{}) // 解除注释看看!
ch := make(chan interface{},len(set.s))
go func() {
set.RLock()
for elem,value := range set.s {
ch <- elem
println("Iter:",elem,value)
}
close(ch)
set.RUnlock()
}()
return ch
}
func main() {
th:=threadSafeSet{
s:[]interface{}{"1","2"},
}
v:=<-th.Iter()
fmt.Sprintf("%s%v","ch",v)
}
2.8、select
- select 中只要有一个case能return,则立刻执行
- 当如果同一时间有多个case均能return则伪随机方式抽取任意一个执行
- 如果没有一个case能return则可以执行”default”块
2.9、for each
// 使用副本的方式,所以m[stu.Name]=&stu实际上一致指向同一个指针, 最终该指针的值为遍历的最后一个struct的值拷贝
func pase_student() {
m := make(map[string]*student)
stus := []student{
{Name: "zhou", Age: 24},
{Name: "li", Age: 23},
{Name: "wang", Age: 22},
}
// 错误写法
for _, stu := range stus {
m[stu.Name] = &stu
}
for k,v:=range m{
println(k,"=>",v.Name)
}
// 正确
for i:=0;i<len(stus);i++ {
m[stus[i].Name] = &stus[i]
}
for k,v:=range m{
println(k,"=>",v.Name)
}
}
3.0、闭包
/*
第一个go func中i是外部for的一个变量,地址不变化。遍历完成后,最终i=10。 故go func执行时,i的值始终是10。
第二个go func中i是函数参数,与外部for中的i完全是两个变量。 尾部(i)将发生值拷贝,go func内部指向值拷贝地址。
*/
func main() {
runtime.GOMAXPROCS(1)
wg := sync.WaitGroup{}
wg.Add(20)
for i := 0; i < 10; i++ {
go func() {
fmt.Println("A: ", i)
wg.Done()
}()
}
for i := 0; i < 10; i++ {
go func(i int) {
fmt.Println("B: ", i)
wg.Done()
}(i)
}
wg.Wait()
}
func test() []func() {
var funs []func()
for i := 0; i < 2; i++ {
//x := i for循环复用局部变量i,每一次放入匿名函数的应用都是想一个变量
funs = append(funs, func() {
println(&i, i)
})
}
return funs
}
func main() {
funs := test()
for _, f := range funs {
f()
}
}
// 闭包引用相同变量
func test(x int) (func(), func()) {
return func() {
println(x)
x += 10
}, func() {
println(x)
}
}
func main() {
a, b := test(100)
a()
b()
}
3.1、方法集
- golang 的方法集仅仅影响接口实现和方法表达式转化,与用过实力或者指针调用方法无关
- 从值的角度来看规则
Values | Methods Receivers |
---|---|
T | (t T) |
*T | (t T) and (t *T) |
- 从接收者的角度来看规则
Values | Methods Receivers |
---|---|
(t T) | T and *T |
(t *T) | *T |
//使用 指接收者 来实现一个接口,那么 只有 指向那个类型的 指针 才能够实现对应的接口
//如果 值接收者 来实现一个接口,那么那个类型的 值和指针 都能够实现对应的接口
①:func (u *user) notify() {
fmt.Printf("Sending user email to %s<%s>\n", u.name, u.email)
}
sendNotificatioin(&u)
②:func (u user) notify() {
fmt.Printf("Sending user email to %s<%s>\n", u.name, u.email)
}
sendNotificatioin(u)
③:func (u user) notify() {
fmt.Printf("Sending user email to %s<%s>\n", u.name, u.email)
}
sendNotificatioin(&u)
package main
import ( "fmt")
type People interface {
Speak(string) string
}
type Stduent struct{}
func (stu *Stduent) Speak(think string) (talk string) {
if think == "bitch" {
talk = "You are a good boy"
} else {
talk = "hi"
}
return
}
func main() {
var peo People = Stduent{} //错误
var peo People = &Stduent{} //正确
think := "bitch"
fmt.Println(peo.Speak(think))
}
3.2、函数返回值命名
- 在函数有多个返回值时,只要有一个返回值有指定命名,其他的也必须有命名
- 如果返回值有有多个返回值必须加上括号
- 如果只有一个返回值并且有命名也需要加上括号
- 函数返回值名字会在函数起始处被初始化为对应类型的 零值 并且作用域为整个函数加粗样式
// defer 需要在函数结束前执行
package main
import "fmt"
func main() {
println(DeferFunc1(1))
println(DeferFunc2(1))
println(DeferFunc3(1))
}
func DeferFunc1(i int) (t int) {
t = i
defer func() {
t += 3
}()
return t
}
func DeferFunc2(i int) int {
t := i
t = 10
defer func() {
t += 3
fmt.Println(t)
}()
t = 20
fmt.Println(t)
return t
}
func DeferFunc3(i int) (t int) {
defer func() {
t += i
}()
return 2
}
//输出
4
20
23
20
3
3.3、结构体比较
- 结构体是否相同不但与属性类型个数有关,还与属性顺序相关;
- 结构体属性中有不可以比较的类型,如map,slice。可以使用reflect.DeepEqual进行比较
3.4、iota
const (
x = iota
y
z = "zz"
k
p = iota
m
)
func main() {
fmt.Println(x, y, z, k, p, m)
}
//输出
0 1 zz zz 4 5
3.5、常量
- 常量不同于变量 在运行期分配内存,常量通常会被编译器在预处理节点直接展开,作为指令数据使用
const cl = 100
var bl = 123
func main() {
println(&bl, bl)
println(&cl, cl) //cannot take the address of cl
}
3.6、goto
- for switch select 语句都可以配合标签 label 形式的表示符使用
- 不能跳转到其它函数或者内层代码
3.7、type alias
- 格式: type MyInt = int
- 本质上是一样的类型,只是起了一个别名,源类型怎么用,别名类型也怎么用 保留原类型的所有方法 字段
3.8、变量作用域
var ErrDidNotWork = errors.New("did not work")
func DoTheThing(reallyDoIt bool) (err error) {
//var result string // 改进
if reallyDoIt {
//if 语 句块内的 err 变量会遮罩函数作用域内的 err 变量
result, err := tryTheThing()
if err != nil || result != "it worked" {
err = ErrDidNotWork
fmt.Println(err)
}
}
return err
}
func tryTheThing() (string, error) {
return "", ErrDidNotWork
}
func main() {
fmt.Println(DoTheThing(true))
fmt.Println(DoTheThing(false))
}
3.9、panic
- panic 仅有最后一个可以被 recover 捕获
// 触发panic("panic")后顺序执行defer,但是defer中还有一个panic,所以覆盖了之前的panic("panic")
package main
import (
"fmt"
"reflect"
)
func main1() {
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
} else {
fmt.Println("fatal")
}
}()
defer func() {
panic("defer panic")
}()
panic("panic")
}
func main() {
defer func() {
if err := recover(); err != nil {
fmt.Println("++++")
f := err.(func() string)
fmt.Println(err, f(), reflect.TypeOf(err).Kind().String())
} else {
fmt.Println("fatal")
}
}()
defer func() {
panic(func() string {
return "defer panic"
})
}()
panic("panic")
}
4.0、map
-
map 的设计也被称为 “The dictionary problem”,任务是设计一种数据结构[哈希查找表(Hashtable)、 搜索树(Searchtree)]用来维护一个集合的数据,并且可以同时对集合进行增删查改的操作
-
方案:
1、搜索树法一般采用自平衡搜索树,包括:AVL 树,红黑树。自平衡搜索树法的最差搜索效率是 O(logN)
2、哈希查找表最差是 O(N)
遍历自平衡搜索树,会按照从小到大的顺序;而哈希查找表则是乱序的 -
前面说了 map 实现的几种方案,Go 语言采用的是哈希查找表,并且使用链表解决哈希冲突(将一个 bucket 实现成一个链表,落在同一个 bucket 中的 key 都会插入这个链表;开放地址法则是碰撞发生后,通过一定的规律,在数组的后面挑选“空位”,用来放置新的 key)
-
通过 key 的哈希值将 key 散落到不同的桶中,每个桶中有 8 个 cell。哈希值的低位决定桶序号,高位标识同一个桶中的不同 key
-
查找、赋值、删除的一个很核心的内容是如何定位到 key 所在的位置
-
makemap 函数返回的结果:*hmap,它是一个指针;而我们之前讲过的 makeslice 函数返回的是 Slice 结构体
-
当 map 和 slice 作为函数参数时,在函数参数内部对 map 的操作会影响 map 自身;而对 slice 却不会。主要原因:一个是指针( *hmap),一个是结构体( slice)。Go 语言中的函数传参都是值传递,在函数内部,参数会被 copy 到本地。*hmap指针 copy 完之后,仍然指向同一个 map,因此函数内部对 map 的操作会影响实参。而 slice 被 copy 后,会成为一个新的 slice,对它进行的操作不会影响到实参。
-
技巧:
操作实现取余
扩容过程是渐进的,主要是防止一次扩容需要搬迁的 key 数量过多,引发性能问题。触发扩容的时机是增加了新元素
bucket 搬迁的时机则发生在赋值、删除期间,每次最多搬迁两个 bucket。 -
需要明确在 Go 中不存在引用传递,所有的参数传递都是值传递
-
map[string]struct{},由于struct{}是空,不关心内容,这样map便改造为set
4.1、slice
- 码农桃花源的分析
- 切片是对底层数组的一个抽象,描述了它的一个片段。
- 切片实际上是一个结构体,它有三个字段:长度,容量,底层数据的地址。
- 多个切片可能共享同一个底层数组,这种情况下,对其中一个切片或者底层数组的更改,会影响到其他切片。
package main
import "fmt"
func main() {
slice := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
s1 := slice[2:5]
s2 := s1[2:6:7]
s2 = append(s2, 100)
s2 = append(s2, 200)
s1[2] = 20
fmt.Println(s1)
fmt.Println(s2)
fmt.Println(slice)
}
// 输出
[2 3 20]
[4 5 6 7 100 200]
[0 1 2 3 20 5 6 7 100 9]
- append 函数会在切片容量不够的情况下,调用 growslice 函数获取所需要的内存,这称为扩容,扩容会改变元素原来的位置。
- 扩容策略并不是简单的扩为原切片容量的 2 倍或 1.25 倍,还有内存对齐的操作。扩容后的容量 >= 原容量的 2 倍或 1.25 倍。
- 当直接用切片作为函数参数时,可以改变切片的元素,不能改变切片本身;
func main() {
s := []int{1, 1, 1}
f(s)
fmt.Println(s)
}
func f(s []int) {
// i只是一个副本,不能改变s中元素的值
//for _, i := range s {
// i++
//}
for i := range s {
s[i] += 1
}
}
// 输出:[2 2 2]
- 想要改变切片本身,可以将改变后的切片返回,函数调用者接收改变后的切片或者将切片指针作为函数参数
func myAppend(s []int) []int {
// 这里 s 虽然改变了,但并不会影响外层函数的 s
s = append(s, 100)
return s
}
func myAppendPtr(s *[]int) {
// 会改变外层 s 本身
*s = append(*s, 100)
return
}
func main() {
s := []int{1, 1, 1}
newS := myAppend(s)
fmt.Println(s)
fmt.Println(newS)
s = newS
myAppendPtr(&s)
fmt.Println(s)
}
//输出:
[1 1 1]
[1 1 1 100]
[1 1 1 100 100]
4.2、sync CPU指令CAS
【atomic】
【sync.WaitGroup】
【mutex内部实现依赖的是信号量用于goroutine的唤醒操作】
context
- 主要用于在 goroutine 之间传递取消信号、超时时间、截止时间以及一些共享的值等。它并不是太完美,但几乎成了并发控制和超时控制的标准做法。
- 传递共享数据
- 取消 goroutine
- 防止 goroutine 泄露
unsafe
net/http库源码
名词解释:
- hander函数: 具有func(w http.ResponseWriter, r *http.Requests)签名的函数
- handler处理器(函数): 经过HandlerFunc结构包装的handler函数,它实现了ServeHTTP接口方法的函数。
- 调用handler处理器的ServeHTTP方法时,即调用handler函数本身。
handler类:实现了Handler接口ServeHTTP方法的结构。 - Golang没有继承,类多态的方式可以通过接口实现
任何结构体,只要实现了ServeHTTP方法。
这个结构就可以称之为handler类
定义的变量称为handler对象
所谓的http服务器:
-
主要在于如何接受 clinet 的 request,并向client返回response
包括注册路由,开启监听,处理连接,路由处理函数 -
接收request的过程中,最重要的莫过于路由(router),即实现一个Multiplexer器。
Go中既可以使用内置的mutilplexer — DefautServeMux,也可以自定义。Multiplexer路由的目的就是为了找到处理器函数(handler),后者将对request进行处理,同时构建response。 -
Clinet -> Requests -> [Multiplexer(router)] -> handler -> Response -> Clinet
追本溯源
- 是讨论Handler接口和ServeMux结构
type textHandler struct {
responseText string
}
func (th *textHandler) ServeHTTP(w http.ResponseWriter, r *http.Request){
fmt.Fprintf(w, th.responseText)
}
type indexHandler struct {}
func (ih *indexHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
html := `<doctype html>
<html>
<head>
<title>Hello World</title>
</head>
<body>
<p>
<a href="/welcome">Welcome</a> | <a href="/message">Message</a>
</p>
</body>
</html>`
fmt.Fprintln(w, html)
}
func main() {
mux := http.NewServeMux()
mux.Handle("/", &indexHandler{})
thWelcome := &textHandler{"TextHandler !"}
mux.Handle("/text",thWelcome)
http.ListenAndServe(":8000", mux)
}
封装结构体,开发者只需要写函数即可,不用再定义结构
func text(w http.ResponseWriter, r *http.Request){
fmt.Fprintln(w, "hello world")
}
func index(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
html := `<doctype html>
<html>
<head>
<title>Hello World</title>
</head>
<body>
<p>
<a href="/welcome">Welcome</a> | <a href="/message">Message</a>
</p>
</body>
</html>`
fmt.Fprintln(w, html)
}
func main() {
mux := http.NewServeMux()
mux.Handle("/", http.HandlerFunc(index))
mux.HandleFunc("/text", text)
http.ListenAndServe(":8000", mux)
}
再去掉自定义的ServeMux
func main() {
http.Handle("/", http.HandlerFunc(index))
http.HandleFunc("/text", text)
http.ListenAndServe(":8000", nil)
}
web server搭建?
- 创建listen socket, 并循环监听
- 接受新的链接请求,并创建网络链接conn,然后开一个goroutine负责处理链接
- 从该链接读取请求参数构造http.Request对象,然后根据请求路径在路由表中查找,上交处理函数
- 应用处理函数处理,并返回不同的信息给用户
- 如果http alive,则不关闭该链接
4.3、内存 GC
- go 的对象在内存中是怎么样的?
- go 的内存分配是怎么样的?
- 栈的内存是怎么分配的?
- GC 是怎么样的?
- GC 怎么帮我们回收对象?
- go 的GC 会不会漏掉对象或者回收还在用的对象?
- go GC什么时候开始?
- go GC什么时候结束?
- go GC会不会太慢,跟不上内存分配的速度?
- go GC会不会暂停我们的应用? 暂停多久?影不影响我们的请求?
4.4、字符咋存?utf8咋编码?string啥结构?
- 字符集就是字符和编号的映射关系;
- 字符编码就是将编号如何存储的方式,至于为什么不直接存储,为了是在字符串中划分单个字符的边界。编码分为定长和变长。定长简单,编解码快,但浪费空间。变长相反;
- go使用记录字符长度的方式决定字符串的边界。其实这类问题的解决方法大多就是边界字符和记录长度。记录长度的最大长度受制于记录字段的字节数。
三、初心
- go源码工程中的技术原理
简洁设计背后的各项实现机制以及具体工作原理
理解某个设计背后所使用的根本原理,以及当他人在实现这个设计的过程中发生的 - 工程决策、实践与实现技巧