携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第12天,点击查看活动详情
前言
本系列文章旨在分享,记录 Go 中的特殊语法,帮助 Gopher 提升对 Go 语法细节的理解和认识。
Go 的特殊语法
常量
在讨论并发之前,先来看看 Go 中的常量。
Go 是强类型语言,它不允许混合数值类型的操作(即不允许数值类型隐式转换),即使是将 int 赋给 int32 也是非法的。这种严格性消除了一种会导致错误或其他故障的常见原因,但是对于常量而言,要求它在简单的上下文中进行类型转换显然是不合理的,如 i=int(0)。所以,Go 中提出了无类型常量的概念,使常量在遵守严格的规则的基础上,同时享有极大的自由度。
无类型与默认类型
对于一个常量 "hello",即使给定名称以后 const hello = "hello",它仍然是一个不具有固定类型的常量值。
type MyStr string
fmt.Printf("%T %v\n", hello, hello)
var s MyStr = hello
fmt.Printf("%T %v\n", s, s)
result:
string hello
main.MyStr hello
你可以在任何允许字符串的情况下使用它,但它并没有类型 string 或 MyStr。那么,当它被赋给变量 s 或者作为 fmt.Printf 的参数时,变量是如何获取类型的呢?
实际上,无类型常量有一个隐含的默认类型,该类型只有当没有其他类型信息可用时公开,它会告诉变量它应该具有什么类型。如果常量的值不能被变量的类型表示,就会产生一个错误。
此外,常量也可以在声明时显式地赋予它类型:const hello string = "hello",这时它就不能直接赋给 MyStr 类型的变量了。
精度与溢出
数值(字符、整数、浮点数、复数)常量在 Go 语言中可拥有任意精度,但它们在被分配给变量时,该值必须能够符合目标的表示范围。例如,我们可以声明 const Huge = 1e10000,但它不能被分配甚至是打印,它会报
cannot use Huge (untyped float constant 1e+10000) as float64 value in argument to fmt.Printf (overflows)
这样的溢出错误。你只能像这样写 fmt.Println(Huge/1e9999),打印出的结果为 10。此外,浮点常量具有非常高的精度,因此运用常量来计算会更加准确。当浮点数常量值被分配给变量时,一部分精度将丢失;赋值将创建最接近高精度值的 float64 或 float32 值。
实现限制:
尽管数值常量在 Go 语言中可拥有任意精度,但实际上编译器会使用有限精度的内部表示来实现它们。不过,每个实现必须:
- 使用至少 256 位表示整数常量。
- 使用至少 256 位表示浮点常量,包括复数常量及尾数部分; 使用至少 32 位表示指数符号。
- 若无法精确表示一个整数常量,则给出一个错误。
- 若由于溢出而无法表示一个浮点或复数常量,则给出一个错误。
- 若由于精度限制而无法表示一个浮点或复数常量,则舍入为最近似的可表示常量。
常量的神奇用法
如果我们想要得到 uint 能够表示的最大值,不能写成:
const MaxUnit32 = 1<<32 - 1
因为 uint 的位数取决于系统结构。还有另一种方法是利用二进制补码来求,-1 的补码表示中所有位都为 1,与最大无符号整数的表示刚好相同,我们可以像下面这样:
var u uint
var v = -1
u = uint(v)
println(u)
但上面是由于利用了变量 v 去强制转换,如果是常量,这种转换是非法的,因为 -1 不在无符号整数的表示范围内。那么,如何将最大的无符号整数值表示为常量呢?其实,我们只需要限制住常量的位数,并翻转每一位上的 0 就可以了。
const MaxUint = ^uint(0)
fmt.Printf("%x\n", MaxUint)
除了上面这种,常量还有一些其他的玩法。由于无类型常量的概念,Go 中的数值常量都存在于一种统一的空间中,也就是说可以对整数、浮点数、复数、字符值进行混合运算。例如:
var f = 'a' * 1.5 // 145.5
var r complex64 = 'b' - 'a' // (1+0i)
var b byte = 1.0 + 3i - 3.0i // 1
实际上,这些规则的意义在于为语言提供了灵活性,让我们可以使用像下面这样的写法:
sqrt2 := math.Sqrt(2)
const millisecond = time.Second/1e3
bigBufferWithHeader := make([]byte, 512+1e6)
并发
在并发编程中,为实现对共享变量的正确访问需要精确的控制。为化解这一难题,Go 将共享的值通过信道来传递。在 Go 中,多个独立执行的 Go 程从不会主动共享,在任意给定的时间点,只有一个 Go 程能够访问该值。数据竞争从设计上就被杜绝了,并且这种思考方式被简化为一句口号:
Do not communicate by sharing memory; instead, share memory by communicating.
不要通过共享内存来通信,而应通过通信来共享内存。
Go 程
Go 程(goroutine)是由 go 语句启动、具有自己的调用堆栈,并可以根据需要进行增长或缩小的独立执行的函数。所有的 Go 程并发运行在同一地址空间,它十分轻量,几乎就只有栈空间的分配,并且栈一开始是很小的。
Go 程会根据需要动态地在多线程上实现多路复用,以保持所有 Go 程的运行:若一个线程阻塞,如等待 I/O,那么其它的线程就会运行。
主函数不会等待 Go 程执行完成,下面是一个简单的例子:
func hello() {
time.Sleep(time.Second)
fmt.Println("Hello!")
}
func main() {
go hello()
// time.Sleep(time.Second)
fmt.Println("exit")
}
<-
<- 操作符结合最左边的 chan 可能的方式:
chan<- chan int // chan<- (chan int)
chan<- <-chan int // chan<- (<-chan int)
<-chan <-chan int // <-chan (<-chan int)
chan (<-chan int)
信道
无缓冲信道在通信时会同步交换数据,它能确保(两个 Go 程的)计算处于确定状态。接收者在收到数据前会一直阻塞。若信道是不带缓冲的,那么在接收者收到上一个已发送的值前,发送者会一直阻塞;若信道是带缓冲的,则在缓冲区未满时,发送者仅在值被复制到缓冲区前阻塞,当缓冲区已满,发送者会一直等待直到某个接收者取出一个值为止。
发送者可通过 close 关闭一个信道来表示没有需要发送的值了。循环 for i := range ch 会不断从信道接收值,直到它被关闭。
此外,nil 信道永远不会准备好通信;从已关闭的信道接收将总是成功,它会立刻返回其元素类型的零值。因此,应使用 ,ok 来检测通信是否成功。
v, ok := <-ch
select
select 语句使一个 Go 程可以等待多个通信操作。select 会阻塞到某个分支可以继续执行为止,这时就会执行该分支。当多个分支都准备好时会随机选择一个执行。为了在尝试发送或者接收时不发生阻塞,可使用 default 分支。
func fibonacci(c chan int) {
x, y := 0, 1
for {
x, y = y, x+y
c <- x
time.Sleep(100 * time.Millisecond)
}
}
func main() {
c := make(chan int, 1)
go fibonacci(c)
timeout := time.After(1000 * time.Millisecond)
for {
select {
case nxt := <-c:
fmt.Println(nxt)
case <-timeout:
fmt.Println("time out")
return
default:
fmt.Println("not ready")
time.Sleep(50 * time.Millisecond)
}
}
}
总结
本篇文章主要介绍了 Go 的类型系统与常量,包括常量的类型、精度与溢出这几个部分,以及并发编程的设计思想与实现中的注意事项。
最后,如果本篇文章对你有所帮助,希望你可以 点赞、收藏、评论,感谢支持 ✧(≖ ◡ ≖✿