error 接口是 Go 原生内置的类型,它的定义如下:
// $GOROOT/src/builtin/builtin.go
type error interface {
Error() string
}
任何实现了 error 的 Error 方法的类型的实例,都可以作为错误值赋值给 error 接口变量。
Go 中有两个常用的函数可生成 error 类型变量:
errors.New
fmt.Errorf
使用示例:
func doSomething(...) error {
... ...
return errors.New("some error occurred")
}
判断错误的最常用方式:
err := doSomething()
if err != nil {
// 不关心err变量底层错误值所携带的具体上下文信息
// 执行简单错误处理逻辑并返回
... ...
return err
}
上面的方式,调用者并不关心具体的错误信息。
通过下面的方式可以针对不同的错误信息,做出不同的处理逻辑。
data, err := b.Peek(1)
if err != nil {
switch err.Error() {
case "bufio: negative count":
// ... ...
return
case "bufio: buffer full":
// ... ...
return
case "bufio: invalid use of UnreadByte":
// ... ...
return
default:
// ... ...
return
}
}
但是上面的方式严重依赖错误信息,一旦错误信息发生改变,调用者就得跟着改变。
Go 1.13 及后续版本,建议使用的错误判断方法:
errors.Is
方法去检视某个错误值是否就是某个预期错误值errors.As
方法去检视某个错误值是否是某自定义错误类型的实例对于 errors.Is
方法,如果 error 类型变量的底层错误值是一个包装错误(Wrapped Error),errors.Is
方法会沿着该包装错误所在错误链(Error Chain),与链上所有被包装的错误(Wrapped Error)进行比较,直至找到一个匹配的错误为止。
例子:
var ErrSentinel = errors.New("the underlying sentinel error")
func main() {
// 用 %w 来包装错误
err1 := fmt.Errorf("wrap sentinel: %w", ErrSentinel)
err2 := fmt.Errorf("wrap err1: %w", err1)
println(err2 == ErrSentinel) // false
if errors.Is(err2, ErrSentinel) { // true
println("err2 is ErrSentinel")
return
}
println("err2 is not ErrSentinel")
}
对于 errors.As
方法,使用模式:
// 类似 if e, ok := err.(*MyError); ok { … }
var e *MyError
if errors.As(err, &e) {
// 如果err类型为*MyError,变量e将被设置为对应的错误值
}
如果 error 类型变量的动态错误值是一个包装错误,errors.As
函数会沿着该包装错误所在错误链,与链上所有被包装的错误的类型进行比较,直至找到一个匹配的错误类型,就像 errors.Is
函数那样。
type MyError struct {
e string
}
func (e *MyError) Error() string {
return e.e
}
func main() {
var err = &MyError{"MyError error demo"}
err1 := fmt.Errorf("wrap err: %w", err)
err2 := fmt.Errorf("wrap err1: %w", err1)
var e *MyError
if errors.As(err2, &e) { // true
println("MyError is on the chain of err2")
// 要特别注意这里,并不是将 err2 赋给了 e
// 而是将 err2 链上匹配上的值 err 赋给了 e
// 因此 err == e
println(e == err) // true
return
}
println("MyError is not on the chain of err2")
}
panic 指的是 Go 程序在运行时出现的一个异常情况。如果异常出现了,但没有被捕获并恢复,Go 程序的执行就会被终止。
panic 主要有两类来源:
当函数 F 调用 panic 函数时,函数 F 的执行将停止。不过,函数 F 中已进行求值的 deferred 函数都会得到正常执行,执行完这些 deferred 函数后,函数 F 才会把控制权返还给其调用者。
在 Go 标准库中,大多数 panic 的使用都是充当类似断言的作用的。
在 Go 中,作为 API 函数的作者,你一定不要将 panic 当作错误返回给 API 调用者。
一个例子:
func foo() {
println("call foo")
bar()
println("exit foo")
}
func bar() {
println("call bar")
panic("panic occurs in bar")
zoo()
println("exit bar")
}
func zoo() {
println("call zoo")
println("exit zoo")
}
func main() {
println("call main")
foo()
println("exit main")
}
其输出结果如下:
call main
call foo
call bar
panic: panic occurs in bar
在整个程序中,都没有对 panic 异常进行捕捉,所以在遇到 panic 异常后,程序就退出了。
在 Go 中使用 recover 函数对 panic 异常进行捕捉。
// 在一个 defer 匿名函数中调用 recover 函数对 panic 进行捕捉
func bar() {
defer func() {
if e := recover(); e != nil {
fmt.Println("recover the panic:", e)
}
}()
println("call bar")
panic("panic occurs in bar")
zoo()
println("exit bar")
}
捕捉异常后的程序运行结果:
call main
call foo
call bar
recover the panic: panic occurs in bar
exit foo
exit main
将 panic 作为断言方式使用:
// $GOROOT/src/encoding/json/encode.go
func (w *reflectWithString) resolve() error {
... ...
switch w.k.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
w.ks = strconv.FormatInt(w.k.Int(), 10)
return nil
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
w.ks = strconv.FormatUint(w.k.Uint(), 10)
return nil
}
// 正常情况下,程序不会走到这里
// 如果走到了这里,说明出现了问题
// 相当于 assert 的作用
panic("unexpected map key type")
}
注意,Go 中 panic 并不同于 Java,Python 中的 raise 异常,所以不要将 panic 像 Exception 一样使用。 就是,作为 Go API 函数的作者,一定不要将 panic 当作错误返回给 API 调用者。
defer 是 Go 语言提供的一种延迟调用机制,defer 的运作离不开函数。
无论是执行到函数体尾部返回,还是在某个错误处理分支显式 return,又或是出现 panic,已经存储到 deferred 函数栈中的函数,都会被调度执行。所以说,deferred 函数是一个可以在任何情况下为函数进行收尾工作的好“伙伴”。
使用 defer 的一个示例:
func doSomething() error {
var mu sync.Mutex
mu.Lock()
defer mu.Unlock()
r1, err := OpenResource1()
if err != nil {
return err
}
defer r1.Close()
r2, err := OpenResource2()
if err != nil {
return err
}
defer r2.Close()
r3, err := OpenResource3()
if err != nil {
return err
}
defer r3.Close()
// 使用r1,r2, r3
return doWithResources()
}
对于自定义的函数或方法,defer 可以给与无条件的支持,但是对于有返回值的自定义函数或方法,返回值会在 deferred 函数被调度执行的时候被自动丢弃。
不是所有的内置函数都能作为 deffered 函数
对于那些不能直接作为 deferred 函数的内置函数,我们可以使用一个包裹它的匿名函数来间接满足要求,以 append 为例是这样的:
defer func() {
_ = append(sl, 11)
}()
Go 语言原生支持并发,Go 并发这个词,它包含两方面内容:
goroutine 是由 Go 运行时(runtime)负责调度的、轻量的用户级线程,为并发程序设计提供原生支持。
Go 语言通过 go关键字+函数/方法
的方式创建一个 goroutine。创建后,新 goroutine 将拥有独立的代码执行流,并与创建它的 goroutine 一起被 Go 运行时调度。
多数情况下,我们不需要考虑对 goroutine 的退出进行控制:goroutine 的执行函数的返回,就意味着 goroutine 退出。
如果 main goroutine 退出了,那么也意味着整个应用程序的退出。
此外,你还要注意的是,goroutine 执行的函数或方法即便有返回值,Go 也会忽略这些返回值。所以,如果你要获取 goroutine 执行后的返回值,你需要另行考虑其他方法,比如通过 goroutine 间的通信来实现。
channel 既可以用来实现 Goroutine 间的通信,还可以实现 Goroutine 间的同步。
channel 是用于 Goroutine 间通信的,所以绝大多数对 channel 的读写都被分别放在了不同的 Goroutine 中。
Go 在语法层面将并发原语 channel 作为一等公民对待,使得我们可以像使用普通变量那样使用 channel,比如:
channel 也是一种复合数据类型,在声明一个 channel 类型变量时,必须给出其具体的元素类型:
// 声明一个元素为 int 类型的 channel 类型变量 ch
var ch chan int
如果 channel 类型变量在声明时没有被赋予初值,那么它的默认值为 nil。
为 channel 类型变量赋初值的唯一方法就是使用 make 函数:
ch1 := make(chan int) // 无缓冲 channel
ch2 := make(chan int, 5) // 有缓冲 channel,缓冲区长度是 5
上面两种类型的变量关于发送(send)与接收(receive)的特性是不同的。
Go 提供了<-
操作符用于对 channel 类型变量进行发送与接收操作:
ch1 <- 13 // 将整型字面值13发送到无缓冲channel类型变量ch1中
n := <- ch1 // 从无缓冲channel类型变量ch1中接收一个整型值存储到整型变量n中
ch2 <- 17 // 将整型字面值17发送到带缓冲channel类型变量ch2中
m := <- ch2 // 从带缓冲channel类型变量ch2中接收一个整型值存储到整型变量m中
无缓冲 channel 的运行时层实现不带有缓冲区,所以 Goroutine 对无缓冲 channel 的接收和发送操作是同步的。也就是说,对同一个无缓冲 channel,只有对它进行接收操作的 Goroutine 和对它进行发送操作的 Goroutine 都存在的情况下,通信才能得以进行,否则单方面的操作会让对应的 Goroutine 陷入挂起状态,比如下面示例代码:
// 这里创建了一个无缓冲的 channel 类型变量 ch1,对 ch1 的读写都放在了一个 Goroutine 中
func main() {
ch1 := make(chan int)
ch1 <- 13 // fatal error: all goroutines are asleep - deadlock!
n := <-ch1
println(n)
}
因此上面代码要进行如下改进:
func main() {
ch1 := make(chan int)
go func() {
ch1 <- 13 // 将发送操作放入一个新goroutine中执行
}()
n := <-ch1
println(n)
}
带缓冲 channel 的运行时层实现带有缓冲区,因此,对带缓冲 channel 的发送操作在缓冲区未满、接收操作在缓冲区非空的情况下是异步的(发送或接收不需要阻塞等待)。
示例:
ch2 := make(chan int, 1)
n := <-ch2 // 由于此时ch2的缓冲区中无数据,因此对其进行接收操作将导致goroutine挂起
ch3 := make(chan int, 1)
ch3 <- 17 // 向ch3发送一个整型数17
ch3 <- 27 // 由于此时ch3中缓冲区已满,再向ch3发送数据也将导致goroutine挂起
我们还可以声明只发送 channel 类型(send-only)和只接收 channel 类型(recv-only):
ch1 := make(chan<- int, 1) // 只发送channel类型
ch2 := make(<-chan int, 1) // 只接收channel类型
<-ch1 // invalid operation: <-ch1 (receive from send-only type chan<- int)
ch2 <- 13 // invalid operation: ch2 <- 13 (send to receive-only type <-chan int)
试图从一个只发送 channel 类型变量中接收数据,或者向一个只接收 channel 类型发送数据,都会导致编译错误。
通常只发送 channel 类型和只接收 channel 类型,会被用作函数的参数类型或返回值,用于限制对 channel 内的操作,或者是明确可对 channel 进行的操作的类型。
例如下面生产者和消费者的例子:
func produce(ch chan<- int) {
for i := 0; i < 10; i++ {
ch <- i + 1
time.Sleep(time.Second)
}
close(ch)
}
func consume(ch <-chan int) {
for n := range ch {
println(n)
}
}
func main() {
ch := make(chan int, 5)
var wg sync.WaitGroup
wg.Add(2)
go func() {
produce(ch)
wg.Done()
}()
go func() {
consume(ch)
wg.Done()
}()
wg.Wait()
}
在这个例子中:
n := <- ch // 当ch被关闭后,n将被赋值为ch元素类型的零值
m, ok := <-ch // 当ch被关闭后,m将被赋值为ch元素类型的零值, ok值为false
for v := range ch { // 当ch被关闭后,for range循环结束
... ...
}
channel 的一个使用惯例,那就是发送端负责关闭 channel。
一旦向一个已经关闭的 channel 执行发送操作,这个操作就会引发 panic,比如:
ch := make(chan int, 5)
close(ch)
ch <- 13 // panic: send on closed channel
无缓冲 channel 兼具通信和同步特性,在并发程序中应用颇为广泛。
第一种用法:用作信号传递
无缓冲 channel 用作信号传递的时候,有两种情况,分别是 1 对 1 通知信号和 1 对 n 通知信号。
第二种用法:用于替代锁机制
无缓冲 channel 具有同步特性,这让它在某些场合可以替代锁,让我们的程序更加清晰,可读性也更好。
带缓冲的 channel 与无缓冲的 channel 的最大不同之处,就在于它的异步性。也就是说,对一个带缓冲 channel,在缓冲区未满的情况下,对它进行发送操作的 Goroutine 不会阻塞挂起;在缓冲区有数据的情况下,对它进行接收操作的 Goroutine 也不会阻塞挂起。
那我们是否可以使用 len 函数来实现带缓冲 channel 的“判满”、“判有”和“判空”逻辑呢?
var ch chan T = make(chan T, capacity)
// 判空
if len(ch) == 0 {
// 此时channel ch空了?
}
// 判有
if len(ch) > 0 {
// 此时channel ch中有数据?
}
// 判满
if len(ch) == cap(ch) {
// 此时channel ch满了?
}
channel 原语用于多个 Goroutine 间的通信,一旦多个 Goroutine 共同对 channel 进行收发操作,len(channel) 就会在多个 Goroutine 间形成“竞态”。
单纯地依靠 len(channel) 来判断 channel 中元素状态,是不能保证在后续对 channel 的收发时 channel 状态是不变的。
如果一个 channel 类型变量的值为 nil,我们称它为 nil channel,对 nil channel 的读写都会发生阻塞。
func main() {
var c chan int
<-c //阻塞
}
或者:
func main() {
var c chan int
c<-1 //阻塞
}
当涉及同时对多个 channel 进行操作时,我们会结合另外一个原语 select,一起使用。
通过 select,我们可以同时在多个 channel 上进行发送 / 接收操作:
select {
case x := <-ch1: // 从channel ch1接收数据
... ...
case y, ok := <-ch2: // 从channel ch2接收数据,并根据ok值判断ch2是否已经关闭
... ...
case ch3 <- z: // 将z值发送到channel ch3中:
... ...
default: // 当上面case中的channel通信均无法实施时,执行该默认分支
}
当 select 语句中没有 default 分支,而且所有 case 中的 channel 操作都阻塞了的时候,整个 select 语句都将被阻塞,直到某一个 case 上的 channel 变成可发送,或者某个 case 上的 channel 变成可接收,select 语句才可以继续进行下去。
Go 语言之父 Rob Pike 还有一句经典名言:“不要通过共享内存来通信,应该通过通信来共享内存”。这就奠定了 Go 应用并发设计的主流风格:使用 channel 进行不同 Goroutine 间的通信。
一般情况下,建议优先使用 channel 并发模型进行并发程序设计。
不过,Go 也并没有彻底放弃基于共享内存的并发模型,而是在提供 CSP 并发模型原语的同时,还通过标准库的:
Mutex 的使用示例:
var mu sync.Mutex
mu.Lock() // 加锁
doSomething()
mu.Unlock() // 解锁
RWMutex 的使用示例:
var rwmu sync.RWMutex
rwmu.RLock() //加读锁
readSomething()
rwmu.RUnlock() //解读锁
rwmu.Lock() //加写锁
changeSomething()
rwmu.Unlock() //解写锁
其实,面向 CSP 并发模型的 channel 原语和面向传统共享内存并发模型的 sync 包提供的原语,已经能够满足 Go 语言应用并发设计中 99.9% 的并发同步需求了。而剩余那 0.1% 的需求,我们可以使用 Go 标准库提供的 atomic 包来实现。
Go 为开发人员提供了阻塞 I/O 模型,Gopher 只需在 Goroutine 中以最简单、最易用的“阻塞 I/O 模型”的方式,进行 Socket 操作就可以。
但这种方式是 Go 模拟出来,是为了让开发者使用起来更加简单易懂,对应的、真实的底层操作系统 Socket,实际上是非阻塞的。
Go 没有使用基于线程的并发模型,而是使用了开销更小的 Goroutine 作为基本执行单元,这让每个 Goroutine 处理一个 TCP 连接成为可能,并且在高并发下依旧表现出色。
虽然目前主流 socket 网络编程模型是 I/O 多路复用模型,但考虑到这个模型在使用时的体验较差,Go 语言将这种复杂性隐藏到运行时层,并结合 Goroutine 的轻量级特性,在用户层提供了基于 I/O 阻塞模型的 Go socket 网络编程模型,这一模型就大大简化了编程难度。
Go Server 端编程套路模板:
func handleConn(c net.Conn) {
defer c.Close()
for {
// read from the connection
// ... ...
// write to the connection
//... ...
}
}
func main() {
l, err := net.Listen("tcp", ":8888")
if err != nil {
fmt.Println("listen error:", err)
return
}
for {
c, err := l.Accept()
if err != nil {
fmt.Println("accept error:", err)
break
}
// start a new goroutine to handle
// the new connection.
go handleConn(c)
}
}
Go Client 端与 Server 端建立连接的两种方式:
conn, err := net.Dial("tcp", "localhost:8888")
// 带有超时机制的建连
conn, err := net.DialTimeout("tcp", "localhost:8888", 2 * time.Second)
net.Dial 函数的第一个参数的可选值,共九个:
从 Socket 读数据的三种情况:
SetReadDeadline(time.Time{})
可用于取消超时时间当 Write 调用的返回值 n 的值,与预期要写入的数据长度相等,且 err = nil 时,我们就执行了一次成功的 Socket 写操作,这是我们在调用 Write 时遇到的最常见的情形。
其它情况还有:
当客户端主动关闭了 Socket,那么服务端的Read调用将会读到什么呢?这里要分“有数据关闭”和“无数据关闭”两种情况。
在 Go 语言中,string 类型的值是不可变的。在进行字符串拼接的时候,Go 语言会把所有被拼接的字符串依次拷贝到一个崭新且足够大的连续内存空间中,并把持有相应指针值的 string 值作为结果返回。
因此,当程序中存在过多的字符串拼接操作的时候,会对内存的分配产生非常大的压力。
与 string 相比,Builder 的优势主要体现在字符串拼接方面。与 string 一样,Builder 的底层也是一个 byte 类型的切片(字节切片),Builder 会按需扩容,不必每次拼接都需要拷贝。
使用 StringBuffer 或是 StringBuild 来拼接字符串,会比使用 + 或 += 性能高三到四个数量级。
当一个 Builder 中的空间够用时,其不会发生扩容,当不够用时,其会自动进行扩容。发生扩容时,会产生新的更大的内存空间,并将旧空间中的数据拷贝到新空间。
Builder 对象中的方法:
strings.Reader 类型是为了高效读取字符串而存在的。
Reader 对象中的方法:
Go 的 strings 包中有很多字符串相关操作函数:
strings.Split
:字符串分割strings.Join
:字符串连接strings.Count
:计数strings.Compare
:字符串比较strings.Contains
:是否包含strings.HasPrefix
:是否包含前缀strings.HasSuffix
:是否包含后缀strings.Index
:某字符的位置,不存在返回 -1strings.LastIndex
strings.Repeat
:重复字符串几次strings.Replace
:字符串替换strings.Split
:字符串分割strings.Trim:
字符串去除某字符strings.TrimLeft
strings.TrimRight
strings.TrimSpace
:去除首尾空格strings.ToLower
:转小写strings.ToUpper
:转大写strings.Title
:首字母转大写strings.Fields
:字符串变数组,将字符串以空白字符分割strings 包主要面向的是 Unicode 字符和经过 UTF-8 编码的字符串,而 bytes 包面对的则主要是字节和字节切片。
在内部,bytes.Buffer 类型同样是使用字节切片作为内容容器的。
bytes.Buffer 类型的用途主要是作为字节序列的缓冲区,bytes.Buffer 是集读、写功能于一身的数据类型。
模式值有:
开发框架
数据分析
云计算
中间件
独立服务
系统工具
Go 程序辅助工具