iota

iota 是常量计数器,在const关键字中被重置为0,随后每出现一行常量定义就会自动加1。

iota 除了赋值表达式来实现,还可以隐式重复。

如果 iota 进行了匿名使用,也会进行跳值与占位。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package main

import "fmt"

const (
	// 实现简洁的枚举
	a = iota // 从 0 开始
	b = iota
	c = iota
	_ // 跳值与占位
	d
)

func main() {
	fmt.Println(b)
	fmt.Println(d)
}

iota 的高级用法是位运算。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package main

import "fmt"

const (
	open    = 1 << iota // 1 << 0 = 1
	close   = 1 << iota // 1 << 1 = 2
	execute = 1 << iota // 1 << 2 = 4
)

func main() {
	fmt.Println(open)
	fmt.Println(close)
	fmt.Println(execute)
	fmt.Printf("%T\n", close)
}

alt text

我们使用 iota 有几个原因,其一是类型安全,配合类型定义 type Priority int,可以创建具有特定含义的枚举。然后是易于维护,如果我们在中间插入一个新的变量,后续所有的 iota 的值都会自动更新,不需要手动更改。最后,也是 go 语言本身的特点,那就是代码简洁,用法简单。

Rune

Rune 就是 Go 语言中的 Unicode 字符,它与 byte 类型的区别在于:Rune 可以表示 32 位,而 byte 只能表示 8 位字符,所以 byte 用于表示 ASCII, 而 Rune 表示 Unicode,更适合处理中文字符。

Rune -> int32

byte -> uint8

1
2
3
4
5
6
7
8
9
func main() {
	s := "Go语言"

	bytes := []byte(s)
	fmt.Println(len(bytes))

	runes := []rune(s)
	fmt.Println(len(runes))
}

alt text

Composition

Go 里面没有继承的概念,而是使用组合来实现,也就是 Composition。Go 里的“继承”不由接口实现者决定,而是由使用者决定。只要 一个对象实现了相关接口的所有方法,就实现了“继承”。

结构体也是如此。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package main

import (
	"fmt"
)

type Engine struct {
	Horsepower int
}

func (e *Engine) Start() {
	fmt.Printf("Max power: %d hp\n", e.Horsepower)
}

type Car struct {
	Engine // extends Engine
	Model  string
}

// 实现 Start 方法
func (c *Car) Start() {
	fmt.Printf("%s is running...\n", c.Model)
	c.Engine.Start()
	fmt.Println("Car is ready!")
}

func main() {
	Tesla := Car{
		Engine: Engine{Horsepower: 300},
		Model:  "CyberTruck",
	}

	// 1. 直接访问提升后的方法
	Tesla.Start()

	// 2. 直接访问提升后的字段
	// 等价于 Tesla.Engine.Horsepower
	fmt.Println("Tesla's hp is: ", Tesla.Horsepower)
}

alt text

结构体和接口的区别在于:

本质上,结构体是具体实现,用来定义字段和状态,而接口是抽象的协议,用于定义一组方法的签名。而在存储内容上,结构体是占用实际内存的,存储具体的数据和字段,而接口不存储数据字段,只持有指向具体实现和数据的指针。在实例化方面,我们可以直接实例化,比如用car := Car{},但我们不能直接实例化接口,而必须由某个结构体来实现它。

最后,结构体可以通过组合来实现复用,而接口可以隐式实现,不需要 implements 关键字。

interface

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package main

import "fmt"

type Usber interface {
	start()
	stop()
}

type Phone struct {
	Name string
}

func (p Phone) start() {
	fmt.Println(p.Name, "start")
}

func (p Phone) stop() {
	fmt.Println(p.Name, "stop")
}

type Camera struct {
}

func (c Camera) start() {
	fmt.Println("start")
}

func (c Camera) stop() {
	fmt.Println("stop")
}

func main() {
	p := Phone{
		Name: "iPhone",
	}
	p.start()

	var p1 Usber // golang中接口就是一个数据类型
	p1 = p
	p1.start()
	p1.stop()

	// 实现接口必须实现接口所带的所有方法
	c := Camera{}
	var c1 Usber = c
	c1.start()
	c1.stop()
}

alt text

总结一下: 结构体负责封装数据和具体的实现逻辑,而接口负责定义行为和实现多态。

匿名字段的结构体

类型断言的语法

使用 int(i) 进行类型转换,它适用于两种情况:底层类型相同或具有相同的底层结构。int(i) 不能用于接口,因为接口变量 i 是一个“盒子”,里面装着具体的值和它的类型信息。int(i) 是试图强行把“盒子”作为某个目标类型来处理,这在语法上是不成立的。

但是可以用 i.(int) 来处理,这就是类型断言。

类型断言的过程是:检查-提取-报错(return bool/panic)

1
2
3
4
5
6
7
// 将一个接口变量 i 转换为具体的 int 类型
// v 是提取出的值,ok 是一个布尔值,告诉你断言是否成功
if v, ok := i.(int); ok {
    fmt.Println("断言成功,值是:", v)
} else {
    fmt.Println("断言失败,i 里面装的不是 int")
}

关于别名(alias)

golang 里的别名定义用 “=” 符号

1
2
// T2 是 T1 的别名
type T1 = T2

关于泛型

Go 1.18 引入的泛型中,any 关键字等价于 interface{}。

使用泛型的一般形式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import "fmt"

type Number interface {
	int | int64 | float64
}

func Sum[T Number](list []T) T {
	var total T
	for _, v := range list {
		total += v
	}
	return total
}

func main() {
	ints := []int{1, 2, 3}
	floats := []float64{1.1, 2.2, 3.3}

	// 调用时,Go 的编译器通常能自动推到 T 的类型
	fmt.Println(Sum(ints))
	fmt.Println(Sum(floats))
}

alt text

使用泛型结构体

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package main

import "fmt"

type Container[T any] struct {
	Data T
}

func main() {
	// 实例化时需要指定具体类型
	c1 := Container[string]{Data: "Hello Go"}
	c2 := Container[int]{Data: 100}

	fmt.Println(c1.Data, c2.Data)
}

alt text

什么时候使用泛型?

当我们在实现一个通用容器、通用切片或映射操作,以及对底层类型执行相同逻辑的时候,适合使用泛型。

而当我们只是在调用一个方法或处理两个逻辑完全不同的类型时,就不推荐使用了。

泛型约束

常见约束:comparable

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 使用内置约束 comparable 来实现切片内的查找函数
// comparable 支持 == 和 != 操作

func Contains[T comparable](list []T, target T) bool {
	for _, v := range list {
		if v == target {
			return true
		}
	}
	return false
}

尝试用泛型写一个简单的栈(Stack)数据结构