golang笔记
这篇文档零散的记载了一些知识点以及容易犯错的语法知识
结构体绑定方法
结构体绑定方法分为两种情况:指针绑定和赋值绑定,下面是两种情况的地址情况
import "fmt"
import "unsafe"
type MyStruct struct {
id int
}
func (self MyStruct) Assignment() {
fmt.Println(unsafe.Pointer(&self))
}
func (self MyStruct) modify_value_assignment() {
self.id = 2
}
func (self *MyStruct) Pointer() {
fmt.Println(unsafe.Pointer(&self))
}
func (self *MyStruct) modify_value_pointer() {
self.id = 2
}
func main() {
s0 := MyStruct{1}
fmt.Println(s0)
fmt.Println(unsafe.Pointer(&s0)) //打印地址
s0.modify_value_assignment()
fmt.Println(s0) // {1}
s0.Assignment()
s0.modify_value_pointer()
fmt.Println(s0) // {2}
s0.Pointer()
}
执行的输出是
{1}
0xc420084008
{1}
0xc420084038
{2}
0xc420092020
由此可见
- 指针绑定 拷贝了指向结构体的指针,二者指向同一块内存
- 赋值绑定 拷贝了整个结构体变量(内存),新的结构体变量改变与原变量无关
slice 与 map
- slice
切片本身并不是动态数组或者数组指针。它内部实现的数据结构通过指针引用底层数组,设定相关属性将数据读写操作限定在指定的区域内。切片本身是一个只读对象,其工作机制类似数组指针的一种封装。
切片(slice)是对数组一个连续片段的引用,所以切片是一个引用类型(因此更类似于 C/C++ 中的数组类型,或者 Python 中的 list 类型)。这个片段可以是整个数组,或者是由起始和终止索引标识的一些项的子集。需要注意的是,终止索引标识的项不包括在切片内。切片提供了一个与指向数组的动态窗口。
给定项的切片索引可能比相关数组的相同元素的索引小。和数组不同的是,切片的长度可以在运行时修改,最小为 0 最大为相关数组的长度:切片是一个长度可变的数组。
Slice 的数据结构定义如下:
type slice struct {
array unsafe.Pointer
len int
cap int
}
切片的结构体由3部分构成,Pointer 是指向一个数组的指针,len 代表当前切片的长度,cap 是当前切片的容量。cap 总是大于等于 len 的。
- map
hashmap
new 与 make
- new
是一个用来分配内存的内建函数,但是与C++不一样的是,它并不初始化内存,只是将其置零。也就是说,new(T)会为T类型的新项目,分配被置零的存储,并且返回它的地址,一个类型为*T的值。在Go的术语中,其返回一个指向新分配的类型为T的指针,这个指针指向的内容的值为zerovalue(zerovalue指的是各个类型的默认初始值),注意并不是指针为零,而是指针所指的内存.
比如,对于bool类型,零值为false;int的零值为0;string的零值是空字符串:
b := new(bool)
fmt.Println(*b)
i := new(int)
fmt.Println(*i)
输出:
false
0
- make
内建函数make(T,len,cap)与new(T)的用途不一样。它只用来创建slice,map和channel,并且返回一个初始化的(而不是置零)类型为T的值(而不是*T)。
之所以有所不同,是因为这三个类型的背后引用了使用前必须初始化的数据结构。例如,slice是一个三元描述符,包含一个指向数据(在数组中)的指针,长度,以及容量,在这些项被初始化之前,slice都是nil的。对于slice,map和channel,make初始化这些内部数据结构,并准备好可用的值。
goroutine 与 channel
- goroutine
类似我们熟知的线程,但是更轻。
以下的程序,我们串行地去执行两次loop函数:
func loop() {
for i := 0; i < 10; i++ {
fmt.Printf("%d ", i)
}
}
func main() {
loop()
loop()
}
毫无疑问,输出会是这样的:
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
下面我们把一个loop放在一个goroutine里跑,我们可以使用关键字go来定义并启动一个goroutine:
func main() {
go loop() // 启动一个goroutine
loop()
}
这次的输出变成了:
0 1 2 3 4 5 6 7 8 9
可是为什么只输出了一趟呢?明明我们主线跑了一趟,也开了一个goroutine来跑一趟啊。
原来,在goroutine还没来得及跑loop的时候,主函数已经退出了。
main函数退出地太快了,我们要想办法阻止它过早地退出,一个办法是让main等待一下:
func main() {
go loop()
loop()
time.Sleep(time.Second) // 停顿一秒
}
这次确实输出了两趟,目的达到了。
可是采用等待的办法并不好,如果goroutine在结束的时候,告诉下主线说“Hey, 我要跑完了!”就好了, 即所谓阻塞主线的办法,回忆下我们Python里面等待所有线程执行完毕的写法:
for thread in threads:
thread.join()
是的,我们也需要一个类似join的东西来阻塞住主线。那就是信道,将在下面的内容介绍。
- channel
channel是goroutine之间互相通讯的东西。类似我们Unix上的管道,用来goroutine之间发消息和接收消息。其实,就是在做goroutine之间的内存共享。- 无缓冲信道
使用make来建立一个信道:
默认的,信道的存消息和取消息都是阻塞的,叫做无缓冲的信道,也就是说, 无缓冲的信道在取消息和存消息的时候都会挂起当前的goroutine,除非另一端已经准备好。 比如以下的main函数和foo函数:var channel chan int = make(chan int) // 或 channel := make(chan int) ``` 那如何向信道存消息和取消息呢? 一个例子: ``` func main() { var messages chan string = make(chan string) go func(message string) { messages <- message // 存消息 }("Ping!") fmt.Println(<-messages) // 取消息 }
那既然信道可以阻塞当前的goroutine, 那么「如何让goroutine告诉主线我执行完毕了」 的问题, 使用一个信道来告诉主线即可:var ch chan int = make(chan int) func foo() { ch <- 0 // 向ch中加数据,如果没有其他goroutine来取走这个数据,那么挂起foo, 直到main函数把0这个数据拿走 } func main() { go foo() <- ch // 从ch取数据,如果ch中还没放数据,那就挂起main线,直到foo函数中放数据为止 }
如果不用信道来阻塞主线的话,主线就会过早跑完,loop线都没有机会执行var complete chan int = make(chan int) func loop() { for i := 0; i < 10; i++ { fmt.Printf("%d ", i) } complete <- 0 // 执行完毕了,发个消息 } func main() { go loop() <- complete // 直到线程跑完, 取到消息. main在此阻塞住 }
无缓冲信道从不存储数据,流入的数据必须要流出才可以。
观察以下的程序:
我们开了5个goroutine,然后又依次取数据。其实整个的执行过程细分的话,5个线的数据 依次流过信道ch, main打印之, 而宏观上我们看到的即 无缓冲信道的数据是先到先出,但是 无缓冲信道并不存储数据,只负责数据的流通.var ch chan int = make(chan int) func foo(id int) { //id: 这个routine的标号 ch <- id } func main() { // 开启5个routine for i := 0; i < 5; i++ { go foo(i) } // 取出信道中的数据 for i := 0; i < 5; i++ { fmt.Print(<- ch) } }
- 缓冲信道
缓冲这个词意思是,缓冲信道不仅可以流通数据,还可以缓存数据。它是有容量的,存入一个数据的话 , 可以先放在信道里,不必阻塞当前线而等待该数据取走。
当缓冲信道达到满的状态的时候,就会表现出阻塞了,因为这时再也不能承载更多的数据了,「你们必须把 数据拿走,才可以流入数据」。
在声明一个信道的时候,我们给make以第二个参数来指明它的容量(默认为0,即无缓冲):
var ch chan int = make(chan int, 2) // 写入2个元素都不会阻塞当前goroutine
存储个数达到2的时候会阻塞
如下的例子,缓冲信道ch可以无缓冲的流入3个元素:
如果你再试图流入一个数据的话,信道ch会阻塞main线, 报死锁。也就是说,缓冲信道会在满容量的时候加锁。func main() { ch := make(chan int, 3) ch <- 1 ch <- 2 ch <- 3 }
其实,缓冲信道是先进先出的,我们可以把缓冲信道看作为一个线程安全的队列:func main() { ch := make(chan int, 3) ch <- 1 ch <- 2 ch <- 3 fmt.Println(<-ch) // 1 fmt.Println(<-ch) // 2 fmt.Println(<-ch) // 3 }
- 信道数据读取和信道关闭
你也许发现,上面的代码一个一个地去读取信道简直太费事了,Go语言允许我们使用range来读取信道:
func main() { ch := make(chan int, 3) ch <- 1 ch <- 2 ch <- 3 for v := range ch { fmt.Println(v) } }
- 无缓冲信道
Interface
- 用法一:
假设某公司有两个员工,一个普通员工和一个高级员工,但是基本薪资是相同的,高级员工多拿奖金。计算公司为员工的总开支。
// 薪资计算器接口
type SalaryCalculator interface {
CalculateSalary() int
}
// 普通员工
type Contract struct {
empId int
basicpay int
}
// 有技术证的员工
type Permanent struct {
empId int
basicpay int
jj int // 奖金
}
func (p Permanent) CalculateSalary() int {
return p.basicpay + p.jj
}
func (c Contract) CalculateSalary() int {
return c.basicpay
}
// 总开支
func totalExpense(s []SalaryCalculator) {
expense := 0
for _, v := range s {
expense = expense + v.CalculateSalary()
}
fmt.Printf("总开支 $%d", expense)
}
func main() {
pemp1 := Permanent{1,3000,10000}
pemp2 := Permanent{2, 3000, 20000}
cemp1 := Contract{3, 3000}
employees := []SalaryCalculator{pemp1, pemp2, cemp1}
totalExpense(employees)
}
- 用法二:
type Test interface {
Tester()
}
type MyFloat float64
func (m MyFloat) Tester() {
fmt.Println(m)
}
func describe(t Test) {
fmt.Printf("Interface 类型 %T , 值: %v\n", t, t)
}
func main() {
var t Test
f := MyFloat(89.7)
t = f
describe(t)
t.Tester()
}
- 空接口
具有0个方法的接口称为空接口。它表示为interface{}。由于空接口有0个方法,所有类型都实现了空接口。
func describe(i interface{}) {
fmt.Printf("Type = %T, value = %v\n", i, i)
}
func main() {
// 任何类型的变量传入都可以
s := "Hello World"
i := 55
strt := struct {
name string
}{
name: "Naveen R",
}
describe(s)
describe(i)
describe(strt)
}
### defer,panic和recover - defer 在函数return之后,返回调用它的函数之前运行 - panic 当golang中遇到panic时,如果不进行recover,便会导致整个程序挂掉 - recover golang 的错误处理流程:当一个函数在执行过程中出现了异常或遇到panic(),正常语句就会立即终止,然后执行 defer语句,再报告异常信息,最后退出goroutine。如果在defer中使用了recover()函数,则会捕获错误信息,使该错误信息终止报告。 ``` func main() { i := 10000 for j := 0; j < 3; j++ { // 使用多协程处理,其中可以预见的是除数为0会抛出异常 divide(i, j) } }
func divide(i, j int) {
defer func() {
if r := recover(); r != nil {
// 这里可以对异常进行一些处理和捕获
fmt.Println(“Recovered:”, r)
}
}()
fmt.Println(i / j) // 第一次触发除0异常,panic
}
运行结果:
10000
5000
<br>
当有多个defer的时候,出现故障,此时跳转到包含recover()的defer函数执行
func main() {
defer func() {
fmt.Println(“1”)
}()
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
}
}()
panic(“fault”)
fmt.Println(“2”)
}
运行结果:
fault
1
### 类型断言 与 类型判断
- 类型断言
类型断言用于提取接口的基础值,语法:i.(T)
func assert(i interface{}){
s:= i.(int)
fmt.Println(s)
}
func main(){
var s interface{} = 55
assert(s)
}
程序打印的是int值, 但是如果我们给s 变量赋值的是string类型,程序就会panic。
当程序改为:
func assert(i interface{}) {
v, ok := i.(int)
fmt.Println(v, ok)
}
func main() {
var s interface{} = 56
assert(s)
var i interface{} = “Steven Paul”
assert(i)
}
如果 i 的值是int类型, 那么v就是i 对应的值,ok就是true。否则ok为false,程序并不会panic。
- 类型判断
类型判断的语法类似于类型断言。在类型断言的语法i.(type)中,类型type应该由类型转换的关键字type替换。
func findType(i interface{}) {
switch i.(type) {
case string:
fmt.Printf(“String: %s\n”, i.(string))
case int:
fmt.Printf(“Int: %d\n”, i.(int))
default:
fmt.Printf(“Unknown type\n”)
}
}
func main() {
findType(“Naveen”)
findType(77)
findType(89.98)
}
<br>
还可以将类型与接口进行比较。如果我们有一个类型并且该类型实现了一个接口,那么可以将它与它实现的接口进行比较。
type Describer interface {
Describe()
}
type Person struct {
name string
age int
}
func (p Person) Describe() {
fmt.Printf("%s is %d years old", p.name, p.age)
}
func findType(i interface{}) {
switch v := i.(type) {
case Describer:
v.Describe()
default:
fmt.Printf(“unknown type\n”)
}
}
func main() {
findType(“Naveen”)
p := Person{
name: “Naveen R”,
age: 25,
}
findType§
}
输出结果:
unknown type
Naveen R is 25 years old
<br>
当一个类型实现了多个接口,switch case按照编写的顺序优先执行,如以下代码:
type Describer interface {
Describe()
}
type Describer2 interface {
Describe2()
}
type Person struct {
name string
age int
}
func (p Person) Describe() {
fmt.Println(“describe”)
}
func (p Person) Describe2() {
fmt.Println(“describe2”)
}
func findType(i interface{}) {
switch v := i.(type) {
case Describer2:
v.Describe2()
case Describer:
v.Describe()
default:
fmt.Printf(“unknown type\n”)
}
}
func main() {
p := Person{
name: “Naveen R”,
age: 25,
}
findType§
}
输出结果:
describe2