grammar
String
这篇专栏讲的很好
- 怎么理解?C语言没有 字符串 这个数据类型。字符串是以 字符串字面值 或者 以’\0’结尾的字符类型数组呈现。
const char *s1 = "xxxx"
char s2[] = "i love to go"
带来问题:
不是原生类型,编译器不会对它进行类型校验,导致类型安全性差;字符串操作时要时刻考虑结尾的’\0’,防止缓冲区溢出;以字符数组形式定义的“字符串”,它的值是可变的,在并发场景中需要考虑同步问题;获取一个字符串的长度代价较大,通常是 O(n) 时间复杂度;C 语言没有内置对非 ASCII 字符(如中文字符)的支持。
Go 语言源文件默认采用的是 Unicode 字符集,Unicode 字符集是目前市面上最流行的字符集,它囊括了几乎所有主流非 ASCII 字符(包括中文字符)。Go 字符串中的每个字符都是一个 Unicode 字符,并且这些 Unicode 字符是以 UTF-8 编码格式存储在内存当中的。
Unicode:
- unicode 码点在 go 中用 rune 类型表示,本质是 int32。由此,一个 rune 实例就是一个 Unicode 字符,一个 Go 字符串也可以被视为 rune 实例的集合。
- unicode 的编码:即如何存储 unicode。
- utf-32: 直接存码点,固定使用4个字节(顾名思义32位嘛)
- ✅ utf-8:可变长度,字节数量从1个到4个,兼容单个字节的 ASCII
看待这种字符集的字符串有两种视角:
- 字节视角:len(s);for循环(按字节输出一个字符串值时看到的字节序列,就是对字符进行 UTF-8 编码后的值,展示时一般以 0x 打头 - 十六进制);下标操作
- 字符视角: utf8.RuneCountInString(s);for range循环(循环出的每个十六进制是某种 unicode 字符表示,即码点,以及该字符在字符串中的偏移值)
- 单个字符字面值的表示:
- 单括号括起
- Unicode 专用转义字符 ‘\u两个十六进制’ or ‘\U四个十六进制’
- 字符串字面值的表示:用双引号括起来的多个单字符
对应到数据,buf里传的是 []byte{} // 即 utf-8数据数组,需要先用 utf8.DecodeRune(buf) 将此解码成 unicode,再通过 string(被解码的 unicode) 展示出来。
p.s. Nodejs 中 String 默认的存储格式是 utf-16,所以涉及到读或转 utf-8 的字符时,需要:Buffer.from(’xxx’, ‘utf-8’).toString()
; 对于其他 encoding 的转换一般也是走 buffer
Array, Slice
var slice1 []int
numbers:= []int{1,2,3,4,5,6,7,8}
var x = []int{1, 5: 4, 6, 10: 100, 15}
y := []int{20, 30, 40}
x = append(x, y...)
numbers:= []int{1,2,3,4,5,6,7,8}
// 从下标2 一直到下标4,但是不包括下标4
numbers1 :=numbers[2:4]
// 从下标0 一直到下标3,但是不包括下标3
numbers2 :=numbers[:3]
// 从下标3 一直到结尾
numbers3 :=numbers[3:]
- 数组:C 语言中的数组变量可视为指向数组第一个元素的指针,但是 Go 中就是数组本身,除非显性的用指针。
- 切片:可以理解成访问与修改数组的窗口。
- 声明切片时不用长度属性。
- Go 编译器会自动为每个新创建的切片,建立一个底层数组,默认底层数组的长度与切片初始元素个数相同。
- 采用 array[low : high : max]语法基于一个已存在的数组创建切片。称为数组的切片化。len = high - low, cap = max - low.
- 在动态扩容的情况下,一旦追加的数据操作触碰到切片的容量上限(实质上也是数组容量的上界),切片就会和原数组解除“绑定”复制到一个更大的新数组,后续对切片的任何修改都不会影响到原数组中了。
-
运行时的结构三元组:
go type slice struct { array unsafe.Pointer len int cap int }
Map
var hash map[T]T
var hash = make(map[T]T,NUMBER)
var country = map[string]string{
"China": "Beijing",
"Japan": "Tokyo",
"India": "New Delhi",
"France": "Paris",
"Italy": "Rome",
}
v := hash[key]
v,ok := hash[key]
m := map[string]int{
"hello": 5,
"world": 10,
}
delete(m, "hello")
- 注意,这里的传递又是引用类型了。
- 不能并发写,会 panic
- 对于指针,Go automatically handles conversion between values and pointers for method calls
e.g. r.area() == rp.area()
Type
底层类型
- 空结构体类型元素:size 为0,可以当做触发事件的 flag
type A struct { AA string }
type B struct {
A A
} 等价于
type B struct {
A
}
可以 B.A.AA 也可以 B.AA
如果一个结构体零值不可用,往往会附加一个 NewT 的方法
测试:在 64 位系统上,如下结构体的一个实例占多大?
type A struct {
a byte
b int64
c uint16
}
a: 1+填充对齐的7 = 8 b: 8 c: 2 总计18 但为了对齐存储,需要圆到 8 的倍数上即 24 位
循环与选择
- 循环里写异步/goroutine 会和 js 有一样只能读到最终结果的被循环值的问题,也可以用闭包或者在块内存值来解决
- 参与 for range 循环的是被循环变量的副本。因此如果想边遍历数组 a 边修改,要换成切片 a[:]因为切片副本里存的是指针,或者直接遍历数组指针 &a.
- 循环变量复用:v := for range 的时候,v 只有一个
- 霍 switch case 居然不用手动写 break 的!
- 没有 do-while 可以采取如下方式
go for { work() if !condition { break } }
函数
func div (a,b int) (int,error){
if b == 0 {
return 0, errors.New("b cat't be 0")
}
return a/b,nil
}
// 可变参函数
func Println(a ...interface{}) (n int, err error)
// -----函数作为参数
package main
import (
"fmt"
)
// 遍历切片的每个元素, 通过给定函数进行元素访问
func visit(list []int, f func(int)) {
for _, v := range list {
f(v)
}
}
func main() {
// 使用匿名函数打印切片内容
visit([]int{1, 2, 3, 4}, func(v int) {
fmt.Println(v)
})
}
// ------函数作为返回值时,一般在闭包和构建功能中间件时使用得比较多
func logging(f http.HandlerFunc) http.HandlerFunc{ return func(w http.ResponseWriter, r *http.Request) { log.Println(r.URL.Path) f(w,r) }}
// ------函数作为值时,可以用来提升服务的扩展性
var opMap = map[string]func(int, int) int{
"+": add,
"-": sub,
"*": mul,
"/": div,
}
f := opMap[op]
f()
- 函数签名:参数列表与返回值列表的组合
- 值传递:整型、数组、结构体
- 引用传递:string、切片、map
- defer: deferred 函数的参数值都是在注册 defer 的时候进行求值的
type 的方法 - 名词:receiver 和 receiver 的基类型(不能是指针或接口类型)
func (p *field) print() {
fmt.Println(p.name)
}
func main() {
data1 := []*field{{"one"}, {"two"}, {"three"}}
for _, v := range data1 { go v.print() } // one two three
data2 := []field{{"four"}, {"five"}, {"six"}}
for _, v := range data2 { go v.print() } // six six six
time.Sleep(3 * time.Second)
}
因为 data2 中,v 不是指针,所以会被自动转换成 (*field).print(&v),即传入的都是变量 v 的地址,由于变量 v 只有一份,所以 3s 后停留到了循环的最后一个值。
若把 receiver 改成 field 后,data1 变成了 (field).print(v),只要是值就都是复制。 - T 类型的方法集合包含所有以 T 为 receiver 参数类型的方法,以及所有以 T 为 receiver 参数类型的方法 // 在实现接口的情况下需要考虑
IIFE
squareOf2 := func() int {
return 2 * 2
}()
fmt.Println(squareOf2)
接口
- interfaces: named collections of method signatures(方法签名).
- The interface{} type, the empty interface, is the source of much confusion
- 在使用接口时,方法接收者使用指针的形式能够带来速度的提升
// 带方法的接口
type InterfaceName interface {
fA()
fB(a int,b string) error
...
}
var s InterfaceName
// 空接口
type Empty interface{}
// 接口可嵌套
// 接口类型断言
func main(){
var s Shape
s = Rectangle{3, 4}
rect := s.(Rectangle)
fmt.Printf("长方形周长:%v, 面积:%v \\n",rect.perimeter(),rect.area())
}
// 根据空接口中动态类型的差异选择不同的处理方式(这在参数为空接口函数的内部处理中使用广泛,例如 fmt 库、JSON 库)
switch f := arg.(type) {
case bool:
p.fmtBool(f, verb)
case float32:
p.fmtFloat(float64(f), 32, verb)
case float64:
p.fmtFloat(f, 64, verb)
}
Panic
论在哪个 Goroutine 中发生未被恢复的 panic,整个程序都将崩溃退出。注意:recover 只能捕获当前协程。
func f() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered in f", r)
}
}()
fmt.Println("Calling g.")
g(0)
fmt.Println("Returned normally from g.")
}
Json, String, Byte, Decode
关于 NewReader() 可以看到有 strings.NewReader()、json.NewReader() 它们都返回了 Reader,Reader 就是典型的接口模式。它会实现 io.Reader、io.ReaderAt..等等 interface.
关于从 json 到 string:
- marshal mode
data, err := ioutil.ReadAll(resp.Body)
if err == nil && data != nil {
err = json.Unmarshal(data, value)
}
- decode mode
err = json.NewDecoder(resp.Body).Decode(value)
keypoint: json.Decode
buffers the entire JSON value in memory before unmarshalling it into a Go value.
- data comes from
io.Reader
stream =>json.Decoder
- have the data in string or somewhere in memory ⇒
json.Unmarshal
io 与 log
- io.Reader:
- 获取:很多方法都实现了它 比如
os.Open("")``request.Body``stirngs.NewReader()
- 使用:
- .Read() .ReadAll() .Copy()
- json.NewDecoder(r).Decode(): decode directly from a Reader
- 如果有需要 take in 或者吐出来 string 的方法,比如 filter, 可以改成 take in 和吐出 io.Reader 的,更有适用性
- io.Copy(http.ResponseWriter, resp.Body) 转发回复。在工业级代码中,一般会写一个 for 循环,控制每一次转发的数据包大小
- log:
f := os.OpenFile(...)
log.SetOutput(f)
// or log.SetOutput(os.Stdout)
// 通过 io.MultiWriter() 可以集合多个输出
- 各种 print 大对比
- print 系列:writes to standard output
- fmt.Print(name, " is ", age, " years old.\n") //注意需要手动添加空格和\n
- fmt.Println(name, "is", age, "years old.")
- fmt.Printf("%s is %d years old.\n", name, age)
- Sprint 系列:returns the resulting string.
- Fprint 系列:writes to specific output
运行时
什么相当于 node.js 的__dirname 或是 process.cwd() 呢?- after the location of the binary in the filesytem
ex, err := os.Executable()
if err != nil {
panic(err)
}
exPath := filepath.Dir(ex)
xxx := path.Join(cwd, "../cmd/confServerConf.yaml")
p.s. 我只知道 test 时,这个路径一般是 test 文件所在的 package 下
Concurrency patterns: Groutines/Context/Pipelines
Channel
The most common uses: Data transfer between goroutines and signalling.
chan int
chan <- float
<-chan string
c <- 5
data := <-c
close(c)
// 通道作为参数
func worker(id int, c chan int) {
for n := range c {
fmt.Printf("Worker %d received %c\\n",
id, n)
}
}
// 通道作为返回值
func createWorker(id int) chan int {
c := make(chan int)
go worker(id, c)
return c
}
// 单方向通道
func receive(receiver chan<- string, msg string) {
receiver <- msg
}
func send(sender <-chan string, receiver chan<- string) {
msg := <-sender
receiver <- msg
}
// select 多个通道多路复用
select {
case <-ch1:
// ...
case x := <-ch2:
// ...use x...
case ch3 <- y:
// ...
default:
// ...
}
// context 处理协程退出
func Stream(ctx context.Context, out chan<- Value) error {
for {
v, err := DoSomething(ctx)
if err != nil {
return err
}
select {
case <-ctx.Done():
return ctx.Err()
case out <- v:
}
}
- 对于无缓冲的 channel,at least two goroutines 并且接收在前。如果当前没有其他 goroutine 正在接收 channel 数据,则发送方会阻塞在发送语句处。
- 通道关闭后,也可以读,只是 ok 为 false。若要避免读,可将通道赋值为 nil
- select 若最后没有 default, 且一个 case 都没有落入时,当前协程就会陷入到堵塞的状态
select
- 每个 case 都必须是一个通道
- 所有 channel 表达式都会被求值
- 所有被发送的表达式都会被求值
- 如果任意某个通道可以进行,它就执行,其他被忽略。
- 如果有多个 case 都可以运行,select 会随机公平地选出一个执行,其他不会执行。
否则:- 如果有 default 子句,则执行该语句。
- 如果没有 default 子句,select 将阻塞,直到某个通道可以运行;Go 不会重新对 channel 或值进行求值。
锁
Atomic 可理解成 one at a time. 因此可以避免数据争用、内存操作乱序的问题。让我想一个例子哈,比方说给烤肉定时翻面,两人先后同时去翻,就可能翻重了🤣
// 这比冰箱买菜的例子更好!!!我之前的冰箱买菜例子是,丈夫看见冰箱里肉没了,去买肉;中途妻子看见冰箱肉没了,也去买肉。乐观锁是买肉放进冰箱时再看一眼冰箱是否还需要肉,悲观锁是看见冰箱肉没了,去买肉前就把冰箱门锁了,这样另一个人就看不见了。
// 原子锁
func add() {
for {
if atomic.CompareAndSwapInt64(&flag, 0, 1) {
count++
atomic.StoreInt64(&flag, 0)
return
}
}
}
// 互斥锁
var m sync.Mutex
func add() {
m.Lock()
count++
m.Unlock()
}
// 读写锁 适合多读少写
type Stat struct {
counters map[string]int64
mutex sync.RWMutex
}
func (s *Stat) getCounter(name string) int64 {
s.mutex.RLock()
defer s.mutex.RUnlock()
return s.counters[name]
}
func (s *Stat) SetCounter(name string){
s.mutex.Lock()
defer s.mutex.Unlock()
s.counters[name]++
}
sync.WaitGroup
sync.Once
(适合用在加载配置中)
Ref: An Introduction to Channels in Go (Golang): https://www.sohamkamani.com/golang/channels/#why-do-we-need-channels 讲得很好 把我讲懂了
The Behavior Of Channels: https://www.ardanlabs.com/blog/2017/10/the-behavior-of-channels.html 更深入的解释了 channel buffer 的几种模式及适用情况
代表这个 func 会在 goroutine 里执行。会有异步的效果:比如这个 func 里有个带 time 阻塞的循环的话,这个循环就不会阻塞主流程,这时可以借助 chan,实现 goroutine 和 主流程 的通信。
对比 js,settimeout 已被自动放发到主流程之外去计时了。但所有的函数都在主流程里执行 除了promise。另外,go routine 是通过通信告诉主流程内容,而 js 是将入参为执行结果的函数塞回到主流程末尾去执行。
- A statement to receive data from a channel will block until some data is received
- A statement to send data to a channel will wait until the sent data has been received
所以只要函数末尾有正在读取 chan 的操作,函数就不会立即结束,而会等待。
Create goroutines == spawn a worker 原来这里的 spawn 也是产卵的意思…噫有点恶心哦!
一个 gorotinue 即一个 worker,如果有 n 个 worker 和 m 个 task,那就和计网里分段传数据的那张图一样,需要 m % n + m / n 个时间段。
例子:如何在并发的 go routine 里往 slice append data?
- 用写锁
type answer struct {
MU sync.Mutex
data []int
}
- 先把 data 存进一个通道里,结束后再从通道循环拿取数据
``` c := make(chan result)
//...循环内 select { case c <- result{path, md5.Sum(data)}: case <-ctx.Done(): return ctx.Err() }
// 结束 go func() { g.Wait() close(c) }()
// 消费数据 m := make(map[string][md5.Size]byte) for r := range c { m[r.path] = r.sum } ```
- 如果知道 slice 的长度,可以先定义 slice
MySlice = make([]*MyStruct, len(params))
,再通过MySlice[i]=xx
的方式赋值
Go语言的并发是基于 goroutine 的,goroutine 类似于线程,但并非线程。可以将 goroutine 理解为一种虚拟线程。Go语言运行时会参与调度 goroutine,并将 goroutine 合理地分配到每个 CPU 中,最大限度地使用 CPU 性能。
多个 goroutine 中,Go语言使用通道(channel)进行通信,通道是一种内置的数据结构,可以让用户在不同的 goroutine 之间同步发送具有类型的消息。这让编程模型更倾向于在 goroutine 之间发送消息,而不是让多个 goroutine 争夺同一个数据的使用权。(通过通道实现多个 goroutine 内存共享)
- 通过 make(chan type) 创建一个 channel
- 创建 func() 函数的并发 groutine:
go func('some param1', channel)
go func('some param2', channel)
==> 然后它们就可以同时执行了!
有一个悖论,既然父程序可以调起另一个子程序去旁边执行,那么正常情况下,父程序一定会自然的执行下去并结束。所以才有了要在父程序结束前,要确保子程序是否也要结束的操作。
Context 主要用来为多个 groutine 提供变量,以及 flag。withCancel(context.Background()) 会返回一个可 cancel 的 context。那如何知道这个 context 被 cancel 了呢?通过监听 context.Done() // 它是一个 channel 哈 Link
Pipeline 不是单独的语法,而是一个实现情境。特别的有 fan in(来自多个 channel 的入参),以及 fan out,出参由多个 channel 分担。Link
sync.waitGroup: A WaitGroup waits for a collection of goroutines to finish. 类似于 Promise.all()
Goroutine
一些体感:目前线程切换速度大约 1~2μs(微秒 10^-6^s),协程约为 0.2 微秒
Automatically set GOMAXPROCS
to match Linux container CPU quota.
Goroutine v.s. Thread
Goroutine | Thread |
---|---|
Goroutines are managed by the go runtime. | Operating system threads are managed by kernel. |
Goroutine are not hardware dependent. | Threads are hardware dependent. |
Goroutines have easy communication medium known as channel. | Thread does not have easy communication medium. |
Due to the presence of channel one goroutine can communicate with other goroutine with low latency. | Due to lack of easy communication medium inter-threads communicate takes place with high latency. |
Goroutine does not have ID because go does not have Thread Local Storage. | Threads have their own unique ID because they have Thread Local Storage. |
Goroutines are cheaper than threads. | The cost of threads are higher than goroutine. |
They are cooperatively scheduled. | They are preemptively scheduled. |
They have faster startup time than threads. | They have slow startup time than goroutines. |
Goroutine has growable segmented stacks. | Threads does not have growable segmented stacks. |
大小:大概4KB | 64 bit JVM: defaults to a 1MB stack |
最重要的还是 goroutine 的调度更加有效率,比如如果某个 goroutine 在等待的 channel 里没有数据,它就不会被调用。 |
这篇文章写的很好:Why you can have millions of Goroutines but only thousands of Java Threads
Context
- 超时退出、级联退出
- 共享数据
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
func main() {
ctx := context.Background()
before := time.Now()
preCtx, _ := context.WithTimeout(ctx, 100*time.Millisecond)
go func() {
childCtx, _ := context.WithTimeout(preCtx, 300*time.Millisecond)
select {
case <-childCtx.Done():
after := time.Now()
fmt.Println("child during:", after.Sub(before).Milliseconds())
}
}()
select {
case <-preCtx.Done():
after := time.Now()
fmt.Println("pre during:", after.Sub(before).Milliseconds())
}
}
原理:使用 context.withXXX()
时会派生出子 context,这时父子 context 会有关联关系。在 http 标准库中,连接请求、发送请求、等待服务器响应…每一层的 ctx 都派生自上级,会重新计算当前 ctx 的 timeout 时间。
并发模型
如果是锁单个资源,用锁实现;如果是消费多个资源,用通道。
常见并发模型:
- 一对一 ping-pong(跑腿)
- 多输入,单输出 fan-in(分工执行,统一结果,如分工search,收快递的家门口)
- 单输入,多输出 fan-out(分发,如 message queue,分拣快递,多级分拣快递)
- 管道 pipeline 还可以管道自己,类似于递归
Go Concurrency Patterns: Pipelines and cancellation - The Go Programming Language
本地与文件
- 获取执行路径
-
执行的 bin 的路径:os.Executable()
-
对标 nodejs 的 __dirname, 当前执行栈的路径:
go var ( _, b, _, _ = runtime.Caller(0) dirpath = filepath.Dir(b) ) fmt.Println(dirpath)
https://stackoverflow.com/a/70491592/6005860
-
working directory 路径:os.Getwd() 因为可以人工设置路径,尤其在 vscode debug 时设置 "cwd" ,这样在执行和测试时都能获取到一致的文件路径!
一个例子:
-
path 与 path/filepath
因为它们都有 Dir() 方法,所以我很迷。结果 pkg overview 上就写了呀,path 适合slash-separated paths → 用于 url 操作;filepath 适合 target operating system-defined → 用于文件路径操作
p.s. 斜杠 forward slash 和反斜杠 backward slash。前者是 unix、类unix、url、file:// 下的文件分隔符,而后者是 windows 下的文件分隔符。
编译
go build [-o 导出名] 入口文件