跳转至

基础

package

初始化

在一个项目中,如果一个包同时被多个包导入,其 init() 方法只会被执行一次:go 会在程序启动时,自动解析所有包的依赖关系,按照依赖的顺序执行 init()

image-20250505155921599

包级变量可以在函数外部初始化,但是不能使用 := 初始化,不能声明和初始化分离

var mp = make(map[string]int)

func main() {

}

包导入

导入方式 作用
import "pkg" 正常导入,需显式使用 pkg.Func()
import _ "pkg" 仅执行 init(),不直接使用包内容
import . "pkg" 直接暴露包内容到当前作用域(慎用,易命名冲突)
import alias "pkg" 为包设置别名(如 alias.Func()

按路径导入:

例如 git.xxx.com/trpc-go/trpc-naming-consul,这里导入的是整个项目的根目录,根目录下的所有 .go 文件会在同一个包中,这里导入的就是项目的主包。 _ 会执行主包所有的 init() 以及依赖链上的所有包的 init() 方法

注意,属于同一个包的多个 .go 文件中可以分别定义多个 init() 方法

数据类型

int

直接使用 int 时,int 的范围取决于机器的位数。

  • 在 32 位系统上int32 位(等价于 int32)。
  • 在 64 位系统上int64 位(等价于 int64)。

math.MaxInt 也是拿到相应的 \(2^{31} - 1\) 或者 \(2^{63} - 1\)

需要注意的是,int32 和 int64 类型的数据之间的赋值需要显式转换 int32(x)

结构体

内存对齐

注意 int 和 unint 的大小取决于操作系统的位宽,一个 word 表示操作系统的位宽

在结构体中,一些数据结构的占用位置大小以及对齐单位如下:

type 大小 对齐单位
字符串 2 words 1 word
指针1 1 word 1 word
slice 3 words 1 word
map 1 word 1 word

结构体的大小在编译期间完全确定;结构体的单位填充对齐长度为所有字段对齐单位的最大值

注意:slice 占用的空间为其 header 占用的空间,数组的实际内容存储在堆上

通过交换结构体字段的声明位置可以减小空间浪费(编译器不会对字段进行重排序)

Json 编解码

编码
func json.Marshal(v any) ([]byte, error)
type Stu struct {
    Name  string `json:"name"` // 重命名 json key
    Age   int
    HIgh  bool
    sex   string // 非导出字段,不序列化
    Class *Class `json:"class"`
}

type Class struct {
    Name  string
    Grade int
}

func main() {
    stu := Stu{
        Name: "张三",
        Age:  18,
        HIgh: true,
        sex:  "男",
    }
    cla := new(Class)
    cla.Name = "1班"
    cla.Grade = 3
    stu.Class=cla

    jsonStu, err := json.Marshal(stu)
    if err != nil {
        fmt.Println("生成json字符串错误")
    }

    fmt.Println(string(jsonStu)) // 转为字符串输出
}

输出结果为:

{"name":"张三","Age":18,"HIgh":true,"class":{"Name":"1班","Grade":3}}
解码

非导出字段无法解码,会得到 nil

type Student struct {
    Name  interface{} `json:"name"`
    Age   interface{}
    sex   interface{}
    Class interface{}
}

type Class struct {
    Name  interface{}
    GRADE interface{}
}

func main() {
    // json 的引号需要用 / 进行转义
    data := "{\"name\":\"lively\",\"Age\":10,\"sex\":\"male\",\"Class\":{\"Name\":\"class1\",\"Grade\":10}}"
    jsonByte := []byte(data)

    stu := Student{}
    json.Unmarshal(jsonByte, &stu)

    fmt.Println(stu)
}
  1. 先查找与 key 一样的 json 标签,找到则赋值给该标签对应的变量(如Name)。
  2. 没有json标签的,就从上往下依次查找变量名与key一样的变量,如Age。或者变量名忽略大小写后与key一样的变量。如GRADE。第一个匹配的就赋值,后面就算有匹配的也忽略。
    (前提是该变量必需是可导出的,即首字母大写)。
  3. 不可导出的变量无法被解析
  4. 当接收体中存在json串中匹配不了的项时,解析会自动忽略该项,该项仍保留空值

组合

Go 通过结构体嵌套实现组合:当将 B 结构体以匿名结构体的形式嵌入结构体 A 时,结构体 A 就获得了结构体 B 的所有方法(和字段)

当进行方法调用时,会先查找当前结构体是否声明了该方法,若没有,再到当前结构体嵌入字段的方法中查找。当多个嵌入字段有重名方法时,需要通过嵌入字段来调用重名方法

var cache = struct {
    sync.Mutex // 嵌入
    mp map
}{
    mp: make(map[string]string)
}

func Query(key string) string {
    cache.Lock() // 使用 cache 直接调用绑定 Mutex 结构体的方法
    val := cache.mp[key]
    cache.UnLock()
    return val
}

指针

Go 中的指针不可以进行偏移和位运算,因此其指针操作只包括 & 和 *

new

func new(Type) *Type

返回一个指针,指针的值为该类型的零值

  • new() 函数在底层使用了 Go 的 runtime.newobject 函数。
  • runtime.newobject 函数会分配一块内存,大小为指定类型的大小,并将该内存清零。
  • 然后,runtime.newobject 函数会返回这块内存的指针。

new 确实会分配空间,但是对于 map / slice / channel 这样的引用类型,只会得到一个值为 nil 的指向这些引用类型的指针。对于这些引用类型,使用了 new 之后,还是需要使用 make 分配空间

  • 例如 new([]int) 只会返回 *slice,但是 slice 结构体的 array 仍然是 nil,len 和 cap 都是 0

make

func make(t Type, size ...IntegerType) Type

只用于 slice, map, channel 的空间分配,返回类型为类型本身

可寻址与不可寻址

  • 方法的接收者为 * 即指针类型时,若调用者可寻址,则提供了为非指针类型直接调用的语法糖
  • 方法接收者为非指针类型时,无论调用者是何种类型,都会

Go 提供值类型和指针类型间进行转换的语法糖。在值类型向指针类型隐式转换时(比如通过值类型调用绑定指针的方法),需要注意值类型不能是临时的,必须要是可寻址的

不可寻址的值类型:中间的运算 / 取值操作没有赋值给实际的变量

临时的生命周期

Go 语言中,变量(如 x := 42)会在内存中分配一个固定的存储位置,因此可以安全地获取其地址(&x)。而以下情况的值是临时的、未绑定到变量的,因此不可寻址:

场景 是否为临时值? 解释
Map 元素 m["key"] 是临时副本 Map 返回的是值的副本,而非原始存储的引用(即使值可修改,地址也不固定)。
字符串字节 s[0] 是临时字节 字符串不可变,取址会破坏不可变性。
函数返回值 createPerson() 是临时结果 返回值未被存储到变量,生命周期仅限于表达式。
算术表达式 x + 1 是临时计算结果 表达式结果未被存储,直接取址无意义。
切片操作结果 nums[:2][0] 是临时值 链式操作生成的中间结果未绑定到变量。
Channel 接收值 <-ch 是临时接收结果 接收的值未被赋值给变量,直接取址无效。

使用 := 赋值给变量后,由于是值拷贝,所以可以取址

匿名函数与闭包

匿名函数

匿名函数没有函数名,需要:

  • 保存到某个变量
  • 或立刻执行
func main() {
    // 将匿名函数保存到变量
    add := func(x, y int) {
        fmt.Println(x + y)
    }
    add(10, 20) // 通过变量调用匿名函数

    //自执行函数:匿名函数定义完加()直接执行
    func(x, y int) {
        fmt.Println(x + y)
    }(10, 20)
}

闭包

闭包 = 函数 + 引用环境

当在一个函数内部定义一个匿名函数(并将其赋值给变量,或作为返回值时),在其生命周期内,其使用的外部函数的变量(通过地址使用)保持有效。即,当匿名函数捕获了外部作用域的变量时,这些变量的生命周期会被延长,直到匿名函数本身不再被引用。

  • 只要变量在闭包的同一作用域内(如同一函数内),无论它在闭包之前或之后声明,闭包都可以访问
  • 即使外部函数执行结束,被闭包引用的变量仍会存活(逃逸到堆上),所以可以开启一个协程来执行闭包
func calc(base int) (func(int) int, func(int) int) {
    add := func(i int) int {
        base += i
        return base
    }

    sub := func(i int) int {
        base -= i
        return base
    }
    return add, sub
}

func main() {
    f1, f2 := calc(10)
    fmt.Println(f1(1), f2(2)) //11 9
    fmt.Println(f1(3), f2(4)) //12 8
    fmt.Println(f1(5), f2(6)) //13 7
}

递归的匿名函数

当匿名函数是一个递归函数时,需要先声明一个变量,否则在定义匿名函数时,在定义中调用的函数无法被识别

var visitAll func(items []string) // 必要
visitAll = func(items []string) {
    for _, item := range items {
        if !seen[item] {
            seen[item] = true
            visitAll(m[item])
            order = append(order, item)
        }
    }
}

闭包的应用 - option 模式

定义了修改结构体属性的函数类型,此类函数会接收外部传入的参数,此参数作为返回函数的闭包变量使用

陷阱

通常出现在先保存一个匿名函数,然后操作了上下文中的变量后再调用

在 go 1.22 中,In a "for" statement, each iteration has its own set of iteration variables rather than sharing the same variables in each iteration.

下面的代码需要为每个 tempDirs 中的 dir 创建一个对应的匿名函数,并放到 rmdir 切片中

错误示例:

var rmdirs []func()
for _, dir := range tempDirs {
    rmdirs = append(rmdirs, func() {
        os.RemoveAll(dir) // 这里记录的是 dir 的地址,只有这个匿名函数真正被调用时,才会从地址中解析出 dir 的实际值,然后通过值传递给 os.RemoveAll()
    })
}

for _, rmdir := range rmdirs {
    rmdir() // 在实际调用的时候,dir 使用的都是最后一次迭代的值
}

修正:

for _, d := range tempDirs() {
    dir := d // 注意这里的 := 
    rmdirs = append(rmdirs, func() {
        os.RemoveAll(dir)
    })
}

这个例子本质上是老版本 for-range 的一个坑:

arr := [2]int{1, 2}
res := []*int{}
for _, v := range arr {
    res = append(res, &v)
}
//expect: 1 2
fmt.Println(*res[0],*res[1]) 
//but output: 2 2

for-range 会首先拷贝一次待遍历的列表,然后对同一个变量重复赋值,也就是说 v 只会有一个地址。这种情况下,再遍历中基于 v 起一个 goroutine 也是不好的:

var m = []int{1, 2, 3}
for i := range m {
    go func() {
        fmt.Print(i)
    }()
}

在 1.22 版本之前,可能重复输出同一个数字

error

定义

error 接口是语言的内置类型,在 builtin 包中,定义如下:

type error interface {
    Error() string
}

只需要绑定了返回 string 的 Error() 方法的结构体就实现了这个接口

errors 包

package errors

func New(text string) error {
    return &errorString{text}
}

type errorString struct {
    s string
}

func (e *errorString) Error() string {
    return e.s
}

这里 errorString 就实现了 error 接口,errors.New() 方法即可返回一个 errorString 结构体的指针

注意,errors.New() 方法的返回值是一个地址,即使 string 内容相同,地址也不同

当需要比较两个 err 是否是同一个类型时,可以通过比较 err1.Error() == err2.Error() 来比较其字符串是否相同

自定义 error

分别设置错误信息和错误码

package myerror

type MyError struct {
    code int
    msg string
}

func (me MyError) Error() string {
    return fmt.Sprintf("code: %d, msg: %s", me.code, me.msg)
}

func New(code int, msg string) error { // 这里返回类型相当于向上转型
    return MyError{
        code: code,
        msg: msg,
    }
}

func Code(err error) int {
    e, ok := err.(MyError)
    if !ok {
        return -1
    }
    return e.code
}

func Msg(err error) string {
    e, ok := err.(MyError)
    if !ok {
        return ""
    }
    return e.msg
}

time 包

time.Time

最核心的结构体为 time.Time

now := time.Now() // 获取当前时间

基于 time.Time 实例可以获取多种信息:

y := time.Year()
m := time.Month() 

timestamp := now.Unix() // 秒级 Unix 时间戳
nano :- now.UnixNano()

time.Duration

type Duration int64

const (
    Nanosecond  Duration = 1
    Microsecond          = 1000 * Nanosecond
    Millisecond          = 1000 * Microsecond
    Second               = 1000 * Millisecond
    Minute               = 60 * Second
    Hour                 = 60 * Minute
)

time 包还定义了一系列常量,如 time.Second 等

定时器

可以使用 time.Tick() 来获取一个定时器,使用方式如下:

不会自动释放,有内存泄漏风险

ticker := time.Tick(time.Second)
for i := range ticker {
    // 每秒执行一次任务
}