golang

· 4833 words · 10 minute read

资料 🔗

  1. 目前 golang 的主要 maintainer 之一: Russ Cox。个人主页:swtch.com/~rsc/
  2. awesome-go 系列

练习操场 playground: go.dev/play。 rust 后来也有 playground。

官方地址改动 🔗

早期都是 golang.org => 现在 go.dev, play.golang.org => go.dev/play

构建 golang 开发环境 🔗

  1. 下载安装包 wget https://golang.google.cn/dl/go1.19.2.src.tar.gz
    google 域名下的地址可能被墙不能访问。
    或者 https://go.dev/dl/ 地址。
  2. 解压并安装到指定目录 tar -zxvf xx.tar.gz -C /usr/local
  3. 配置环境变量
vim .bash_profile
export GOROOT=~/..
export PATH=$PATH:$GOROOT/bin
export GOPATH=~/..
source .bash_profile
  1. 最后,验证一下 go version。go env。

编译 golang 的源码项目 🔗

从源码去编译 golang 项目。

  • git clone https://github.com/golang/go.git
  • 设置 golang 自举编译器的地址。 export GOROOT_BOOTSTRAP=$GOROOT(或者其他)
  • 进去源码目录中的 all.bash 脚本。 ./go/src/all.bash。 或者./go/src/make.bash 脚本会利用自居编译器 GOROOT_BOOTSTRAP 去编译 golang 源码项目。
  • 最后的编译产生了~/go/src/github.com/golang/go, ~/go/src/github.com/golang/go/bin, pkg 库文件等。
  • 验证改动后的执行命令 利用新编译器的绝对路径。 ~/go/src/github.com/golang/go run test_print.go。

go build -n main.go 不编译并打印编译的过程。 -n 参数和 make 的-n 参数是一致的。

变量的内存大小 🔗

没有char类型,特添加type rune = int32类型处理utf-8的变长字符编码问题。

import (
	"fmt"
	"runtime"
	"unsafe"
)

func main() {
	fmt.Printf("arch=%s, os=%s\n", runtime.GOARCH, runtime.GOOS) //arch=amd64, os=linux
	var a int = 190
	fmt.Printf("type=%T, size=%d\n", a, unsafe.Sizeof(a)) //type=int, size=8
	var b int32 = 190
	fmt.Printf("type=%T, size=%d\n", b, unsafe.Sizeof(b)) //type=int32, size=4
	var c uint8 = 'a'
	fmt.Printf("type=%T, size=%d\n", c, unsafe.Sizeof(c)) //type=uint8, size=1
	var d rune = '中'
	fmt.Printf("type=%T, size=%d\n", d, unsafe.Sizeof(d)) //type=int32, size=4
	var e string = "a"
	fmt.Printf("type=%T, size=%d\n", e, unsafe.Sizeof(e)) //type=string, size=16
	var f string = "中"
	fmt.Printf("type=%T, size=%d\n", f, unsafe.Sizeof(f)) //type=string, size=16
}

多态,继承, 范型 🔗

golang 中实现的面向对象的方式

继承 🔗

golang 中没有继承,利用组合结构体实现类似继承的功能。
一个结构体嵌入另一个结构体,能够实现对嵌入结构体的字段以及其实现的方法的继承。

type person struct {
    name string
    age  int
}

type student struct {
    person //   匿名字段,通过组合的方式
    school string
}

func (p * person) talk() {
    fmt.Println(p.name, p.age)
}

func (s *student) getSchool() {
    return s.school
}

func main() {
    s := student{person{"brettkk", 18}, "yidu"}
    fmt.Println(s.name) // 继承person的name字段
    s.talk() // 继承person实现的方法
}

why 为什么需要在 golang 使用 embedding 🔗

golang 中没有 extends, implement 等继承概念。
golang 中通过组合来实现类似继承的功能。
embedding 比较方便组合 golang 中的 struct,interface 来实现类似其他语言中的继承和实现的概念。

what 什么是嵌入 🔗

有三种嵌入的情况:

  1. embedding struct in struct
  2. embedding interface in interface
  3. embedding interface in struct

在 struct 中嵌入 struct 🔗

  1. 可以直接访问嵌入的结构体
  2. 可以直接调用嵌入的结构体上的方法
  3. 上一条满足,自然地被嵌入的结构体实现了接口,外层结构体也就实现了接口

例如: 在 go 中目标结构体 A 中嵌入 sync.Mutex。方便使用 A.lock()可以加锁。如果 lock 只在内部使用,还是正常使用mu sync.Mutex

在 interface 中嵌入 interface 🔗

interface 可以被嵌入到另一个 interface 中,也可以嵌入到 struct 中。
interface 被嵌入到 interface 中,通过组合的方式扩展了接口中的函数集合。 golang 标准库中 Reader 和 Writer 接口被嵌入(组合)到 ReadWriter 接口。
golang 的 container 包中子包 heap 中,通过 embedding interface 的方式表明若要实现 heap.Interface 接口必须要先实现 sort.Interface 接口。

// heap.Interface
type Interface interface {
  sort.Interface
  Push(x interface{}) // add x as element Len()
  Pop() interface{}   // remove and return element Len() - 1.
}

// sort.Interface
type Interface interface {
  Len() int
  Less(i, j int) bool
  Swap(i, j int)
  Push(x interface{}) // add x as element Len()
  Pop() interface{}   // remove and return element Len() - 1.
}

在 struct 中嵌入 interface 🔗

这个是三种 embedding 中让人疑惑的一种。

  • struct 类型的对象可以访问内嵌接口中的方法(提升到外层结构体中
  • 上一条满足,自然地 struct 类型的对象也实现了内嵌接口。

如果初始化 struct 类型对象时,没有初始化 embedding interface 则 embedding interface 被初始化为 nil, 则访问 embedding interface 中的方法时 panic。

type StatsConn struct {
  net.Conn
  BytesRead uint64
}
// 拦截net.Conn.Read方法。
func (sc *StatsConn) Read(p []byte) (int, error) {
  n, err := sc.Conn.Read(p)
  sc.BytesRead += uint64(n)
  return n, err
}

resp, err := ioutil.ReadAll(sconn)
if err != nil {
  log.Fatal(err)
}
fmt.Println(sconn.BytesRead)
type reverse struct {
  sort.Interface
}

func (r reverse) Less(i, j int) bool {
  return r.Interface.Less(j, i)
}
func Reverse(data sort.Interface) sort.Interface {
  return &reverse{data}
}
sort.Sort(sort.Reverse(sort.IntSlice(lst)))
fmt.Println(lst)

Example: context.WithValue.

how 🔗

语法糖,依赖编译阶段的语法解析的处理。

多态 🔗

go 没有 implements, extends 关键字, golang 是去鸭子类型:看起来像鸭子, 那么它就是鸭子。
多态: 父类指针或引用去调用方法,在执行的时候,能够根据子类的类型去执行子类当中的方法。

type Person interface {
    func gender() string
}

type Men struct{
    Gender string
    Age int
    Name string
}

type Woman struct {
    Gender string
    Age int
    Name strings
}

func (m Men) gender() {
    return m.Gender
}

func (w Woman) gender() {
    return w.Gender
}

func JudgeFunc() bool {
    // may has many code
    // can't not get boolean result until running this JudgeFunc
}

func main() {
    var a Person
    // so compiler can't not known clearly about the type info of a
    a = JudgeFunc() ? Men{} : Woman{}
    var b Person
    // compiler can judge the type info of a
    b = Men{} // or b = Woman{}
}

范型 🔗

空 interface{} 类型,没有方法集的接口,只需要存类型和类型对应的值即可。
interface{} = (type, value) ==> (nil, nil) == nil

// 不含方法的空接口
type eface struct {       // 16 byte
	_type *_type          // 指向类型
	data  unsafe.Pointer  // 指向数据
}

// 含有方法的接口
type iface struct {       // 16 byte
    tab *itab             //
    data unsafe.Pointer   //
}

type _type struct {
    size uintptr // 类型占用的内存大小
    ptrdata uintptr //
    hash int32
    align uint8
    ...
}

type itab struct {        // 32 byte
    inter *interfacetype  // 类型信息
    _type *_type          // 类型信息
    hash uint32           //
    _ [4]byte             // padding align size = 8 byte
    fun [1]uintptr        // 用户运行时动态派发的虚函数表,存储函数指针。
}

接口的使用实践 🔗

golang 源码里的较好实现方式是: 使用小的接口,尤其是只包含一个方法的接口;通过小接口的组合来定义大接口。
好处是:使用者可以只依赖于必要功能的最小接口。

type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

type ReadWriter interface {
    Reader
    Writer
}

func store(reader Reader) error {
    // 入参可以只依赖于必要功能的最小接口, 让函数的返回更广
}
func store(readWriter ReadWriter) error {
    // 入参依赖了不必要功能的接口,让限制了函数的使用范围为更小
}

易模糊的点 🔗

字符编码 🔗

ascii 码,占一个字节,实际使用了 7 bit,128 个字符,表示英语够了。
在 ascii 码的第 8 位置为 0,这个特点深深影响了其他国家和后来的字符编码方式。
欧洲法语 é 等字符,采用 Latin1, 利用 acsii 码的最高位, 可以表示了。
亚洲国家的文字符号更多,汉字 10 万加,必须多个字节表示一个符号。 例如 GB2312
unicode 万国码。只是定义了符号的二进制代码,如何区分不同的编码?没有规定二进制如何存放。
utf-8 历史, 开发 plan 9 操作系统的产物,发明了后来广泛使用的字符编码。
utf-8 是一种针对 unicode 的变长字符编码的实现方式。使用 1-4 个字节表示一个字符。
二进制代码第一位是 0, 则这个字符只占一个字节。 如果第一位是 1,连续有多少个 1,表示当前字符占用多少个字节, 后面字节的前两位均以 10 开头。

例如 严。 unicode = 100111000100101 = 4E25,
utf-8=11100100 10111000 10100101 = E4B8A5.

浮点数的存储与处理 🔗

复杂 todo

rune 类型 🔗

golang 为应对 utf-8 字符处理设计出来的一种类型type rune = int32
字符串有字符组成, 字符分 2 种, 一种是占 1byte 的字符,如英文字符;一种是占 1-4 byte 的字符用 rune 表示,如中文。
ascii 码: 48-0, 65-A, 97-a...
在 builtin/builtin.go 中定义

type rune = int32
fmt.Println(len("hallo中文"))// 5 + 3 + 3
fmt.Println(len([]rune("hallo中文"))) //5 + 1 + 1
for (index, item) range "ABC你好" {}
//等价于
for (index, item) range []rune("ABC你好"){}

数组与切片 🔗

数组的初始化 (元素类型, 数组大小)。 数组是值类型, 切片是引用类型。 切片作为函数的参数。

// cmd/compile/internal/types.NewArray
func NewArray(elem *Type, bound int64) *Type {...}

2 种初始化方式,显式的指定数组的大小

  1. arr := [3]int{1, 2, 3}
  2. [...]T 声明数组 // arr := [...]int{1, 2, 3}

数组的访问越界检查。 编译时 cmd/compile/internal/gc.typecheck1 函数 op=OINDEX 时处理。ssa 阶段也会插入检查函数 runtime.panicIndex。

切片,动态数组。

  • 声明切片 []int, []interface{}
  • 初始化切片 []int{1,2,3}, slice := make([]int, 3)
// cmd/compile/internal/types.NewSlice
func NewSlice(elem *Type) *Type {
    ...
}
type SliceHeader struct {
    Data uintptr
    Len int
    Cap int
}
type StringHeader struct {
    Data uintptr
    Len int
}

for range 的注意点 🔗

for range x x 是原对象的拷贝。

函数的接受者 🔗

函数接受者为指针类型:

  1. 函数可以修改接受者的内容
  2. 避免方法调用时变量的拷贝,而是指针的拷贝

实现接受者为值类型的方法,会自动实现接受者为指针类型的方法。

golang 值拷贝 🔗

go 严格上只有复制拷贝传递, 总是创建副本按值传递,只不过这个副本可以是变量的副本,也可以是指针的副本

闭包 🔗

闭包=函数+引用环境。

unsafe 包 🔗

*T <==> unsafe.Pointer <==> uintptr, uintptr 与 unsafe.Pointer(类似 void*)

  1. T1 指针与 T2 指针类型之间的转换
  2. 修改内存数据, uintptr 常用于与 unsafe.Pointer 配合,用于做指针运算
//结构体的成员变量在内存存储上是一段连续的内存
type Num struct{
	i string
	j int64
}

func main(){
	n := Num{i: "EDDYCJY", j: 1}
	nPointer := unsafe.Pointer(&n)
	// 结构体的初始地址就是第一个成员变量的内存地址
	niPointer := (*string)(unsafe.Pointer(nPointer))
	*niPointer = "wuhan"
	// 基于结构体的成员地址去计算偏移量。就能够得出其他成员变量的内存地址
	//uintptr 是 Go 的内置类型。返回无符号整数,可存储一个完整的地址。后续常用于指针运算
	//unsafe.Offsetof:返回成员变量 x 在结构体当中的偏移量。更具体的讲,就是返回结构体初始位置到 x 之间的字节数
	//uintptr 类型是不能存储在临时变量中的 临时变量是可能被垃圾回收掉的 之后的内存操作有迷茫了
	njPointer := (*int64)(unsafe.Pointer(uintptr(nPointer) + unsafe.Offsetof(n.j)))
	*njPointer = 91

	fmt.Printf("n.i: %s, n.j: %d", n.i, n.j)
}

指针是对内存区域的地址, 与指针相配合的类型 说明区域有哪些属性,如何去解析。

反射 🔗

反射 Reflection ,interface 对象=(Type, Value)

反射类型 reflect.Type, reflect.Value)

三种反射的基本操作:

  1. 反射可以将 interface 类型的变量 转换为 反射对象(reflect.Type reflect.Value)
  2. 反射可以将反射对象 还原为 interface 对象
  3. 反射对象可以修改,提前是 value 值是变量的指针
var x float= 3.1
v:= reflect.ValueOf(&x) //需要传入x的指针
v.Elem().SetFloat(3.4);

作用:

  1. 运行时动态调用方法
  2. 运行时构造函数进行 mock 插桩

内存管理 🔗

  1. 基本想法:局部 P 上(per cpu)和全局队列想结合的均衡思想。局部分配无需加锁,局部分配无法满足时加锁全局分配
  • src/runtime/mheap.go:mspan
  • 内存回收原理
    • 三色标记法 (未使用,已使用,带处理)
    • root 对象开始,BFS 遍历标记,
  • 逃逸分析(对象是否被函数外面引用 例如闭包; 对象过大也可能在堆中)
    • 逃逸分析的目的是决定变量的内存分配地址是在栈还是堆
    • 逃逸分析在编译阶段完成的
    • 传递指针真的比传值高效吗?不一定 由于指针传递会产生逃逸,可能会使用堆,增加 GC 负担。
    • go build -gcflags=-m // escapes to heap

golang 中的特色语法 🔗

defer, recover 🔗

defer 调用的函数是一个function literal,也就是闭包或者匿名函数
多个 defer 的执行顺序为后进先出 stack。
defer 声明时会先计算确定参数的值

func f() {
    i := 3
    // print 3
    defer fmt.Println(i)
    i++
    return
}

defer 修改有名返回值函数的返回值

// f return 6
func f() (result int) {
    defer func() {
        result *= 3;
    }()
    return 2
}

只能修改有名返回值(named result parameters)函数,匿名返回值函数是无法修改的

// f return 10
func f() int {
    i := 9
    defer func() {
        i++
    }()
    //匿名返回值函数是在return执行时被声明
    return i
}

recover只能在 defer 中才能生效。

nil 🔗

  • what is nil
    • nil is a kind of zero
  • what is nil in go
    • 各种类型的零值
    • 基本类型的零值不为 nil
      • bool 为 false
      • number 为 0
      • string 为""
    • 非基本类型的零值为 nil
      • pointer, slice, map, channel, function, interface{} ==> nil
      • interface{}类型 需要 type 和 value 均为 nil 时,才会为 nil
      • structure{},因为有结构 type 存在,type 不为 nil,所以结构体变量不会是 nil

函数调用的类型 🔗

  • 顶层函数 func f()
  • 函数 with 值调用者
  • 函数 with 指针调用者
  • func literal 匿名函数 or 闭包(A function literal represents an anonymous function)
type Aer interface {
	a()
	b()
}

type AStruct struct {
	a_ int32
	b_ string
}

func (a AStruct) a() {
	fmt.Printf("call a(), a=%d\n", a.a_)

}
func (a *AStruct) b() {
	fmt.Printf("call b(), b=%s\n", a.b_)
}

func main() {
	var aStruct = AStruct{a_: 1, b_: "c"}
	aStruct.a()
	aStruct.b()
	var aStructPtr = &AStruct{a_: 2, b_: "cc"}
	aStructPtr.a()
	aStructPtr.b()
	var aInterfz Aer = &AStruct{a_: 3, b_: "ccc"}
	aInterfz.a()
	aInterfz.b()
	// AStruct does not implement Aer (method b has pointer receiver)
	// var aInterfz Aer = AStruct{}
}

goroutine 🔗

GMP 运行时。 G:代表一个 Goroutine,每个 Goroutine 都有自己独立的栈。M:表示内核线程。
P:代表一个虚拟的 Processor 处理器,它维护一个 local Goroutine Queue,工作线程优先使用自己的局部运行队列,只有必要时才会去访问 Global Queue。

interfaceinterface{} 🔗

接口的类型转换类型断言以及动态派发机制

接口是一组方法签名。
当一个类型(结构体)为接口中的所有方法提供定义时,被称为实现了该接口, 也称为duck typing

空接口interface{}, (类型+数据)。所有类型都实现了空接口, 空接口可以用来做泛型。

一个接口值由两个指针: 一个指向该值底层类型的方法表,另一个指向实际数据。

image

go 设计模式 🔗

search keyword go patterns
创建型, 结构型, 行为型

创建型 🔗

type Builder interface {
    SetA() Builder
    SetB() Builder
    Build() AInterfz
}
type AInterfz interface {
    A()
    B()
}
assembly.SetA().SetB().Build().A()
type singleton map[string]string
var (
    once sync.Once
    instance singleton
)
func new() singleton {
    once.Do(func() {
        instance = make(singleton)
    })
    return instance
}

装饰器模式 decorator pattern 🔗

type Object func(int) int
func log_decorator(fn Object) Object {
    return func(n int) int {
        log.Println("before")
        result := fn(n)
        log.Println("after")
        return result
    }
}
func double(n int) int { return n*2}
f := log_decorator(double)
f(3)

代理模式 proxy pattern 🔗

type IObject interface {
    objDo(action string)
}
type Object struct { action string}
func (obj *Object) objDo(action string) {
    fmt.Println("%s.", action)
}
type ProxyObject struct {object *Object}
func (p *ProxyObject) objDo(action string) {
    if p.object == nil {p.object = new(Object)}
    if action == "sing" {
        p.object.objDo(action)
    }
}

观察者模式 observer pattern 🔗

策略模式 strategy pattern 🔗

责任链 🔗

通过闭包实现的 http 中间件,在原有函数的基础上包裹中间功能,而不破坏原有函数

func main() {
    http.HandleFunc("/hello", timewrapper(hello))
    http.ListenAndServe(":8888", nil)
}

func timewrapper(f func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
    return func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        f(w, r)
        end := time.Now()
        fmt.Println("take time: ", end.Sub(start))
    }
}

reference 🔗

[1] Embedding structs in structs
[2] Go by Example: Struct Embedding
[3] Embedding Interfaces in Go (Golang)
[4] Embedding in Go: Part 1 - structs in structs
[5] Embedding in Go: Part 3 - interfaces in structs
[6]Go 程序员面试笔试宝典