Go by Example - Pointers, Strings and Runes, Structs, Methods, Interfaces, Enums and Struct Embedding

Go by Example - Pointers, Strings and Runes, Structs, Methods, Interfaces, Enums and Struct Embedding

介绍go中的指针, 字符串和字符, 结构体, 方法, 接口, 枚举类型和内联结构体

Pointers

Go 支持指针,允许在程序中传递对值和记录的引用。

pointers.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
package main

import "fmt"

// 我们将通过两个函数:zeroval 和 zeroptr 来展示指针如何与值相对应地工作
// zeroval 有一个 int 参数,因此参数将通过值传递给它
func zeroval(ival int) {
ival = 0
}

// 与此相反,zeroptr 有一个 *int 参数,这意味着它接收一个 int 指针
// 然后,函数体中的 *iptr 代码将指针从其内存地址转到该地址的当前值
// 为取消引用的指针赋值会改变引用地址上的值
func zeroptr(iptr *int) {
*iptr = 0
}

func main() {
i := 1
fmt.Println("initial:", i)

zeroval(i)
fmt.Println("zeroval:", i)

// &i语法给出i的内存地址,即指向i的指针
zeroptr(&i)
fmt.Println("zeroptr:", i)

// 指针也可以打印
fmt.Println("pointer:", &i)
}
log
1
2
3
4
5
$ go run pointers.go
initial: 1
zeroval: 1
zeroptr: 0
pointer: 0x42131100

Zeroval 不改变 main 中的 i,但 zeroptr 改变了,因为它引用了该变量的内存地址。

Strings and Runes

Go 字符串是只读的字节片段。Go 语言和标准库将字符串作为以 UTF-8 编码的文本容器进行特殊处理。在其他语言中,字符串是由 "字符 "组成的。在 Go 中,"字符 "的概念被称为 "符文"--它是一个整数,代表一个 Unicode 代码点。这篇 Go 博文很好地介绍了这一主题。

strings-and-runes.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
40
41
42
43
44
45
46
47
48
49
50
51
52
package main

import (
"fmt"
"unicode/utf8"
)

func main() {
// s是一个字符串,分配了一个表示泰语中单词"hello"的文字值
// Go字符串文字是UTF-8编码的文本
const s = "สวัสดี"

// 由于字符串等同于[]字节,这将产生存储在字符串中的原始字节的长度
fmt.Println("Len:", len(s))

// 对字符串进行索引会在每个索引处生成原始字节值
// 这个循环会生成构成 s 中代码点的所有字节的十六进制值
for i := 0; i < len(s); i++ {
fmt.Printf("%x ", s[i])
}
fmt.Println()

// 要计算字符串中有多少符文,我们可以使用 utf8 软件包
// 请注意,RuneCountInString 的运行时间取决于字符串的大小,因为它必须按顺序解码每个 UTF-8 符节
// 有些泰文字符用 UTF-8 代码点表示,可能跨越多个字节,因此计数结果可能会出人意料
fmt.Println("Rune count:", utf8.RuneCountInString(s))

// 范围循环专门处理字符串,并解码每个符文及其在字符串中的偏移量
for idx, runeValue := range s {
fmt.Printf("%#U starts at %d\n", runeValue, idx)
}

// 我们可以通过显式使用utf8.DecodeRuneInString函数来实现相同的迭代
fmt.Println("\nUsing DecodeRuneInString")
for i, w := 0, 0; i < len(s); i += w {
runeValue, width := utf8.DecodeRuneInString(s[i:])
fmt.Printf("%#U starts at %d\n", runeValue, i)
w = width

// 这演示了将符文值传递给函数
examineRune(runeValue)
}
}

func examineRune(r rune) {
// 用单引号括起来的值是符文文字。我们可以直接将符文值与符文文字进行比较
if r == 't' {
fmt.Println("found tee")
} else if r == 'ส' {
fmt.Println("found so sua")
}
}
log
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ go run strings-and-runes.go
Len: 18
e0 b8 aa e0 b8 a7 e0 b8 b1 e0 b8 aa e0 b8 94 e0 b8 b5
Rune count: 6
U+0E2A 'ส' starts at 0
U+0E27 'ว' starts at 3
U+0E31 'ั' starts at 6
U+0E2A 'ส' starts at 9
U+0E14 'ด' starts at 12
U+0E35 'ี' starts at 15
Using DecodeRuneInString
U+0E2A 'ส' starts at 0
found so sua
U+0E27 'ว' starts at 3
U+0E31 'ั' starts at 6
U+0E2A 'ส' starts at 9
found so sua
U+0E14 'ด' starts at 12
U+0E35 'ี' starts at 15

Structs

Go 的结构体是字段的类型集合。它们可用于将数据分组以形成记录。

structs.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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
package main

import "fmt"

// 此 preson 结构体类型有姓名和年龄字段
type person struct {
name string
age int
}

// newPerson 会使用给定的名称构建一个新的人物结构
func newPerson(name string) *person {

// 您可以安全地返回指向局部变量的指针,因为局部变量将在函数的作用域中继续存在
p := person{name: name}
p.age = 42
return &p
}

func main() {
// 该语法创建了一个新结构体
fmt.Println(person{"Bob", 20})

// 可以在初始化结构时命名字段
fmt.Println(person{name: "Alice", age: 30})

// 省略的字段将为零值
fmt.Println(person{name: "Fred"})

// &前缀会产生一个指向结构体的指针
fmt.Println(&person{name: "Ann", age: 40})

// 在构造函数中封装新结构的创建是惯常的做法
fmt.Println(newPerson("Jon"))

// 使用点访问结构字段
s := person{name: "Sean", age: 50}
fmt.Println(s.name)

// 您也可以在结构指针中使用点,指针会自动取消引用
sp := &s
fmt.Println(sp.age)

// 结构体是可变的
sp.age = 51
fmt.Println(sp.age)

// 如果结构类型只用于单个值,我们就不必给它命名
// 该值可以使用匿名结构类型。这种技术通常用于 table-driven 测试
dog := struct {
name string
isGood bool
}{
"Rex",
true,
}
fmt.Println(dog)
}
log
1
2
3
4
5
6
7
8
9
10
$ go run structs.go
{Bob 20}
{Alice 30}
{Fred 0}
&{Ann 40}
&{Jon 42}
Sean
50
51
{Rex true}

Methods

GO支持在结构体类型上定义方法。

methods.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
package main

import "fmt"

type rect struct {
width, height int
}

// 该 area 方法的接收器类型为 *rect
func (r *rect) area() int {
return r.width * r.height
}

// 可以为指针或值接收器类型定义方法。下面是一个值接收器的示例
func (r rect) perim() int {
return 2*r.width + 2*r.height
}

func main() {
r := rect{width: 10, height: 5}

// 在这里,我们调用为结构定义的两个方法
fmt.Println("area: ", r.area())
fmt.Println("perim:", r.perim())

// Go 会自动处理方法调用中值与指针之间的转换
// 您可能希望使用指针接收器类型来避免方法调用中的复制,或允许方法更改接收结构
rp := &r
fmt.Println("area: ", rp.area())
fmt.Println("perim:", rp.perim())
}
log
1
2
3
4
5
$ go run methods.go
area: 50
perim: 30
area: 50
perim: 30

接下来,我们来看看 Go 用于分组和命名相关方法集的机制:接口。

Interfaces

接口是方法签名的命名集合。

interfaces.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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
package main

import (
"fmt"
"math"
)

// 这是几何图形的基本接口
type geometry interface {
area() float64
perim() float64
}

// 在我们的示例中,我们将在矩形和圆形类型上实现该接口
type rect struct {
width, height float64
}
type circle struct {
radius float64
}

// 要在 Go 中实现一个接口,我们只需实现接口中的所有方法
// 在这里,我们实现了矩形上的几何体
func (r rect) area() float64 {
return r.width * r.height
}
func (r rect) perim() float64 {
return 2*r.width + 2*r.height
}

// 圆的实现
func (c circle) area() float64 {
return math.Pi * c.radius * c.radius
}
func (c circle) perim() float64 {
return 2 * math.Pi * c.radius
}

// 如果变量具有接口类型,那么我们就可以调用命名接口中的方法
// 下面的通用测量函数就是利用了这一点,可用于任何几何体
func measure(g geometry) {
fmt.Println(g)
fmt.Println(g.area())
fmt.Println(g.perim())
}

func main() {
r := rect{width: 3, height: 4}
c := circle{radius: 5}

// 圆和矩形结构类型都实现了几何接口
// 因此我们可以使用这些结构的实例作为参数进行测量
measure(r)
measure(c)
}
log
1
2
3
4
5
6
7
$ go run interfaces.go
{3 4}
12
14
{5}
78.53981633974483
31.41592653589793

要了解更多关于Go界面的信息,请查看这篇很棒的博客文章

Enums

枚举类型(枚举)是总和类型的一种特例。枚举类型有固定数量的可能值,每个值都有一个不同的名称。Go 并没有将枚举类型作为一种独特的语言特性,但枚举类型很容易使用现有的语言习语来实现。

enums.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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
package main

import "fmt"

// 我们的枚举类型ServerState有一个基础的int类型
type ServerState int

// ServerState 的可能值被定义为常量
// 特殊关键字 iota 会自动生成连续的常量值,例如 0、1、2 等等
const (
StateIdle = iota
StateConnected
StateError
StateRetrying
)

// 通过实现 fmt.Stringer 接口,可以将 ServerState 的值打印出来或转换为字符串
// 如果可能的值很多,这可能会很麻烦
// 在这种情况下,可以将 stringer 工具与 go:generate 结合使用,自动完成这一过程
// 更详细的解释请参见本帖: https://eli.thegreenplace.net/2021/a-comprehensive-guide-to-go-generate
var stateName = map[ServerState]string{
StateIdle: "idle",
StateConnected: "connected",
StateError: "error",
StateRetrying: "retrying",
}

func (ss ServerState) String() string {
return stateName[ss]
}

// 如果我们有一个int类型的值,我们不能将它传递给转换-编译器将抱怨类型不匹配
// 这为枚举提供了一定程度的编译时类型安全
func main() {
ns := transition(StateIdle)
fmt.Println(ns)

ns2 := transition(ns)
fmt.Println(ns2)
}

// transition枚举服务器的状态转换;它接收现有状态并返回一个新状态
func transition(s ServerState) ServerState {
switch s {
case StateIdle:
return StateConnected
case StateConnected, StateRetrying:

// 假设我们在这里检查一些谓词,以确定下一个状态...
return StateIdle
case StateError:
return StateError
default:
panic(fmt.Errorf("unwknown state: %s", s))
}

return StateConnected
}
log
1
2
3
$ go run enums.go
connected
idle

Struct Embedding

Go 支持结构体和接口的嵌入,以表达更无缝的类型组合。这不能与 //go:embed 混淆,后者是 Go 1.16+ 版本中引入的 go 指令,用于将文件和文件夹嵌入应用程序二进制文件。

struct-embedding.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
40
41
42
43
44
45
46
package main

import "fmt"

type base struct {
num int
}

func (b base) describe() string {
return fmt.Sprintf("base with num=%v", b.num)
}

// 一个 container 嵌入一个 base 。嵌入看起来就像一个没有名称的字段
type container struct {
base
str string
}

func main() {
// 当创建具有文本的结构时,我们必须显式地初始化嵌入;这里,嵌入类型用作字段名
co := container{
base: base{
num: 1,
},
str: "some name",
}

// 我们可以直接在co上访问基地的字段,例如co.num
fmt.Printf("co={num: %v, str: %v}\n", co.num, co.str)

// 或者,我们可以使用嵌入的类型名称拼写出完整的路径
fmt.Println("also num:", co.base.num)

// 由于 container 嵌入了 base,因此 base 的方法也成为 container 的方法
// 在这里,我们调用了一个从 base 直接嵌入到 co 的方法
fmt.Println("describe:", co.describe())

type describer interface {
describe() string
}

// 使用方法嵌入结构可以用于将接口实现赋给其他结构
// 在这里,我们看到容器现在实现了describer接口,因为它嵌入了base
var d describer = co
fmt.Println("describer:", d.describe())
}
log
1
2
3
4
5
$ go run struct-embedding.go
co={num: 1, str: some name}
also num: 1
describe: base with num=1
describer: base with num=1

参考链接

Go by Example - Pointers, Strings and Runes, Structs, Methods, Interfaces, Enums and Struct Embedding

https://blog.wty.cool/2024/02/03/go_by_example/Pointers-Strings_and_Runes-Structs-Methods-Interfaces-Enums-Struct_Embedding/

作者

孤独小狼

发布于

2024-02-03

更新于

2024-02-03

许可协议

评论