go语言的设计理念
不要通过共享内存来通信,而是通过通信来共享内存
翻译为大白话就是说:
多个 goroutine 之间通过 channel 来通信,而不是通过共享内存来通信。
通过channel来通信
package main
import (
"fmt"
)
func add(a, b int, ch *chan int) {
// 通过channel来通信
*ch <- a + b
}
func main() {
ch := make(chan int, 1)
go add(1, 2, &ch)
// 从channel中获取数据
fmt.Println(<-ch)
}
问题1: go add(1, 2, &ch) 这里为啥传递的是&ch,而不是ch?
分析:这里其实是没必要的写法 ,Go 里 channel 本身就是指针,传值即传指针,所以不需要再套一层 。
为啥这里没有必须要写成: &ch ? 因为:
Go 的 channel 值内部就是一个指向hchan 结构体的指针,
按值传递时拷贝的只是这个指针 ,底层队列是共享的。所以:
func add(a, b int, ch chan int) { ch <- a + b }
go add(1, 2, ch)
效果和现在完全一样,而且更符合 Go 的习惯。
而且标准库里所有接收 channel 的函数
(如context.WithCancel 、time.After 的使用方式)都是直接传 channel 值。
问题2:func add(a, b int, ch *chan int) 这里为啥是 *chan ?
分析:因为调用时传的是&ch ,也就是"channel 变量本身的地址"
所以形参必须声明成*chan int (指向 channel 的指针)
类型和实参必须匹配,这是& 带来的直接结果,不是设计需要。
将上面的代码优化为下面的
package main
import (
"fmt"
)
func add(a, b int, ch chan int) {
// 通过channel来通信
ch <- a + b
}
func main() {
ch := make(chan int, 1)
go add(1, 2, ch)
// 从channel中获取数据
fmt.Println(<-ch)
}
&ch 就是取变量ch 自己的地址,类型为*chan int
&ch 就是取变量ch 自己的地址,类型为*chan int。
ch := make(chan int, 2)
变量 ch hchan(底层通道结构,堆上)
┌──────────────┐ ┌────────────────────────
│ 0xc000012345 │ ─────────► │ qcount / dataqsiz │
└──────────────┘ │ buf(环形缓冲区) │
▲ │ sendq / recvq(等待队列)│
│ └────────────────────────┘
│
&ch = 这个格子本身的地址(*chan int)
ch的值 =hchan的地址(一个指针),类型为chan int&ch= 存放这个指针的那块变量内存的地址 → 类型*chan int
区分指针和内存地址
- 地址 = 某块内存在哪里(一个数字,如
0xc000014080) - 指针 = 一个专门用来存地址的变量(或这种类型)
所以"指针"是容器,"地址"是容器里装的东西。
通过代码来区分指针和内存地址
x := 10
p := &x // p 是指针变量,存的是 x 的地址
fmt.Println(&x) // 0xc000014080 ← 地址
fmt.Println(p) // 0xc000014080 ← 和上面一样
fmt.Println(*p) // 10 ← 顺着地址取到的值
fmt.Println(&p) // 0xc000014088 ← p 自己也有地址
「谁是指针?」 只有p 。p 是变量,类型*int
换一句话说:&x /&p 是 取地址操作
p 是 指针变量
指针和内存地址的区别
指针是一个专门用来存地址的变量,地址是内存的编号(运行时概念)
指针是能变的那支箭头,地址是箭头指到的坐标
一个常见误区
&ch 不是"ch 指向的对象",而是"ch 这个变量所在的内存地址"。
同理&ch 也有类型,就是*chan int
一句话: & 问"你在哪",* 问"那里装着什么"。