Base
- GOROOT: golang的安装路径
- GOPATH: 工作目录
- "_": 空白标识符
Go 不是像 C ++,Java,Ruby 和 C#一样的面向对象的(OO)语言。它没有对象和继承的概念,也没有很多与面向对象相关的概念,例如多态和重载。
Go没有类或者说面向对象的概念, 结构体就是Go里面的类
镜像复制
func main() {
goku := Saiyan{"Power", 9000}
Super(goku)
fmt.Println(goku.Power)
}
func Super(s Saiyan) {
s.Power += 10000
}
上面程序运行的结果是 9000,而不是 19000,。为什么?因为 Super 修改了原始值 goku 的复制版本,而不是它本身
&: 取地址符
func main() {
goku := &Saiyan{"Power", 9000}
Super(goku)
fmt.Println(goku.Power)
}
func Super(s *Saiyan) {
s.Power += 10000
}
这里注意到我们仍然传递了一个 goku 的值的副本给 Super,但这时 goku 的值其实是一个地址。所以这个副本值也是一个与原值相等的地址,
这就是我们间接传值的方式。想象一下,就像复制一个指向饭店的方向牌。你所拥有的是一个方向牌的副本,但是它仍然指向原来的饭店。
复制一个指针比复制一个复杂的结构的消耗小多了
Go结构体使用组合的方式, 代替面向对象中的继承
指针类型和值类型(指针类型就是类似对象了, 分配在堆上的内存)
当不确定要用值还是指针时, 就用指针
数组
在 Go 中,像其它大部分语言一样,数据的长度是固定的。我们在声明一个数组时需要指定它的长度,一旦指定了长度,那么它的长度值是不可以改变的了
var scores [10]int
scores[0] = 339
数组非常高效但是很死板
切片
在 Go 语言中,我们很少直接使用数组。取而代之的是使用切片。切片是轻量的包含并表示数组的一部分的结构。
//和数组声明不同的是,我们的切片没有在方括号中定义长度
scores := []int{1,4,293,4,9}
- make方式创建切片
scores := make([]int, 10)- 使用 make 关键字代替 new, 是因为创建一个切片不仅是只分配一段内存(这个是 new 关键字的功能)。
- 具体来讲,我们必须要为一个底层数组分配一段内存,同时也要初始化这个切片。
- 上面的代码中,我们初始化了一个长度是 10 ,容量是 10 的切片
- 长度是切片的长度,容量是底层数组的长度
- 在使用 make 创建切片时,我们可以分别的指定切片的长度和容量:
scores := make([]int, 0, 10)
- 上面的代码创建了一个长度是 0 ,容量是 10 的切片
理解切片的长度和容量之间的关系
切片就是从数组切一段下来, 切片的长度就是标识切了多长的数组, 容量就是底层数组的长度
我们可以调整的切片大小最大范围是多少呢?达到它的容量, 就像例子中的10
这实际上并没有解决数组固定长度的问题。但是 append 是相当特别的。如果底层数组满了,它将创建一个更大的数组并且复制所有原切片中的值
Go 使用 2x 算法来增加数组长度(加倍容量)
四种方式创建切片
- names := []string{"leto", "jessica", "paul"} //字面量方式
- checks := make([]bool, 10) //知道容量
- var names []string //创建空切片
- scores := make([]int, 0, 20) //创建指定容量的切片
切片的内存理解
其他语言的切片 ---> 一个切片实际上是复制了原始值的新数组。
scores = [1,2,3,4,5]
slice = scores[2..4]
slice[0] = 999
puts scores
//答案是 [1, 2, 3, 4, 5] 。那是因为 slice 是一个新数组,并且复制了原有的值。
Go ---> 切片是指向原始数组的指针
scores := []int{1,2,3,4,5}
slice := scores[2:4]
slice[0] = 999
fmt.Println(scores)
//输出是 [1, 2, 999, 4, 5]
只有显示的使用copy内建函数, 才会复制新数组
总结
// 数组是连续存储的空间 切片只存放指针 没有存放数据 所以 指针的位置
// 只收上界影响,也就是数据的基址, 他的操作范围 只能从指针开始到连续的内存结束 如果超过容量 扩容后
// 就产生了新的数组和内存空间 切片的指针也会指向新的内存
映射
看错了, 以为是反射
映射就是字典, 使用 make 方法来创建
- 使用 len 方法类获取映射的键的数量
- 使用 delete 方法来删除一个键对应的值
- 映射是动态变化的
- 通过传递第二个参数到 make 方法来设置一个初始大小(如果你事先知道映射会有多少键值,定义一个初始大小将会帮助改善性能。)
包管理
在 Go 语言中,包名遵循 Go 项目的目录结构。(意思是目录就是包名)
- 循环导入问题
- 可见性, 如果类型和函数的名字是大写就是外界可见的, 小写的名字就是外界不可见
- go get 库链接(会把下载下来的库, 放在工作目录GOPATH)
- 依赖管理 go get -u 包更新
go get的缺点: 无法指定版本
接口
目前为止,我们看到的类型都是具体的类型,一个具体的类型可以准确的描述它所代表的值。而接口类型是一种抽象的类型。它不会暴露出它所代表的对象的内部值的结构和这个对象支持的基础操作的集合;它们只会表现出它们自己的方法。也就是说当你有看到一个接口类型的值时,你不知道它是什么,唯一知道的就是可以通过它的方法来做什么。
type Namer interface {
Method1(param_list) return_type
Method2(param_list) return_type
...
}
- interface可以被任意的对象实现
- 一个对象可以实现任意多个interface
- 任意的类型都实现了空interface(我们这样定义:interface{}),也就是包含0个method的interface
接口有助于将代码与特定的实现进行分离(按照iOS的Delegate来理解)
接口就是一种协议,和现实中的接口一个道理。
一提到usbc你就知道那个接口长什么样,但是你不关心usbc芯片是什么样的,怎么接线。
代码通过接口定义来表明自己对外开放的能力,具体的实现则交给实现这个接口的类。
只要实现了这个接口的类,都一定存在对应的方法,那么我拿到这个类的实例对象的时候,就一定可以调用这个对象所实现的方法。
具体的好处需要多实践才好领会,
比如ios编程常见的委托,java常用的控制反转之类。
如果写过http接口,实际上也是一样的道理。
type Logger interface {
Log(message string)
}
错误处理
Go 首选错误处理方式是返回值,而不是异常。
作为最后一点,Go 确实有 panic 和 recover 函数。 panic 就像抛出异常,而 recover 就像 catch,它们很少使用。
defer
尽管 Go 有一个垃圾回收器,一些资源仍然需要我们显式地释放他们。例如,我们需要在使用完文件之后 Close() 他们。
这种代码总是很危险。一方面来说,当我们在写一个函数的时候,很容易忘记关闭我们声明了 10 行的东西。
另一方面,一个函数可能有多个返回点。Go 给出的解决方案是使用 defer 关键字:
无论什么情况,在函数返回之后(本例中为 main() ),defer 将被执行。这使您可以在初始化的位置附近释放资源并处理多个返回点。
go fmt
格式化代码
当你在一个项目内的时候,你可以运用格式化规则到这个项目及其所有子目录:
go fmt ./...
空接口和转换
字符串和字节数组
函数类型
函数是一种类型:
type Add func(a int, b int) int
可以作为参数传递
并发
Go 通常被描述为一种并发友好的语言。 原因是它提供了两种强大机制的简单语法: 协程 和 通道
package main
import (
"fmt"
"time"
)
func main() {
fmt.Println("start")
go process()
time.Sleep(time.Millisecond * 10) // this is bad, don't do this!
fmt.Println("done")
}
func process() {
fmt.Println("processing")
}
- 开始一个 协程 。 我们只需使用 go 关键字,然后使用我们想要执行的函数。
- 协程 易于创建且开销很小。最终多个 协程 将会在同一个底层的操作系统线程上运行。 这通常也称为 M:N 线程模型,因为我们有 M 个应用线程( 协程 )运行在 N 个操作系统线程上。结果就是,一个 协程 的开销和系统线程比起来相对很低(几 KB)。在现代的硬件上,有可能拥有数百万个 协程 。
同步
并发编程中的问题:
- 变量会被其他线程读写, 如果保证读写安全
- 加锁的效率问题: 需要个优雅的锁操作; 否则,我们最终会把多条快速通道走成单车道的。
- 加锁死锁的问题: 当协程 A 拥有锁 lockA ,想去访问锁 lockB ,同时协程 B 拥有锁 lockB 并需要访问锁 lockA 。
解决办法:
- 有一个常见的锁叫读写互斥锁。它主要提供了两种锁功能:一个锁定读取和一个锁定写入。它的区别是允许多个同时读取,同时确保写入是独占的。在 Go 中, sync.RWMutex 就是这种锁。
- 通道 旨在让并发编程更简洁和不容易出错。(加锁容易出问题)
通道
而Go是通过一种特殊的类型,通道(channel),一个可以用于发送类型化数据的管道,由其负责协程之间的通信,从而避开所有由共享内存导致的陷阱;这种通过通道进行通信的方式保证了同步性。数据在通道中进行传递:在任何给定时间,一个数据被设计为只有一个协程可以对其访问,所以不会发生数据竞争。 数据的所有权(可以读写数据的能力)也因此被传递。
- 通道操作符 <-
并发编程的最大调整源于数据的共享。如跨多个请求共享数据。内存缓存和数据库
- 通道是协程之间用于传递数据的管道
换而言之,一个协程可以通过一个通道向另外一个协程传递数据。因此,在任意时间点,只有一个协程可以访问数据。
- 一个通道,和其他任何变量一样,都有一个类型,这个类型是在通道中传递的数据的类型。例如,创建一个通道用于传递一个整数,我们要这样做:
c := make(chan int)
- 通道只支持两个操作:接收和发送。
- 接收和发送操作是阻塞的。
也就是,当我们从一个通道接收的时候, goroutine 将会直到数据可用才会继续执行。类似地,当我们往通道发送数据的时候,goroutine 会等到数据接收到之后才会继续执行。
- Go 保证了发送到通道的数据只会被一个接收器接收。(保证多线程数据安全, 读写的唯一)
通道阻塞
默认情况下,通信是同步且无缓冲的:在有接受者接收数据之前,发送不会结束。
一个无缓冲的通道在没有空间来保存数据的时候:必须要一个接收者准备好接收通道的数据然后发送者可以直接把数据发送给接收者。
所以通道的发送/接收操作在对方准备好之前是阻塞的:
- 对于同一个通道,发送操作(协程或者函数中的)在接收者准备好之前是阻塞的:如果ch中的数据无人接收,就无法再给通道传入其他数据:新的输入无法在通道非空的情况下传入。所以发送操作会等待 ch 再次变为可用状态:就是通道值被接收时就可以再传入变量。
- 对于同一个通道,接收操作是阻塞的(协程或函数中的),直到发送者可用:如果通道中没有数据,接收者就阻塞了。
缓冲通道
一个无缓冲通道只能包含 1 个元素,有时显得很局限。我们给通道提供了一个缓存,可以在扩展的 make 命令中设置它的容量
buf := 100
ch1 := make(chan string, buf)
- 在缓冲满载(缓冲被全部使用)之前,给一个带缓冲的通道发送数据是不会阻塞的,而从通道读取数据也不会阻塞,直到缓冲空了。
- 缓冲容量和类型无关,所以可以给一些通道设置不同的容量,只要他们拥有同样的元素类型。内置的 cap 函数可以返回缓冲区的容量。
- 如果容量大于 0,通道就是异步的:因为缓冲满载(发送)或变空(接收)之前通信不会阻塞,元素会按照发送的顺序被接收。
- 如果容量是0或者未设置,通道就是同步的:通信仅在收发双方准备好的情况下才可以成功。
通道注意事项:
- channel 在 Golang 中是一等公民,它是线程安全的,面对并发问题,应首先想到 channel
- 关闭一个未初始化的 channel 会产生 panic
- 重复关闭同一个 channel 会产生 panic
- 向一个已关闭的 channel 发送消息会产生 panic
- 从已关闭的 channel 读取消息不会产生 panic,且能读出 channel 中还未被读取的消息,若消息均已被读取,则会读取到该类型的零值。
- 从已关闭的 channel 读取消息永远不会阻塞,并且会返回一个为 false 的值,用以判断该 channel 是否已关闭(x,ok := <- ch)
- 关闭 channel 会产生一个广播机制,所有向 channel 读取消息的 goroutine 都会收到消息
Select
select 是 Go 中的一个控制结构。select 语句类似于 switch 语句,但是select会随机执行一个可运行的case。如果没有case可运行,它将阻塞,直到有case可运行。
Select语句的作用(注意:它仅能用于channel的相关操作,select 里的 case 表达式要求是对信道的操作)
当我们需要同时从多个通道接收数据时,如果通道里没有数据将会发生阻塞。为了应对这种场景,Go内置了select关键字,可以同时响应多个通道的操作。
package main
import (
"fmt"
)
func main() {
c1 := make(chan string, 1)
c2 := make(chan string, 1)
c2 <- "hello"
select {
case msg1 := <-c1:
fmt.Println("c1 received: ", msg1)
case msg2 := <-c2:
fmt.Println("c2 received: ", msg2)
default:
fmt.Println("No data received.")
}
}
//c2 received: hello
select语句的处理逻辑:case随机选择处理列出的多个通信情况中的一个
- 如果都阻塞了,会等待直到其中一个可以处理
- 如果多个可以处理,随机选择一个
- 为了避免死锁,应该编写default分支或手动实现超时机制,否则select 整体就会一直阻塞
- select 的 case 是随机的,而 switch 里的 case 是顺序执行;
- select 里没有类似 switch 里的 fallthrough 的用法;
- select 里的 case 表达式要求是对信道的操作,不能像 switch 一样接函数或其他表达式;