go知识点

 

编码长度

  • 汉字占3个字节,普通字符占1个字节。

golang 的堆和栈

var p *int    //全局指针变量
func f(){
    var i int
    i = 1
    p = &i    //全局指针变量指向局部变量i
}
func f(){
    p := new(int) //局部指针变量,使用new申请的空间
    *p = 1
}

上面程序中,第一个程序虽然i是通过var申请的局部变量,但是由于有外部指针指向访问,我们有路径可找到这个空间(变量能够逃逸出函数),所以局部变量i是申请在堆空间上。而第二个程序中p指针变量虽然是使用new申请的空间,但是由于退出函数就没有路径可寻找到它(变量无法逃出函数),所以局部变量p是申请在栈空间上的。

  • golang 1.13 defer新增了一个heap字段用于标识_defer 是在堆上还是在栈上。编译器会根据应用场景去选择使用 deferproc 还是 deferprocStack 方法,他们分别是针对分配在堆上和栈上的使用场景。也就是当 defer func 是顶级函数时,将会分配到栈上。但是若在 defer func 外层出现显式的迭代循环,又或是出现隐式迭代,将会分配到堆上。

  • 得益于其延迟对象的堆栈分配规则的改变,措施是由编译器通过对 defer 的 for-loop 迭代深度进行分析,如果 loopdepth 为 1,则设置逃逸分析的结果,将分配到栈上,否则分配到堆上。进而加快了defer的处理速度,官方说明 Go1.13 defer 性能提高 30%。

Go1.13 defer 的性能是如何提高的?

defer

  • defer 是后进先出。panic 需要等defer 结束后才会向上传递。 出现panic会先按照defer的后入先出的顺序执行,最后才会执行panic。

    例子1:

package main

import "fmt"

func calc(index string, a, b int) int {
	ret := a + b
	fmt.Println(index, a, b, ret)
	return ret
}

func main() {
	a := 1
	b := 2
	defer calc("1", a, calc("10", a, b))
	a = 0
	defer calc("2", a, calc("20", a, b))
	b = 1
}
OUTPUT >> 
10 1 2 3
20 0 2 2
2 0 2 2
1 1 3 4

  • 解释:defer 依据先入后出的规则,外围的calc 将会是先2后1, 在calc内部的calc 会在声明defer的时候立即执行。先声明的1,所以calc(“10”,a,b) 将会先执行函数,进行fmt.Println() 输出。输出完后,会返回ret 给外围的calc 进行计算。 所以执行的顺序是10 x x x、20 x x x、2 x x x、1 x x x。

for

例子1:

  • for range 语句相当于将range 结果赋值给for 后面的参数。如果使用指针赋值的话。结果会与预期不同。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
type student struct {
    Name string
    Age  int
}

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)
    }
}
OUTPUT >> 
wang,wang,wang

解决方法: 加入中间变量,使用中间变量来赋值。 在迭代过程中引入中间变量,且每次都重新生成中间变量。这样每次赋值的时候都是新的指针的值。就能得到预期结果。

  • 索引方式
1
2
3
4
5
6
7
	for i := 0; i < len(stus); i++ {
		var a = student{}
		a = stus[i]
		m[stus[i].Name] = &a
		// fmt.Println(stus[i])
	}
	
  • range方式
1
2
3
4
5
	for _, stu := range stus {
		a := stu
	 	m[stu.Name] = &a
	}
	

select

  • select 中只要有一个case能return,则立刻执行。
  • 如果同一时间有多个case均能return则伪随机方式抽取任意一个执行。
  • 如果没有一个case能return则可以执行”default”块。
  • golang 的 select 就是监听 IO 操作,当 IO 操作发生时,触发相应的动作
  • 每个case语句里必须是一个IO操作,确切的说,应该是一个面向channel的IO操作

Go的并发机制

  • Go 搭建了一个特有的两级线程模型,创造了独有的goroutine 这个专有名词。
  • Go 推荐使用channel 来进行多个goroutine之间的数据传递,并保证整个过程的并发安全性。同时也提供了传统的同步方法,(比如互斥量,条件变量等)。
  • Go 不以共享内存来进行通信,而是使用通信来进行共享内存。

MPG

  • M : machine 一个M代表一个内核线程,或称“工作线程”。
  • P : processor 一个P代表执行一个Go代码片段所必须的资源(或称上下文环境)。
  • G : goroutine 一个G代表一个Go代码片段。前者是对后者的一种封装。

Go 的runtime(运行时)会自动的维护这个MPG框架。 M与系统内核绑定。负责最终调用CPU 执行任务。同时与P(processor,上下文)对接,一个M可以对接一个P, 一个P 又可以对接多个G(Go的代码片段),P中维护一个G的队列。其中M与P,P与G的关系是易变的,在实际调度中会发生改变。一个G 的执行需要P和M 共同支持、

关于锁(sync.Mutex)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type UserAges struct {
	ages map[string]int
	sync.Mutex
}
func (ua *UserAges) Add(name string, age int) {
	ua.Lock()
	defer ua.Unlock()
	ua.ages[name] = age
}
func (ua *UserAges) Get(name string) int {
	if age, ok := ua.ages[name]; ok {
		return age
	}
	return -1
}
  • 这段代码在执行Get时可能会出现panic报错

  • 虽然有使用sync.Mutex做写锁,但是map是并发读写不安全的。map属于引用类型,并发读写时多个协程见是通过指针访问同一个地址,即访问共享变量,此时同时读写资源存在竞争关系。会报错误信息:“fatal error: concurrent map read and map write”。

  1. sync.Mutex互斥锁。
  2. sync.RWMutex读写锁,基于互斥锁的实现,可以加多个读锁或者一个写锁
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
type UserAges struct {
	ages map[string]int
	sync.RWMutex
}

func (ua *UserAges) Add(name string, age int) {
	ua.Lock()
	defer ua.Unlock()
	ua.ages[name] = age
}

func (ua *UserAges) Get(name string) int {
	ua.RLock()
	defer ua.RUnlock()
	if age, ok := ua.ages[name]; ok {
		return age
	}

	return -1
}

func main() {
	count := 10000
	gw := sync.WaitGroup{}
	gw.Add(count * 3)
	u := UserAges{ages: map[string]int{}}
	add := func(i int) {
		u.Add(fmt.Sprintf("user_%d", i), i)
		gw.Done()
	}
	for i := 0; i < count; i++ {
		go add(i)
		go add(i)
	}
	for i := 0; i < count; i++ {
		go func(i int) {
			defer gw.Done()
			u.Get(fmt.Sprintf("user_%d", i))
			fmt.Print(".")
		}(i)
	}
	gw.Wait()
	fmt.Println("Done")
}

channel

1
2
3
4
5
6
7
8
9
10
11
12
func (set *threadSafeSet) Iter() <-chan interface{} {
	ch := make(chan interface{}) 
	go func() {
		set.RLock()
		for elem := range set.s {
			ch <- elem
		}
		close(ch)
		set.RUnlock()
	}()
	return ch
}
  • 上面代码在make chan 的时候使用了默认的配置。默认channel是没有缓存的,这样会导致go func() 内部的for迭代写channel的操作一直阻塞。直到channel中的数据被读出。

  • ch := make(chan interface{}) 和 ch := make(chan interface{},1)是不一样的
  • 无缓冲的 不仅仅是只能向 ch 通道放 一个值 而是一直要有人接收,那么ch <- elem才会继续下去,要不然就一直阻塞着,也就是说有接收者才去放,没有接收者就阻塞。
  • 而缓冲为1则即使没有接收者也不会阻塞,因为缓冲大小是1只有当 放第二个值的时候 第一个还没被人拿走,这时候才会阻塞

struct

  • go是强类型语言,所以不同类型的结构不能作比较,但是同一类型的实例值是可以比较的,实例不可以比较,因为是指针类型。
  • 判断struct是否为空 if (Person{}) == P1 {fmt.Println(“P1 is empty”)}
type Person struct {
    id   int 64
    name string
    addr []byte
}
  • 假如在Person中用[]byte类型保存了另外一个字段:地址(addr)这样的 struct 比较的时候只能使用reflect.DeepEqual(P1,Person{}),或者在struct中加一个字段,用来标记是否被初始化:
    type Person struct {
      ready bool
      id    int64
      name  string
      addr  []byte
    }
    
  • 两种方法各有优劣,利用反射无需额外增加字段,但是利用发射往往意味着牺牲点性能,但是,若非经常性的操作,感觉还可以接受。额外增加字段的方法,恼人的是每次初始化struct的时候都需要记得把这个字段赋值,要不然就很尴尬了。

指针和值类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package main

import (
	"fmt"
)

type People interface {
	Speak(string) string
}
type Student struct{}

func (stu *Student) Speak(think string) (talk string) {
	if think == "bitch" {
		talk = "You are a good boy"
	} else {
		talk = "hi"
	}
	return
}
func main() {
	// var peo People = Student{} // 错误
	var peo People = &Student{} 
    
	think := "bitch"
	fmt.Println(peo.Speak(think))
}
  • 上面Student 的Speak方法,被声明成了只能被Student的指针调用。
  • 可以声明成值调用。去掉* 。 调用的时候即使用了指针 goruntime 也能自动解指针。

reflect

原文

  • 反射机制就是在运行时动态的调用对象的方法和属性。
  • Golang的gRPC也是通过反射实现的。
  • 在Golang的实现中,每个interface变量都有一个对应pair,pair中记录了实际变量的值和类型:

    (value, type)

  • value是实际变量值,type是实际变量的类型。一个interface{}类型的变量包含了2个指针,一个指针指向值的类型【对应concrete type】,另外一个指针指向实际的值【对应value】。

  • 例如,创建类型为*os.File的变量,然后将其赋给一个接口变量r:
tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0)

var r io.Reader
r = tty
  • 接口变量r的pair中将记录如下信息:(tty, *os.File),这个pair在接口变量的连续赋值过程中是不变的,将接口变量r赋给另一个接口变量w:
var w io.Writer
w = r.(io.Writer)
  • 接口变量w的pair与r的pair相同,都是:(tty, *os.File),即使w是空接口类型,pair也是不变的。

  • interface及其pair的存在,是Golang中实现反射的前提,理解了pair,就更容易理解反射。反射就是用来检测存储在接口变量内部(值value;类型concrete type) pair对的一种机制。

  • reflect.TypeOf()是获取pair中的type,reflect.ValueOf()获取pair中的value。

reflect总结:

  • 反射可以大大提高程序的灵活性,使得interface{}有更大的发挥余地
    • 反射必须结合interface才玩得转
    • 变量的type要是concrete type的(也就是interface变量)才有反射一说
  • 反射可以将“接口类型变量”转换为“反射类型对象”
    • 反射使用 TypeOf 和 ValueOf 函数从接口中获取目标对象信息
  • 反射可以将“反射类型对象”转换为“接口类型变量
    • reflect.value.Interface().(已知的类型)
    • 遍历reflect.Type的Field获取其Field
  • 反射可以修改反射类型对象,但是其值必须是“addressable”
    • 想要利用反射修改对象状态,前提是 interface.data 是 settable,即 pointer-interface
  • 通过反射可以“动态”调用方法

  • 因为Golang本身不支持模板,因此在以往需要使用模板的场景下往往就需要使用反射(reflect)来实现

panic

原文

  • 查看编译过程。 go tool compile -S main.go

  • panic 方法实际上就是处理当前 Goroutine(g) 上所挂载的 ._panic 链表(所以无法对其他 Goroutine 的异常事件响应),然后对其所属的 defer 链表和 recover 进行检测并处理,最后调用退出命令中止应用程序

const

  • 不能获取常量的指针。常量不同于变量的在运行期分配内存,常量通常会被编译器在预处理阶段直接展开,作为指令数据使用。

goto

  • goto 不能跳转到其他函数或者内层代码

Type Alias

package main
import "fmt"

func main()  {
    type MyInt1 int
    type MyInt2 = int
    var i int =9
    var i1 MyInt1 = i
    var i2 MyInt2 = i
    fmt.Println(i1,i2)
}
  • MyInt1和MyInt2是不同的类型。
  • 基于基本类型int创建了新类型MyInt1,因为Go是强类型语言,所以类型之间的转换必须强制转换,因为int和MyInt1是不同的类型,所以这里会报编译错误。
  • MyInt2只是int的一个别名,本质上还是一个int类型,所以可以直接赋值,不会有问题。

new和make

  • The new build-in function allocates memory(仅仅分配空间). The first argument is a type, not a value, and the value returned is a pointer to a newly allocated zero value of that type.
    • 翻译如下:
      • 内置函数 new 分配空间。传递给new 函数的是一个类型,不是一个值。返回值是 指向这个新分配的零值的指针。
  • The make built-in function allocates and initializes an object(分配空间 + 初始化) of type slice, map or chan(only). Like new , the first arguement is a type, not a value. Unlike new, make’s return type is the same as the type of its argument, not a pointer to it. The specification of the result depends on the type.
    • 翻译为:
      • 内建函数 make 分配并且初始化 一个 slice, 或者 map 或者 chan 对象。 并且只能是这三种对象。 和 new 一样,第一个参数是 类型,不是一个值。 但是make 的返回值就是这个类型(即使一个引用类型),而不是指针。 具体的返回值,依赖具体传入的类型。
  • new 的作用是 初始化 一个指向类型的指针 (*T)。回指向初始化内存的指针
  • make 的作用是为 slice, map 或者 channel 初始化,并且返回引用 T。
  • make(T, args)函数的目的与new(T)不同。它仅仅用于创建 Slice, Map 和 Channel,并且返回类型是 T(不是T*)的一个初始化的(不是零值)的实例。 这中差别的出现是由于这三种类型实质上是对在使用前必须进行初始化的数据结构的引用。 例如, Slice是一个 具有三项内容的描述符,包括 指向数据(在一个数组内部)的指针,长度以及容量。在这三项内容被初始化之前,Slice的值为nil。对于Slice,Map和Channel, make()函数初始化了其内部的数据结构,并且准备了将要使用的值。

const

const (
	x = iota
	y
	z = "zz"
	k
	p = iota
)
const (
	x2 = iota
	y2
	z2 = "zz"
	k2
	p2 = iota
)

func main() {
	fmt.Println(x, y, z, k, p, x2, y2, z2, k2, p2)
}
  • output » 0 1 zz zz 4 0 1 zz zz 4

不能使用简短声明来设置字段的值

错误示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 错误示例
type info struct {
	result int
}

func work() (int, error) {
	return 3, nil
}

func main() {
	var data info
	data.result, err := work()	// error: non-name data.result on left side of :=
	fmt.Printf("info: %+v\n", data)
}

正确示例

1
2
3
4
5
6
7
8
9
10
11
12
13
// 正确示例
func main() {
	var data info
	var err error	// err 需要预声明

	data.result, err = work()
	if err != nil {
		fmt.Println(err)
		return
	}

	fmt.Printf("info: %+v\n", data)
}

MAP

package main

import "fmt"

func main() {
	m := make(map[string]int)

	m["Answer"] = 42 //获得元素
	fmt.Println("The value:", m["Answer"])

	m["Answer"] = 48 
	fmt.Println("The value:", m["Answer"])

	delete(m, "Answer") // 删除元素
	fmt.Println("The value:", m["Answer"])

	v, ok := m["Answer"] // 通过双赋值检测某个键存在
	fmt.Println("The value:", v, "Present?", ok)
}
    • 如果map的一个字段是strcut 类型 则无法直接更新该struct的单个字段。
    • map 中的元素是不可寻址的。需区分开的是,slice 的元素可寻址。
    • go中的map的value本身是不可寻址的,因为map在扩容的时候,可能要做key/val pair迁移,value本身地址是会改变的,所以不支持寻址是为了安全考虑。
    • 不论是直接map[key]这样取值还是for…range来遍历,取得都是实际值的拷贝。如果是结构体,修改的其实是拷贝的副本,原数据不变;如果是结构体的指针,由于原指针和拷贝指针都指向同一个内存,所以可以成功修改。 同样道理的还有struct的receiver,如果不想修改(比如你的配置结构),则用struct;如果需要修改,用struct指针。

nil != nil

var err error

  • 这时的err是一个接口类型。
  • 第一点,其实Go在实现接口的时候,保存了两个东西,一个是这一个接口背后的类型 T,一个是这个类型背后的值 V,因此当我们在讨论接口这个东西的时候,实际上我们永远是在讨论类型以及类型的值。
  • 第二点,只有类型与值同时是 nil 的时候这个接口才会是 nil,这点就是我们问题的来源。
  • 第三点,我们都知道接口是隐式实现的,当我们用一个接口类型去接收一个 nil 结构体的时候,这个结果将不是 nil,因为此时的接口值是有类型(T)的, 只是它的值(V)是 nil,所以说我们刚才的 if 判断应该是成立的。

nil 是 interface、function、pointer、map、slice 和 channel 类型变量的默认初始值.但声明时不指定类型,编译器也无法推断出变量的具体类型。

– 返回顶部 –