以下内容都来源于Go语言101: 字符串 -Go语言101
字符串
关于字符串的一些简单事实
- 字符串值(和布尔以及各种数值类型的值)可以被用做常量。
- Go支持两种风格的字符串字面量表示形式:双引号风格(解释型字面表示)和反引号风格(直白字面表示)。具体介绍请阅读前文。
- 字符串类型的零值为空字符串。一个空字符串在字面上可以用""或者``来表示。
- 我们可以用运算符+和+=来衔接字符串。
- 字符串类型都是可比较类型。同一个字符串类型的值可以用==和!=比较运算符来比较。 并且和整数/浮点数一样,同一个字符串类型的值也可以用>、<、>=和<=比较运算符来比较。 当比较两个字符串值的时候,它们的底层字节将逐一进行比较。如果一个字符串是另一个字符串的前缀,并且另一个字符串较长,则另一个字符串为两者中的较大者。
Go中的字符串和Java中的字符串相同,都是不可修改的。底层是一个个字符组成的串。
语法糖
字符串当作字节切片使用
内置函数copy和append可以用来复制和添加切片元素。 事实上,做为一个特例,如果这两个函数的调用中的第一个实参为一个字节切片的话,那么第二个实参可以是一个字符串。 (对于append函数调用,字符串实参后必须跟随三个点...。) 换句话说,在此特例中,字符串可以当作字节切片来使用。
package main
import "fmt"
func main() {
hello := []byte("Hello ")
world := "world!"
// helloWorld := append(hello, []byte(world)...) // 正常的语法
helloWorld := append(hello, world...) // 语法糖
fmt.Println(string(helloWorld))
helloWorld2 := make([]byte, len(hello) + len(world))
copy(helloWorld2, hello)
// copy(helloWorld2[len(hello):], []byte(world)) // 正常的语法
copy(helloWorld2[len(hello):], world) // 语法糖
fmt.Println(string(helloWorld2))
}
函数
变长参数和变长参数函数类型
一个函数的最后一个参数可以是一个变长参数。一个函数可以最多有一个变长参数。一个变长参数的类型总为一个切片类型。 变长参数在声明的时候必须在它的(切片)类型的元素类型前面前置三个点...,以示这是一个变长参数。 两个变长函数类型的例子:
func (values ...int64) (sum int64)
func (sep string, tokens ...string) string
通道
通道类型和值
和数组、切片以及映射类型一样,每个通道类型也有一个元素类型。 一个通道只能传送它的(通道类型的)元素类型的值。
通道可以是双向的,也可以是单向的。
- 字面形式chan T表示一个元素类型为T的双向通道类型。 编译器允许从此类型的值中接收和向此类型的值中发送数据。
- 字面形式chan<- T表示一个元素类型为T的单向发送通道类型。 编译器不允许从此类型的值中接收数据。
- 字面形式<-chan T表示一个元素类型为T的单向接收通道类型。 编译器不允许向此类型的值中发送数据。
双向通道chan T的值可以被隐式转换为单向通道类型chan<- T和<-chan T,但反之不行(即使显式也不行)。 类型chan<- T和<-chan T的值也不能相互转换。
通道操作详解
为了让解释简单清楚,在本文后续部分,通道将被归为三类:
- 零值(nil)通道;
- 非零值但已关闭的通道;
- 非零值并且尚未关闭的通道。
下表简单地描述了三种通道操作施加到三类通道的结果。
操作 | 一个零值nil通道 | 一个非零值但已关闭的通道 | 一个非零值且尚未关闭的通道 |
关闭 | 产生恐慌 | 产生恐慌 | 成功关闭(C) |
发送数据 | 永久阻塞 | 产生恐慌 | 阻塞或者成功发送(B) |
接收数据 | 永久阻塞 | 永不阻塞(D) | 阻塞或者成功接收(A) |
我们可以认为一个通道内部维护了三个队列(均可被视为先进先出队列):
- 接收数据协程队列(可以看做是先进先出队列但其实并不完全是,见下面解释)。此队列是一个没有长度限制的链表。 此队列中的协程均处于阻塞状态,它们正等待着从此通道接收数据。
- 发送数据协程队列(可以看做是先进先出队列但其实并不完全是,见下面解释)。此队列也是一个没有长度限制的链表。 此队列中的协程亦均处于阻塞状态,它们正等待着向此通道发送数据。 此队列中的每个协程将要发送的值(或者此值的指针,取决于具体编译器实现)和此协程一起存储在此队列中。
- 数据缓冲队列。这是一个循环队列(绝对先进先出),它的长度为此通道的容量。此队列中存放的值的类型都为此通道的元素类型。 如果此队列中当前存放的值的个数已经达到此通道的容量,则我们说此通道已经处于满槽状态。 如果此队列中当前存放的值的个数为零,则我们说此通道处于空槽状态。 对于一个非缓冲通道(容量为零),它总是同时处于满槽状态和空槽状态。
一些通道的使用例子
来看一些通道的使用例子来加深一下对上一节中的解释的理解。
一个简单的通过一个非缓冲通道实现的请求/响应的例子:
package main
import (
"fmt"
"time"
)
func main() {
c := make(chan int) // 一个非缓冲通道
go func(ch chan<- int, x int) {
time.Sleep(time.Second)
// <-ch // 此操作编译不通过
ch <- x*x // 阻塞在此,直到发送的值被接收
}(c, 3)
done := make(chan struct{})
go func(ch <-chan int) {
n := <-ch // 阻塞在此,直到有值发送到c
fmt.Println(n) // 9
// ch <- 123 // 此操作编译不通过
time.Sleep(time.Second)
done <- struct{}{}
}(c)
<-done // 阻塞在此,直到有值发送到done
fmt.Println("bye")
}
(c, 3) 是启动 goroutine 时传递的参数,具体含义如下:
- c:表示将通道 c 传递给 goroutine,使得这个 goroutine 可以向通道 c 发送值。
- 3:表示将整数 3 传递给 goroutine,这个整数将在 goroutine 中被使用。
整个 go func(ch chan<- int, x int) {...} 这个表达式是一个匿名函数(函数字面量)的启动,创建了一个新的 goroutine,这个 goroutine 接收两个参数:一个是通道 c,一个是整数 3。
在这个匿名函数中,通过 ch <- x*x 将 3*3 的结果发送到通道 c。由于通道 c 是非缓冲通道,这个发送操作会阻塞,直到有其他 goroutine 准备好接收这个值。
这样的设计可以确保在主 goroutine 中创建的两个 goroutines 之间建立了同步关系,第一个 goroutine 发送数据,而第二个 goroutine 接收数据。整体上,这种通过通道进行数据传递和同步的机制是 Go 中协程之间通信的一种常见方式。
demo解释:先定义一个通道 c,然后定义一个匿名函数,用之前定义的通道 c 发送数据。然后又定义了一个通道done,然后定义一个匿名函数,用之前定义的通道done接收数据。
这行代码 done <- struct{}{} 实际上是向通道 done 发送一个空结构体 {}。在 Go 语言中,通常使用空结构体来表示不包含任何信息的数据。struct{}{} 创建了一个空的结构体实例。
在这个特定的例子中,这行代码的目的是通知主 goroutine 已经完成了任务。通道 done 是一个非缓冲通道,因此发送操作 done <- struct{}{} 会导致发送者阻塞,直到有其他 goroutine 准备好从通道中接收这个值。
在主 goroutine 中,<-done 这行代码会阻塞,直到有值发送到通道 done。因此,当后台 goroutine 执行 done <- struct{}{} 时,主 goroutine 将收到通道中的值,解除阻塞,从而继续执行后面的代码,打印 "bye"。
简而言之,这行代码的作用是通过非缓冲通道 done 向主 goroutine 发送一个信号,告知主 goroutine 后台 goroutine 的任务已经完成,可以继续执行。这是一种在并发编程中用于同步协程的常见模式。
方法
方法声明
在Go中,我们可以为类型T和*T显式地声明一个方法,其中类型T必须满足四个条件:
- T必须是一个定义类型;
- T必须和此方法声明定义在同一个代码包中;
- T不能是一个指针类型;
- T不能是一个接口类型
// Age和int是两个不同的类型。我们不能为int和*int
// 类型声明方法,但是可以为Age和*Age类型声明方法。
type Age int
func (age Age) LargerThan(a Age) bool {
return age > a
}
func (age *Age) Increase() {
*age++
}
// 为自定义的函数类型FilterFunc声明方法。
type FilterFunc func(in int) bool
func (ff FilterFunc) Filte(in int) bool {
return ff(in)
}
// 为自定义的映射类型StringSet声明方法。
type StringSet map[string]struct{}
func (ss StringSet) Has(key string) bool {
_, present := ss[key]
return present
}
func (ss StringSet) Add(key string) {
ss[key] = struct{}{}
}
func (ss StringSet) Remove(key string) {
delete(ss, key)
}
// 为自定义的结构体类型Book和它的指针类型*Book声明方法。
type Book struct {
pages int
}
func (b Book) Pages() int {
return b.pages
}
func (b *Book) SetPages(pages int) {
b.pages = pages
}
属主参数的传参是一个值复制过程
和普通参数传参一样,属主参数的传参也是一个值复制过程。 所以,在方法体内对属主参数的直接部分的修改将不会反映到方法体外。