Bootstrap

Golang 不常被注意的特性

同步发布在我的博客:
Golang 不常被注意的特性

Golang 不常被注意的特性

阅读项目https://github.com/astaxie/build-web-application-with-golang进行的查漏补缺。

1. rune和byte类型

runebyte是 go 内置的两种类型别名。其中runeint32的别称,byteuint8的别称。

在处理中文时,使用rune可以正确计算字符串长度(截取字符串也是这样):

fmt.Println(len("Go语言编程"))  // 输出:14  
// 转换成 rune 数组后统计字符串长度
fmt.Println(len([]rune("Go语言编程")))  // 输出:6

这是因为UTF-8使用1~4个字节编码

参见 详解 Go 中的 rune 类型

2. slice的容量

每个slice 都对应一个底层数组(一个数组可以对应多个 slice),slice 可以视为一个结构体,包含了三个元素

  • 一个指针,指向数组中slice指定的开始位置
  • 长度,即slice的长度
  • 最大长度,也就是slice开始位置到数组的最后位置的长度

举一个例子:


// 声明一个数组
var array = [10]byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'}
// 声明两个slice
var aSlice, bSlice []byte

aSlice = array[3:7]  // aSlice包含元素: d,e,f,g,len=4,cap=7(d~j)
// 从slice中获取slice
bSlice = aSlice[0:6] // 对slice的slice可以在cap范围内扩展,此时bSlice包含:d,e,f,g,h,i

最后一个操作合法是因为aSlice的 cap 默认设置为切片起始位置到数组末尾的长度。

如果在声明切片时显示指定cap,这样这个产生的新的slice就没办法访问最后的三个元素。:

aslice = array[3:7:8]  // aSlice包含元素: d,e,f,g,len=4,cap=5(d~h)
bSlice = array[0:6]  // panic: runtime error: slice bounds out of range [:6] with capacity 5

初始容量和增长率规则:

初始 cap:取决于创建切片的方式(可以指定,也可以通过字面量决定)。

增长率

  • 当容量小于 1024 时,cap 按 2 倍增长。
  • 当容量大于等于 1024 时,cap 按 25% 增长。

3. switch 优化

如果 switch 语句的条件是 连续整数值,Go 编译器可能会优化生成一个 查找表(jump table)来加速分支跳转,实现 O(1) 的跳转速度。

如果 switch 中的条件值是非连续但可以排序的,编译器可能会将 switch 语句转换为一种二分查找的形式,从而减少分支的比较次数。这种优化适用于较大的 switch 语句,尤其当分支数较多时。

如果 switch 语句中的条件是字符串或布尔值(或其他复杂类型),通常不会有查找表优化或二分查找优化,而是逐一比较每个分支,这与 if-else 的行为基本一致。

4. 区分 new、make 和 字面量初始化

在 Go 语言中,newmake 和字面量初始化(也称为字面量表达式)是用于创建和初始化变量的不同方式。它们之间有一些重要的区别,适用于不同的场景。下面将详细比较这三者:

4.1. new 函数

  • 功能new(T) 分配了类型 T 的内存,并返回一个指向该类型零值的指针(即 *T 类型)。
  • 适用类型new 可以用于任何类型(包括基本类型、结构体、数组、切片、map、channel 等)。new 的使用相对较少,主要用于在没有字面量初始化的情况下,快速获取一个类型的零值指针。在一些复杂的反射操作中,new 可以用来动态创建类型实例,特别是当你需要通过反射动态创建某个类型的实例时。在实际开发中,大部分情况下开发者更倾向于使用结构体、数组或切片字面量语法来进行初始化,而不是依赖 new
  • 返回值:返回类型 T 的零值指针。
  • 初始化行为new 不会初始化为非零值,只会分配零值(例如:int0string""struct 所有字段为零值)。

示例:

x := new(int)   // 返回一个指向 int 类型零值的指针,x 的值是 *int 类型,初始为 0
y := new([]int) // 返回一个指向空切片(零值切片)的指针
m := new(map[string]int) // 返回一个 *map[string]int 类型的指针,值是 nil

4.2. make 函数

  • 功能make(T, args) 用于初始化内建类型(slicemapchannel)。make 会为这些类型分配内存,并且初始化数据结构的内部字段(例如:slice 的底层数组,map 的哈希表等)。
  • 适用类型:只能用于 slicemapchannel 类型。
  • 返回值:返回类型 T,不是指针。返回的是已经初始化的类型,而不是类型的零值。
  • 初始化行为make 会将这些类型的内部结构初始化为有效状态,使它们能够被使用。对于 slice,它会返回一个具有指定长度和容量的切片;对于 mapchannel,它会分配内存并返回一个空的、有效的 mapchannel

示例:

s := make([]int, 10)       // 创建一个长度为 10 的切片,元素为零值(0)
m := make(map[string]int)  // 创建一个空的 map
ch := make(chan int, 2)    // 创建一个缓冲区大小为 2 的 channel

4.3. 字面量初始化

  • 功能:字面量初始化是通过直接使用字面量来创建并初始化一个变量。你可以为变量的各个字段或元素指定值。
  • 适用类型:可以用于所有类型,包括基本类型、数组、结构体、切片、map 和 channel。
  • 返回值:返回已初始化的值,通常是一个变量,而不是指针(除非显式使用 & 来获取指针)。
  • 初始化行为:字面量初始化会直接赋值并初始化变量为指定值,可以是零值,也可以是非零值。

示例:

x := 42                // int 类型变量,直接赋值为 42
s := []int{1, 2, 3}    // 创建并初始化一个切片
m := map[string]int{"a": 1, "b": 2} // 创建并初始化一个 map
b := Box{width: 10, height: 5}   // 创建并初始化一个结构体

4.4. 比较总结

特性newmake字面量初始化
适用类型所有类型(包括基本类型、结构体、数组等)仅限内建类型(slicemapchannel所有类型
返回值返回类型的零值指针(*T 类型)返回初始化后的值(T 类型)返回初始化后的值(T 类型)
初始化行为初始化为零值初始化为有效的非零值根据字面量的定义初始化,可能是零值或非零值
是否需要指针返回一个指针返回一个值,非指针返回一个值,非指针(除非使用 &
用途主要用于获取指向零值的指针用于初始化切片、映射和通道用于直接创建和初始化变量,最常用

4. 无类型常量和类型自动匹配

Golang 不支持变量的隐式转换。

在 Go 中,常量可以是无类型常量(untyped constant)。这意味着常量在声明时并不直接指定其类型,而是由使用该常量的上下文决定其类型。无类型常量的特性使得它们在赋值或作为表达式的部分时,可以自动地根据目标类型进行转换或匹配。

但是在常量和特定情况下,Go 会在编译时进行类型自动匹配(例如,将 int 类型的常量转为 bytefloat64 类型)。

看下面的例子:

package main

import "fmt"

const(
	WHITE = iota  // 无类型常量
	BLACK
	BLUE
	RED
	YELLOW
)

type Color byte

type Box struct {
	width, height, depth float64
	color Color
}

func (b *Box) SetColor(c Color) { 
	b.color = c 
}

func (bl BoxList) BiggestColor() Color {
	v := 0.00
	k := Color(WHITE)  // 进行显示转换
	for _, b := range bl {
		if bv := b.Volume(); bv > v {
			v = bv
			k = b.color
		}
	}
	return k
}

func (bl BoxList) PaintItBlack() {
	for i := range bl {
		bl[i].SetColor(BLACK)  // 没有进行显式转换
	}
}

注意到BiggestColorPaintItBlack方法中有对于常量WHITE的赋值有两个不同的写法。

  • k := Color(WHITE)强制进行了类型转换,这是因为**变量k**会自动推断为等号右侧的类型,如果不进行类型转换,k会被视为iota的默认类型int,使得k = b.color这一行出现编译器报错:无法将 ‘b. color’ (类型 Color) 用作类型 int
  • bl[i].SetColor(BLACK) 没有参数进行类型转换,BLACK传入SetColor 函数后自动变为了Color类型(可以通过DEBUG证实)。这是因为Colorbyte类型的别名,而int类型能够安全转化为byte类型,因此编译器自动进行了转换。

这里如果将WHITE = iota语句改为WHITE Color = iota,即可省去BiggestColor方法中的显示转换。

更详细可见博客 小心golang中的无类型常量"

5. 指针作为receiver

Go 中 method 可以作用于任何自定义类型中(不只是 struct,可以是type 声明的任何类型),此时这个类型称为方法的 receiver。

用Rob Pike的话来说就是:

“A method is a function with an implicit first argument, called a receiver.”

也就是说,可以把 receiver 当作方法的第一个参数(如 Python 的 self),因此,想要区分在普通类型和指针上方法的特性,就可以使用函数的值传递和引用传递的视角来解读:如果一个方法想要修改结构体内的成员,那么这个方法需要定义在指针上;否则,方法的接收者实际上只是结构体的一个 copy。

此外,Go能智能地识别调用的是指针的方法还是非指针的方法

  • 如果一个method的receiver是*T,你可以在一个T类型的实例变量V上面调用这个method,而不需要&V去调用这个method;
  • 如果一个method的receiver是T,你可以在一个*T类型的变量P上面调用这个method,而不需要 *P去调用这个method;

回顾 4 的例子:

package main

import "fmt"

const(
	WHITE = iota  // 无类型常量
	BLACK
	BLUE
	RED
	YELLOW
)

type Color byte

type Box struct {
	width, height, depth float64
	color Color
}

// 因为要修改结构体成员,所以要使用指针作为方法的接收者。
func (b *Box) SetColor(c Color) { 
	b.color = c  // 不需要写作`*b.color = c`或`b->color = c`,Go会自动识别这是个指针。
}

func (bl BoxList) PaintItBlack() {
	for i := range bl {
        bl[i].SetColor(BLACK)  // 调用指针上的方法。这里不需要写作`&(bl[i]).SetColor(BLACK)`,同样,Go会自动识别。
	}
}

6. 结构体匿名字段成员重载和method重写

我们知道,Go 结构体允许存在匿名字段:

type Human struct {
	name string
	age int
	weight int
}

type Student struct {
	Human  // 匿名字段,那么默认Student就包含了Human的所有字段
    int    // 任意的内置类型或自定义类型都可以作为匿名字段
}

func main() {
	// 初始化学生Jane
    jane := Student{Human:Human{"Jane", 35, 100}, int: 100}
	// 可以直接通过 Human 的实例访问匿名字段的方法
	fmt.Println("Her name is ", jane.name)  // "Jane"
    fmt.Println("Her preferred number is", jane.int)  // 100

可以很方便地在 Student 重载 Human 的字段:

type Human struct {
	name string
	age int
	weight int 
}

type Student struct {
	Human
    int
    weight int  // 重载Human的相同字段
}

func main() {
	// 初始化学生Jane
    jane := Student{Human:Human{"Jane", 35, 100}, int: 100, weight: 70}
	// 最外层的优先访问
	fmt.Println("Her weight is ", jane.weight)  // 70
    // 如果我们要访问Human的weight字段
    fmt.Println("Human's weight is", jane.Human.weight)  // 100
}

如果匿名字段拥有方法,那么包含这个匿名字段的结构体也能调用这个方法:

type Human struct {
	name string
	age int
	phone string
}

type Student struct {
	Human //匿名字段
	school string
}

//在human上面定义了一个method
func (h *Human) SayHi() {
	fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)
}

func main() {
	mark := Student{Human{"Mark", 25, "222-222-YYYY"}, "MIT"}
	mark.SayHi()  // 直接调用匿名字段的方法
}

同样,可以对这个方法进行重写:

type Human struct {
	name  string
	age   int
	phone string
}

type Student struct {
	Human  //匿名字段
	school string
}

// Human定义method
func (h *Human) SayHi() {
	fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)
}

// Student的method重写Human的method
func (s *Student) SayHi() {
	fmt.Printf("Hi, I am %s, I study in %s. Call me on %s\n", s.name,
		s.school, s.phone)
}

func main() {
	mark := Student{Human{"Mark", 25, "222-222-YYYY"}, "MIT"}
	mark.SayHi()  // 调用重载后的方法
	mark.Human.SayHi()  // 重载前的方法
}
;