Skip to content

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 会随机公平地选出一个执行,其他不会执行。
    否则:
    1. 如果有 default 子句,则执行该语句。
    2. 如果没有 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 内存共享)

  1. 通过 make(chan type) 创建一个 channel
  2. 创建 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" ,这样在执行和测试时都能获取到一致的文件路径!

    一个例子:

    Untitled

  • 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 导出名] 入口文件