Go

开始

1
2
3
4
5
6
7
package main

import "fmt"

func main() {
fmt.Println("hello world")
}
1
2
3
4
5
6
7
8
9
10
11
12
要运行这个程序,先将将代码放到名为 hello-world.go 的文件中,然后执行 go run
$ go run hello-world.go
hello world

如果我们想将程序编译成二进制文件(Windows 平台是 .exe 可执行文件), 可以通过 go build 来达到目的。
$ go build hello-world.go
$ ls
hello-world hello-world.go

然后我们可以直接运行这个二进制文件。
$ ./hello-world
hello world

变量

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

import "fmt"

func main() {
// var 声明 1 个或者多个变量。
var a = "initial"
fmt.Println(a)

var b, c int = 1, 2
fmt.Println(b, c)
// Go 会自动推断已经有初始值的变量的类型。
var d = true
fmt.Println(d)
// 声明后却没有给出对应的初始值时,变量将会初始化为 零值
var e int
fmt.Println(e)
// := 语法是声明并初始化变量的简写, 例如 var f string = "short" 可以简写为右边这样
f := "short"
fmt.Println(f)
}
1
2
3
4
5
6
$ go run variables.go
initial
1 2
true
0
short

常量

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

import (
"fmt"
"math"
)

const s string = "constant"

func main() {
fmt.Println(s)
// const 语句可以出现在任何 var 语句可以出现的地方
const n = 500000000

const d = 3e20 / n
fmt.Println(d)
// 数值型常量没有确定的类型,直到被给定某个类型,比如显式类型转化。
fmt.Println(int64(d))
// 一个数字可以根据上下文的需要(比如变量赋值、函数调用)自动确定类型。
fmt.Println(math.Sin(n))
}
1
2
3
4
5
$ go run constant.go 
constant
6e+11
600000000000
-0.28470407323754404

循环

for 是 Go 中唯一的循环结构。这里会展示 for 循环的一些基本使用方式。

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

import "fmt"

func main() {

i := 1
for i <= 3 {
fmt.Println(i)
i = i + 1
}

for j := 7; j <= 9; j++ {
fmt.Println(j)
}

for {
fmt.Println("loop")
break
}

for n := 0; n <= 5; n++ {
if n%2 == 0 {
continue
}
fmt.Println(n)
}
}
1
2
3
4
5
6
7
8
9
10
11
$ go run for.go
1
2
3
7
8
9
loop
1
3
5

if/else分支

注意,在 Go 中,条件语句的圆括号不是必需的,但是花括号是必需的。

Go 没有三目运算符, 即使是基本的条件判断,依然需要使用完整的 if 语句。

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"

func main() {

if 7%2 == 0 {
fmt.Println("7 is even")
} else {
fmt.Println("7 is odd")
}

if 8%4 == 0 {
fmt.Println("8 is divisible by 4")
}

if num := 9; num < 0 {
fmt.Println(num, "is negative")
} else if num < 10 {
fmt.Println(num, "has 1 digit")
} else {
fmt.Println(num, "has multiple digits")
}
}
1
2
3
4
$ go run if-else.go 
7 is odd
8 is divisible by 4
9 has 1 digit

switch分支

switch 是多分支情况时快捷的条件语句。

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"
"time"
)

func main() {

i := 2
fmt.Print("write ", i, " as ")
switch i {
case 1:
fmt.Println("one")
case 2:
fmt.Println("two")
case 3:
fmt.Println("three")
}

switch time.Now().Weekday() {
case time.Saturday, time.Sunday:
fmt.Println("It's the weekend")
default:
fmt.Println("It's a weekday")
}

t := time.Now()
switch {
case t.Hour() < 12:
fmt.Println("It's before noon")
default:
fmt.Println("It's after noon")
}

whatAmI := func(i interface{}) {
switch t := i.(type) {
case bool:
fmt.Println("I'm a bool")
case int:
fmt.Println("I'm an int")
default:
fmt.Printf("Don't know type %T\n", t)
}
}
whatAmI(true)
whatAmI(1)
whatAmI("hey")
}
1
2
3
4
5
6
7
$ go run switch.go
Write 2 as two
It's a weekday
It's after noon
I'm a bool
I'm an int
Don't know type string

数组

在 Go 语言中,数组(Array)是一种固定长度的集合类型,它存储一组同类型的元素。数组的长度是固定的,一旦定义了数组的大小,就不能改变。

1. 数组的定义

数组的定义包括指定元素的类型和数组的长度。例如,一个存储 5 个整数的数组可以这样定义:

1
var arr [5]int  // 定义一个长度为 5 的整数数组

也可以在定义时直接初始化数组的元素:

1
var arr = [3]int{1, 2, 3}  // 定义并初始化数组

或者 Go 支持根据初始化值自动推导数组的长度:

1
var arr = [...]int{1, 2, 3}  // 自动推导长度为 3

2. 数组的访问和修改

数组的元素通过索引访问,索引从 0 开始。可以通过下标访问和修改数组的元素:

1
2
3
4
arr := [3]int{1, 2, 3}
fmt.Println(arr[0]) // 输出 1
arr[1] = 10 // 修改第二个元素
fmt.Println(arr[1]) // 输出 10

3. 数组的长度

数组的长度是固定的,可以使用 len() 函数获取数组的长度:

1
2
arr := [3]int{1, 2, 3}
fmt.Println(len(arr)) // 输出 3

4. 数组的传递

在 Go 中,数组是值类型,当你将一个数组作为参数传递给函数时,实际上是传递了该数组的副本。如果你修改了副本中的元素,原始数组不会受到影响。

1
2
3
4
5
6
7
func modifyArray(arr [3]int) {
arr[0] = 100
}

arr := [3]int{1, 2, 3}
modifyArray(arr)
fmt.Println(arr) // 输出 [1, 2, 3],原始数组没有被修改

如果你希望修改原数组的内容,可以传递指向数组的指针:

1
2
3
4
5
6
7
func modifyArray(arr *[3]int) {
arr[0] = 100
}

arr := [3]int{1, 2, 3}
modifyArray(&arr)
fmt.Println(arr) // 输出 [100, 2, 3],原数组已被修改

5. 多维数组

Go 支持多维数组,可以通过嵌套的方式定义多维数组。例如,定义一个 2 行 3 列的数组:

1
2
3
4
5
6
7
8
var arr [2][3]int
arr[0][0] = 1
arr[0][1] = 2
arr[0][2] = 3
arr[1][0] = 4
arr[1][1] = 5
arr[1][2] = 6
fmt.Println(arr)

输出:

1
[[1 2 3] [4 5 6]]

6. 数组的初始化

数组可以在声明时进行初始化,也可以在定义之后进行逐一赋值。以下是几种初始化数组的方式:

  • 声明并初始化:

    1
    arr := [3]int{1, 2, 3}  // 定义并初始化数组
  • 使用 ... 自动推导数组长度:

    1
    arr := [...]int{1, 2, 3}  // 自动推导数组长度为 3
  • 使用 new 函数创建一个指向数组的指针:

    1
    2
    3
    arr := new([3]int)  // 创建一个指向长度为 3 的数组的指针
    arr[0] = 10
    fmt.Println(arr) // 输出 &[10 0 0]

7. 数组与切片的区别

数组和切片的区别是一个重要的概念,尤其是 Go 语言的切片(Slice)非常常用。切片是对数组的一个抽象,允许灵活的动态扩展和缩减,而数组的大小是固定的。

  • 数组:长度固定,类型和值传递。
  • 切片:动态大小,引用传递,可以灵活扩展。

示例,使用切片:

1
2
3
arr := [3]int{1, 2, 3}    // 数组
slice := arr[:] // 切片
fmt.Println(slice) // 输出 [1 2 3]

8. 数组的应用场景

数组在 Go 中通常用于以下几种情况:

  • 当需要固定大小的数据集合时。
  • 需要用到固定大小的内存时(例如嵌入式系统或性能优化的场景)。
  • 数组的值传递方式可以避免不必要的内存共享,确保数据不会被意外修改。

总结

  • Go 语言中的数组是固定长度的,定义时需要指定数组的长度。
  • 数组通过下标访问和修改元素,且下标从 0 开始。
  • Go 中的数组是值类型,传递数组时会复制副本。如果希望修改原始数组,需要传递指针。
  • 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
package main

import "fmt"

func main() {

var a [5]int
fmt.Println("emp:", a)

a[4] = 100
fmt.Println("set:", a)
fmt.Println("get:", a[4])

fmt.Println("len:", len(a))

b := [5]int{1, 2, 3, 4, 5}
fmt.Println("dcl:", b)

var twoD [2][3]int
for i := 0; i < 2; i++ {
for j := 0; j < 3; j++ {
twoD[i][j] = i + j
}
}
fmt.Println("2d: ", twoD)
}
1
2
3
4
5
6
7
$ go run arrays.go
emp: [0 0 0 0 0]
set: [0 0 0 0 100]
get: 100
len: 5
dcl: [1 2 3 4 5]
2d: [[0 1 2] [1 2 3]]

切片

在 Go 语言中,slice(切片)是一种灵活、动态的数组类型,它是对数组的一个引用,具有更高效的内存管理和更灵活的功能。切片不像数组那样具有固定的长度,它可以动态增长和缩小。

1. 创建切片

  • 切片的创建有几种方式:

    • 使用 make 函数:

      1
      slice := make([]int, 5) // 创建一个长度为5的切片

      你还可以指定切片的容量:

      1
      slice := make([]int, 5, 10) // 创建一个长度为5,容量为10的切片
    • 直接使用数组字面量:

      1
      slice := []int{1, 2, 3, 4, 5} // 创建一个包含5个元素的切片

2. 切片的属性

  • 每个切片都包含三个基本属性:

    • 指针(Pointer): 指向底层数组的指针。
    • 长度(Length): 切片中元素的数量。
    • 容量(Capacity): 切片底层数组的大小,从切片的起始位置到底层数组的末尾。

    你可以通过 len()cap() 函数获取切片的长度和容量:

    1
    2
    fmt.Println(len(slice)) // 输出切片的长度
    fmt.Println(cap(slice)) // 输出切片的容量

3. 切片的切割操作

  • 切片可以从另一个切片或数组中通过索引和范围来切割:

    1
    2
    3
    slice := []int{1, 2, 3, 4, 5}
    subSlice := slice[1:4] // 获取从索引1到索引3的元素
    fmt.Println(subSlice) // 输出: [2 3 4]

    你还可以指定切片的容量:

    1
    2
    subSlice := slice[1:4:5] // 切片从索引1到索引3,容量为5
    fmt.Println(subSlice) // 输出: [2 3 4]

4. 动态扩展切片

  • 使用 append 扩展切片:

    1
    2
    3
    slice := []int{1, 2, 3}
    slice = append(slice, 4, 5) // 扩展切片
    fmt.Println(slice) // 输出: [1 2 3 4 5]

append 函数用于向切片中添加元素,并且如果需要,它会自动调整切片的容量。

5. 删除切片元素

  • 删除指定索引的元素:

    1
    2
    3
    4
    slice = append(slice[:index], slice[index+1:]...)
    slice := []int{1, 2, 3, 4, 5}
    slice = append(slice[:2], slice[3:]...) // 删除索引2的元素
    fmt.Println(slice) // 输出: [1 2 4 5]

6. 切片的反转

  • 反转切片元素:

    1
    2
    3
    for i := 0; i < len(slice)/2; i++ {
    slice[i], slice[len(slice)-1-i] = slice[len(slice)-1-i], slice[i]
    }

7. 切片的查找

  • 查找指定元素:

    1
    2
    3
    4
    5
    6
    7
    found := false
    for _, v := range slice {
    if v == target {
    found = true
    break
    }
    }

8. 切片的分块

  • 将切片分割成多个小块:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    func chunk(slice []int, size int) [][]int {
    var chunks [][]int
    for i := 0; i < len(slice); i += size {
    end := i + size
    if end > len(slice) {
    end = len(slice)
    }
    chunks = append(chunks, slice[i:end])
    }
    return chunks
    }

9. 切片排序

  • 对切片进行排序:

    1
    2
    import "sort"
    sort.Ints(slice) // 对整数切片进行升序排序

10. 删除重复元素

  • 去除切片中的重复元素:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    func removeDuplicates(slice []int) []int {
    seen := make(map[int]struct{})
    result := []int{}
    for _, v := range slice {
    if _, ok := seen[v]; !ok {
    seen[v] = struct{}{}
    result = append(result, v)
    }
    }
    return result
    }

11. copy 函数

  • 复制切片内容:

    1
    copy(dst, src) // 将 src 的内容复制到 dst

总结

Go 切片提供了高效且灵活的操作方式,以下是常见的切片操作总结:

  • 创建:通过 make 或字面量创建。
  • 切割:使用切片操作符可以截取切片的部分。
  • 扩展:通过 append 函数动态扩展切片。
  • 删除:通过 append 删除切片中的元素。
  • 反转:通过交换元素实现反转。
  • 查找:通过遍历切片查找元素。
  • 分块:将切片分割成多个小块。
  • 排序:使用标准库中的 sort 对切片进行排序。
  • 去重:通过 map 去除切片中的重复元素。
  • 复制:使用 copy 将一个切片的内容复制到另一个切片中。
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
package main

import "fmt"

func main() {

s := make([]string, 3)
fmt.Println("emp:", s)

s[0] = "a"
s[1] = "b"
s[2] = "c"
fmt.Println("set:", s)
fmt.Println("get:", s[2])

fmt.Println("len:", len(s))

s = append(s, "d")
s = append(s, "e", "f")
fmt.Println("apd:", s)

c := make([]string, len(s))
copy(c, s)
fmt.Println("cpy:", c)

l := s[2:5]
fmt.Println("sl1:", l)

l = s[:5]
fmt.Println("sl2:", l)

l = s[2:]
fmt.Println("sl3:", l)

t := []string{"g", "h", "i"}
fmt.Println("dcl:", t)

twoD := make([][]int, 3)
for i := 0; i < 3; i++ {
innerLen := i + 1
twoD[i] = make([]int, innerLen)
for j := 0; j < innerLen; j++ {
twoD[i][j] = i + j
}
}
fmt.Println("2d: ", twoD)
}
1
2
3
4
5
6
7
8
9
10
11
12
$ go run slices.go
emp: [ ]
set: [a b c]
get: c
len: 3
apd: [a b c d e f]
cpy: [a b c d e f]
sl1: [c d e]
sl2: [a b c d e]
sl3: [c d e f]
dcl: [g h i]
2d: [[0] [1 2] [2 3 4]]

map

在Go语言中,map 是一种内建的数据类型,用于存储键值对(key-value pairs)。它类似于其他语言中的哈希表或字典,能够快速地根据键查找对应的值。

1. 创建 map

Go语言中的 map 可以通过内建的 make 函数或者字面量(literal)来创建。

使用 make 函数

1
m := make(map[string]int)

上面的代码创建了一个空的 map,键为 string 类型,值为 int 类型。

使用字面量(literal)创建

1
2
3
4
m := map[string]int{
"apple": 5,
"banana": 3,
}

上面的代码创建了一个初始包含两个键值对的 map

2. 向 map 中添加和更新元素

你可以直接通过键来添加或更新值:

1
2
m["apple"] = 10  // 更新值
m["orange"] = 7 // 添加新元素

3. 获取 map 中的值

通过键来访问对应的值:

1
2
value := m["apple"]
fmt.Println(value) // 输出 10

4. 删除 map 中的元素

使用 delete 函数来删除指定键的元素:

1
delete(m, "banana")  // 删除键为"banana"的元素

5. 检查键是否存在

通过多返回值的方式来判断键是否存在。如果键存在,第二个返回值为 true,否则为 false

1
2
3
4
5
6
value, ok := m["apple"]
if ok {
fmt.Println("Found:", value)
} else {
fmt.Println("Not found")
}

6. 遍历 map

使用 for 循环来遍历 map 的所有键值对:

1
2
3
for key, value := range m {
fmt.Println(key, value)
}

7. map 的特性

  • 无序性map 中的元素是无序的,遍历时顺序是随机的。
  • 线程不安全:在多线程(goroutines)中并发修改同一个 map 时,Go 运行时会引发恐慌(panic)。如果需要并发访问 map,应使用 sync.Mutex 或者 sync.RWMutex 来加锁。

8. map 的容量

可以使用 len() 函数获取 map 中元素的数量:

1
fmt.Println(len(m))  // 输出 map 中的元素个数

9. 空 mapnilmap

  • 如果 map 没有被初始化(nil),则它的行为类似于空 map,但是尝试向 nil map 中添加或删除元素会引发运行时错误。
  • 可以使用 make 函数初始化一个空 map

示例代码:

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"

func main() {
// 创建一个 map
m := make(map[string]int)

// 向 map 中添加元素
m["apple"] = 5
m["banana"] = 2

// 更新元素
m["apple"] = 10

// 获取元素
if value, ok := m["apple"]; ok {
fmt.Println("Apple:", value)
}

// 删除元素
delete(m, "banana")

// 遍历 map
for key, value := range m {
fmt.Println(key, value)
}

// 获取 map 的长度
fmt.Println("Length of map:", len(m))
}
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
package main

import "fmt"

func main() {

m := make(map[string]int)

m["k1"] = 7
m["k2"] = 13

fmt.Println("map:", m)

v1 := m["k1"]
fmt.Println("v1: ", v1)

fmt.Println("len:", len(m))

delete(m, "k2")
fmt.Println("map:", m)

_, prs := m["k2"]
fmt.Println("prs:", prs)

n := map[string]int{"foo": 1, "bar": 2}
fmt.Println("map:", n)
}
1
2
3
4
5
6
7
$ go run maps.go 
map: map[k1:7 k2:13]
v1: 7
len: 2
map: map[k1:7]
prs: false
map: map[foo:1 bar:2]

range

在Go语言中,range 是一个非常强大的关键字,用于遍历数组、切片、map 和通道等数据结构。通过 range,可以轻松遍历数据结构的元素,并同时获得索引(或键)和值。

1. range 遍历数组和切片

数组

1
2
3
4
arr := [3]int{1, 2, 3}
for index, value := range arr {
fmt.Println(index, value)
}

在遍历数组时,range 返回两个值:

  • index:当前元素的索引。
  • value:当前元素的值。

切片

1
2
3
4
slice := []string{"apple", "banana", "cherry"}
for index, value := range slice {
fmt.Println(index, value)
}

对于切片,range 也返回两个值:

  • index:当前元素的索引。
  • value:当前元素的值。

2. range 遍历 map

map 中,range 返回的是键值对:

1
2
3
4
m := map[string]int{"apple": 5, "banana": 3}
for key, value := range m {
fmt.Println(key, value)
}

对于 maprange 返回两个值:

  • key:当前元素的键。
  • value:当前元素的值。

注意事项:

  • map 的遍历顺序是无序的,每次遍历的顺序可能不同。

3. range 遍历字符串

range 也可以用于遍历字符串。它会逐个获取字符串中的 Unicode 字符,而不是按字节遍历:

1
2
3
4
str := "hello"
for index, runeValue := range str {
fmt.Println(index, runeValue)
}

在遍历字符串时,range 返回两个值:

  • index:当前字符的索引。
  • runeValue:当前字符的 Unicode 码点值(rune 类型)。

4. 只获取值或索引

你可以通过 _ 忽略 range 返回的某些值。

  • 只获取索引:
1
2
3
for index := range arr {
fmt.Println(index)
}
  • 只获取值:
1
2
3
for _, value := range arr {
fmt.Println(value)
}

5. range 遍历通道(channel)

range 也可以用来遍历通道(channel)中的元素。当通道关闭时,range 循环会停止。

1
2
3
4
5
6
7
8
9
ch := make(chan int, 3)
ch <- 1
ch <- 2
ch <- 3
close(ch)

for value := range ch {
fmt.Println(value)
}

当通道关闭后,range 会自动遍历通道中的所有元素。

6. 示例:综合使用 range

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

import "fmt"

func main() {
// 遍历数组
arr := [3]int{1, 2, 3}
for index, value := range arr {
fmt.Printf("arr[%d] = %d\n", index, value)
}

// 遍历切片
slice := []string{"apple", "banana", "cherry"}
for _, value := range slice {
fmt.Println(value)
}

// 遍历map
m := map[string]int{"apple": 5, "banana": 3}
for key, value := range m {
fmt.Printf("%s has %d fruits\n", key, value)
}

// 遍历字符串
str := "hello"
for index, runeValue := range str {
fmt.Printf("Index: %d, Rune: %c\n", index, runeValue)
}
}

总结

  • range 是 Go 语言中遍历数据结构的关键字,可以遍历数组、切片、map、通道等。
  • 在遍历 map 时,range 返回的是键和值;遍历数组或切片时,返回的是索引和值。
  • 可以使用 _ 来忽略不需要的返回值(如只关注索引或只关注值)。
  • range 在处理通道时会在通道关闭时停止。

range 是一个高效且灵活的工具,能够简化代码并提高可读性。

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

import "fmt"

func main() {

nums := []int{2, 3, 4}
sum := 0
for _, num := range nums {
sum += num
}
fmt.Println("sum:", sum)

for i, num := range nums {
if num == 3 {
fmt.Println("index:", i)
}
}

kvs := map[string]string{"a": "apple", "b": "banana"}
for k, v := range kvs {
fmt.Printf("%s -> %s\n", k, v)
}

for k := range kvs {
fmt.Println("key:", k)
}

for i, c := range "go" {
fmt.Println(i, c)
}
}
1
2
3
4
5
6
7
8
9
$ go run range.go
sum: 9
index: 1
a -> apple
b -> banana
key: a
key: b
0 103
1 111

函数

在 Go 语言中,函数(function)是基本的代码结构之一,用于封装一组语句,并通过函数调用来执行这些语句。函数可以有输入参数、返回值,也可以没有参数或返回值。

1. 定义函数

Go 语言使用 func 关键字来定义函数。

基本函数定义

1
2
3
func add(a int, b int) int {
return a + b
}

这个函数的定义包含了:

  • func 关键字,表示定义一个函数。
  • add,函数的名称。
  • (a int, b int),是函数的参数列表,表示这个函数接收两个 int 类型的参数。
  • int,是返回值的类型,表示该函数返回一个 int 类型的结果。

2. 调用函数

函数定义后,可以通过函数名调用它,并传递参数:

1
2
result := add(3, 4)
fmt.Println(result) // 输出 7

3. 函数参数

  • 单一参数类型:可以在函数参数中使用相同类型的多个参数,简化代码。
1
2
3
func add(a, b int) int {
return a + b
}
  • 可变参数:Go 支持传递可变数量的参数,使用 ... 来表示可变参数。
1
2
3
4
5
6
7
func sum(numbers ...int) int {
total := 0
for _, number := range numbers {
total += number
}
return total
}

调用:

1
fmt.Println(sum(1, 2, 3, 4))  // 输出 10

4. 返回值

Go 函数可以返回多个值:

1
2
3
func swap(a, b int) (int, int) {
return b, a
}

调用:

1
2
x, y := swap(1, 2)
fmt.Println(x, y) // 输出 2 1

5. 命名返回值

Go 允许给返回值命名,返回值就像局部变量一样,可以在函数体内直接使用。这也简化了代码,使得不需要显式地使用 return 语句。

1
2
3
4
func add(a, b int) (sum int) {
sum = a + b
return // 使用命名返回值
}

6. 函数作为值

Go 允许函数作为值传递。这意味着可以将函数赋值给变量或作为参数传递给其他函数。

将函数赋值给变量

1
2
3
4
5
6
func multiply(a, b int) int {
return a * b
}

var f func(int, int) int = multiply
fmt.Println(f(2, 3)) // 输出 6

函数作为参数

1
2
3
4
5
func operate(a, b int, op func(int, int) int) int {
return op(a, b)
}

fmt.Println(operate(2, 3, multiply)) // 输出 6

7. 匿名函数

Go 支持匿名函数,即没有函数名的函数。匿名函数常常作为回调函数或临时使用。

1
2
3
func() {
fmt.Println("Hello from anonymous function!")
}()

8. 函数闭包

Go 支持闭包,闭包是一个函数,它可以“记住”并访问其外部作用域中的变量。即使外部函数返回,闭包仍然可以访问这些变量。

1
2
3
4
5
6
7
8
9
10
11
func outer() func() int {
x := 10
return func() int {
x++
return x
}
}

increment := outer()
fmt.Println(increment()) // 输出 11
fmt.Println(increment()) // 输出 12

在上面的例子中,increment 是一个闭包,它记住了 x 的值,并且每次调用时都会更新并返回新的值。

9. 函数的递归调用

Go 支持函数的递归调用,即一个函数在其定义中调用自己。

1
2
3
4
5
6
7
func factorial(n int) int {
if n == 0 {
return 1
}
return n * factorial(n-1)
}
fmt.Println(factorial(5)) // 输出 120

在这个例子中,factorial 函数调用自己来计算阶乘。

10. 多返回值函数

Go 允许函数返回多个值,常用于处理错误处理等场景。

1
2
3
4
5
6
7
8
9
10
11
12
13
func divide(a, b int) (int, error) {
if b == 0 {
return 0, fmt.Errorf("division by zero")
}
return a / b, nil
}

result, err := divide(10, 2)
if err != nil {
fmt.Println(err)
} else {
fmt.Println(result) // 输出 5
}

总结

  • Go 语言中的函数是非常灵活的,可以定义单一返回值、多个返回值、可变参数和命名返回值等。
  • Go 支持将函数作为值传递,允许匿名函数和闭包等特性。
  • 函数调用时,可以直接使用返回值,也可以通过传递函数作为参数来进行更高阶的编程。

基础用法:

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

import "fmt"

func plus(a int, b int) int {

return a + b
}

func plusPlus(a, b, c int) int {
return a + b + c
}

func main() {

res := plus(1, 2)
fmt.Println("1+2 =", res)

res = plusPlus(1, 2, 3)
fmt.Println("1+2+3 =", res)
}
1
2
3
4
	
$ go run functions.go
1+2 = 3
1+2+3 = 6

多返回值:

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

import "fmt"

func vals() (int, int) {
return 3, 7
}

func main() {

a, b := vals()
fmt.Println(a)
fmt.Println(b)

_, c := vals()
fmt.Println(c)
}
1
2
3
4
5
	
$ go run multiple-return-values.go
3
7
7

变参函数:

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

import "fmt"

func sum(nums ...int) {
fmt.Print(nums, " ")
total := 0
for _, num := range nums {
total += num
}
fmt.Println(total)
}

func main() {

sum(1, 2)
sum(1, 2, 3)

nums := []int{1, 2, 3, 4}
sum(nums...)
}
1
2
3
4
$ go run variadic-functions.go 
[1 2] 3
[1 2 3] 6
[1 2 3 4] 10

闭包:

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

import "fmt"

func intSeq() func() int {
i := 0
return func() int {
i++
return i
}
}

func main() {

nextInt := intSeq()

fmt.Println(nextInt())
fmt.Println(nextInt())
fmt.Println(nextInt())

newInts := intSeq()
fmt.Println(newInts())
}
1
2
3
4
5
$ go run closures.go
1
2
3
1

递归:

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

import "fmt"

func fact(n int) int {
if n == 0 {
return 1
}
return n * fact(n-1)
}

func main() {
fmt.Println(fact(7))

var fib func(n int) int

fib = func(n int) int {
if n < 2 {
return n
}
return fib(n-1) + fib(n-2)

}

fmt.Println(fib(7))
}
1
2
3
$ go run recursion.go 
5040
13

指针

在 Go 语言中,指针是存储变量内存地址的类型,它允许程序间接访问变量的值。指针可以非常方便地用于修改函数外部的变量值,以及在内存中操作数据。Go 的指针与 C 语言类似,但它不允许进行指针算术运算,这让它更加安全。

1. 定义和声明指针

在 Go 中,指针的声明方式是使用 * 表示指向某个类型的指针,类型前加 * 表示该变量是该类型的指针。

1
var ptr *int

这意味着 ptr 是一个指向 int 类型的指针,但它当前没有指向任何具体的内存地址。

2. 获取变量的地址(取地址操作符 &

要获取一个变量的内存地址,可以使用 & 操作符,它会返回变量的地址。

1
2
3
x := 10
ptr := &x // ptr 是 x 的指针
fmt.Println(ptr) // 输出 x 的地址

3. 解引用指针(取值操作符 *

通过指针访问它所指向的变量的值,使用 * 操作符。这个过程叫做“解引用”。

1
2
3
x := 10
ptr := &x // 获取 x 的地址
fmt.Println(*ptr) // 输出 10,解引用指针,得到 x 的值

4. 指针与变量的关系

  • & 获取变量的地址。
  • * 解引用指针,获取指针所指向的值。

示例:

1
2
3
4
x := 10
ptr := &x // 获取 x 的地址
fmt.Println(ptr) // 输出 x 的内存地址
fmt.Println(*ptr) // 输出 10,通过指针访问 x 的值

5. 修改变量的值通过指针

指针允许通过间接访问修改原变量的值。通过解引用指针可以修改它指向的变量。

1
2
3
4
x := 10
ptr := &x
*ptr = 20 // 修改 x 的值,通过 ptr 指针
fmt.Println(x) // 输出 20

6. 指针作为函数参数

在 Go 中,函数参数是值传递的。如果希望函数修改传入的变量,可以传递变量的指针。这通常用于避免复制大量数据的成本,或允许函数修改传入的参数。

1
2
3
4
5
6
7
func modifyValue(a *int) {
*a = 20 // 通过指针修改原始值
}

x := 10
modifyValue(&x) // 传递 x 的指针
fmt.Println(x) // 输出 20

7. 指针类型与结构体

结构体也可以使用指针,结构体指针常用于修改结构体的成员,避免复制结构体的副本。

1
2
3
4
5
6
7
8
9
10
11
12
13
type Person struct {
Name string
Age int
}

func updateAge(p *Person) {
p.Age = 30 // 修改 Person 的 Age
}

p := &Person{Name: "John", Age: 25}
fmt.Println(p.Age) // 输出 25
updateAge(p)
fmt.Println(p.Age) // 输出 30

8. nil 指针

指针可以是 nil,即它不指向任何有效的内存地址。你可以检查指针是否为 nil 来避免解引用空指针。

1
2
3
4
var ptr *int
if ptr == nil {
fmt.Println("ptr is nil")
}

9. 零值和指针

Go 中,指针的零值是 nil,表示它没有指向任何有效的内存地址。当一个指针没有显式初始化时,它的默认值就是 nil

1
2
var ptr *int  // ptr 默认为 nil
fmt.Println(ptr) // 输出 nil

10. 指针的应用

  • 修改函数外部的变量值:通过指针参数来修改传入的变量。
  • 避免大数据的复制:对于大的数据结构(如数组或结构体),可以通过指针传递,避免复制整个数据结构。
  • 链表、树等数据结构:指针广泛应用于实现链表、二叉树等动态数据结构。

总结

指针是 Go 语言中的一个重要特性,它让你能够间接操作内存,并通过引用传递修改变量的值。Go 语言中的指针有如下特点:

  • 不允许指针算术运算,避免了 C 语言中的很多潜在错误。
  • 使用 & 获取变量的地址,使用 * 解引用指针。
  • 可以通过指针修改变量的值,传递指针给函数可以改变函数外的变量。

指针在 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
package main

import "fmt"

func zeroval(ival int) {
ival = 0
}

func zeroptr(iptr *int) {
*iptr = 0
}

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

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

zeroptr(&i)
fmt.Println("zeroptr:", i)

fmt.Println("pointer:", &i)
}
1
2
3
4
5
$ go run pointers.go
initial: 1
zeroval: 1
zeroptr: 0
pointer: 0x42131100

字符串和rune类型

一、字符串(string)

1. 字符串的定义

  • 不可变性:字符串是不可变的,即一旦创建后,无法直接修改其中的内容。任何修改操作都会生成一个新的字符串。
  • 底层结构:字符串底层是一个只读的字节序列([]byte
  • 编码:通常用来存储 UTF-8 编码的数据,可以包含 ASCII 字符、汉字、特殊符号等。

2. 字符串的常见操作

声明与初始化
  • 使用双引号声明普通字符串。
  • 使用反引号声明原始字符串,保留原格式,包括换行和特殊字符。
1
2
3
s1 := "hello, world"         // 普通字符串
s2 := `hello,
world with "quotes"` // 原始字符串
获取字符串长度
  • 使用 len() 获取字符串长度,返回的是字节数而非字符数。
1
2
s := "你好"
fmt.Println(len(s)) // 输出:6,因为每个汉字占3个字节
索引访问字符串
  • 可以通过索引访问字符串中的字节:
1
2
3
s := "hello"
fmt.Println(s[0]) // 输出:104,'h' 的 ASCII 值
fmt.Printf("%c\n", s[0]) // 输出:h
  • 注意:通过索引访问的是单个字节,而不是字符。
字符串拼接
  • 使用 +fmt.Sprintf() 拼接字符串。
1
2
3
4
s1 := "hello"
s2 := "world"
s3 := s1 + ", " + s2 + "!"
fmt.Println(s3) // 输出:hello, world!
切片操作
  • 支持通过切片截取子字符串,但结果是字节切片,可能导致截断多字节字符。
1
2
3
s := "你好世界"
sub := s[:3] // 截取前3个字节
fmt.Println(sub) // 输出:乱码(截断了“你”)

3. Go标准库中的字符串方法

Go 提供了 strings 包来处理字符串,包括查找、替换、切分等操作。

常用函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import "strings"

// 查找子串
fmt.Println(strings.Contains("hello, world", "world")) // true
fmt.Println(strings.Index("hello, world", "o")) // 4
fmt.Println(strings.LastIndex("hello, world", "o")) // 8

// 切分与连接
parts := strings.Split("a,b,c", ",") // [a b c]
joined := strings.Join(parts, "-") // a-b-c

// 替换
replaced := strings.ReplaceAll("hello, hello", "hello", "hi") // hi, hi

// 大小写转换
fmt.Println(strings.ToUpper("hello")) // HELLO
fmt.Println(strings.ToLower("WORLD")) // world

二、Rune

1. Rune的定义

  • rune 是 Go 的一个别名类型,等价于 int32,用于表示单个 Unicode 字符
  • 每个 rune 占用4个字节,能够表示所有 Unicode 代码点。

2. Rune的用途

  • 用于处理多字节字符(如汉字、emoji)或逐字符操作。
  • 可以将字符串转换为 []rune,以逐字符处理,而不是逐字节。

3. Rune的常见操作

字符串与Rune的相互转换
  • 将字符串转为 []rune

    1
    2
    runes := []rune("你好世界")
    fmt.Println(runes) // [20320 22909 19990 30028]
  • []rune 转为字符串:

    1
    2
    s := string([]rune{20320, 22909, 19990, 30028})
    fmt.Println(s) // 你好世界
逐字符处理
  • 使用 for遍历 []rune 可以逐字符操作:

    1
    2
    3
    4
    s := "hello, 世界"
    for i, r := range []rune(s) {
    fmt.Printf("第%d个字符:%c (Unicode: %U)\n", i, r, r)
    }
修改字符串
  • 字符串不可变,但可以通过 []rune修改后重新构造:

    1
    2
    3
    4
    5
    s := "hello"
    runes := []rune(s)
    runes[0] = 'H'
    s = string(runes)
    fmt.Println(s) // Hello
统计字符数量
  • 使用 len() 获取的是字节数。若要获取字符数,需要将字符串转换为 []rune

    1
    2
    3
    s := "你好世界"
    fmt.Println(len(s)) // 12(字节数)
    fmt.Println(len([]rune(s))) // 4(字符数)

三、字符串与Rune的关系

特性字符串(string)rune
类型不可变的字节序列表示单个 Unicode 代码点
内存占用每个字符占用 1~4 字节(UTF-8 编码)每个字符固定占用 4 字节(int32 类型)
表示范围UTF-8 编码的字节序列Unicode 字符
适用场景用于存储和操作整段文本用于逐字符处理,支持多字节字符
索引操作索引访问的是字节可以通过 []rune 访问字符

四、使用场景与注意事项

  1. 字符串处理
    • 如果主要处理整段文本数据,使用 string
    • 避免直接索引多字节字符,否则可能导致错误。
  2. 多字节字符处理
    • 如果需要逐字符操作,使用 []rune
    • 转换为 []rune 后,才能安全地进行字符级别的索引和修改。
  3. 性能权衡
    • 操作 string 通常更高效,因为底层是只读的 []byte
    • 使用 []rune 需要额外的内存,适合复杂字符处理场景。

五、总结

  • 字符串(string):主要用于存储和处理文本,适合完整的文本操作。支持丰富的库函数,如查找、替换、切分等。
  • Rune:用来处理单个 Unicode 字符,尤其适合多字节字符和逐字符遍历操作。
  • 转换技巧[]runestring 的相互转换,是处理字符和文本的桥梁。
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
package main

import (
"fmt"
"unicode/utf8"
)

func main() {

const s = "สวัสดี"

fmt.Println("Len:", len(s))

for i := 0; i < len(s); i++ {
fmt.Printf("%x ", s[i])
}
fmt.Println()

fmt.Println("Rune count:", utf8.RuneCountInString(s))

for idx, runeValue := range s {
fmt.Printf("%#U starts at %d\n", runeValue, idx)
}

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")
}
}
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

结构体

1. 定义结构体

结构体使用 type 关键字定义,格式如下:

1
2
3
4
5
type StructName struct {
Field1 Type1
Field2 Type2
...
}

示例:

1
2
3
4
type Person struct {
Name string
Age int
}

2. 创建结构体实例

创建结构体实例的方式有以下几种:

2.1 使用结构体字面量

1
2
p := Person{"Alice", 30}
fmt.Println(p)

2.2 使用字段名初始化(推荐)

1
2
p := Person{Name: "Bob", Age: 25}
fmt.Println(p)

2.3 创建空结构体并赋值

1
2
3
4
var p Person
p.Name = "Charlie"
p.Age = 35
fmt.Println(p)

2.4 使用 new 创建结构体指针

1
2
3
4
p := new(Person)
p.Name = "Diana"
p.Age = 40
fmt.Println(*p)

3. 结构体的零值

结构体的零值是其所有字段的零值,例如:

1
2
3
4
5
6
type Example struct {
IntField int
StringField string
}
var e Example
fmt.Println(e) // 输出:{0 ""}

4. 结构体字段的访问与修改

结构体字段可以通过点操作符(.)访问和修改:

1
2
3
4
p := Person{Name: "Eve", Age: 28}
fmt.Println(p.Name) // 访问字段
p.Age = 29 // 修改字段
fmt.Println(p.Age)

5. 结构体的比较

两个结构体实例可以直接比较,但前提是它们的所有字段都支持比较操作。

1
2
3
p1 := Person{Name: "Alice", Age: 30}
p2 := Person{Name: "Alice", Age: 30}
fmt.Println(p1 == p2) // 输出:true

6. 匿名字段

结构体支持匿名字段(嵌套结构体),可以通过类型名直接使用:

1
2
3
4
5
6
7
8
9
10
11
12
type Address struct {
City, State string
}

type User struct {
Name string
Age int
Address // 匿名字段
}

u := User{Name: "Frank", Age: 32, Address: Address{City: "New York", State: "NY"}}
fmt.Println(u.City) // 直接访问匿名字段的成员

7. 结构体的方法

Go 中的方法是绑定到特定类型(包括结构体)的函数。

7.1 定义方法

方法定义时,必须有一个接收者(receiver)。接收者可以是值类型或指针类型。

1
2
3
func (p Person) SayHello() {
fmt.Printf("Hello, my name is %s\n", p.Name)
}

7.2 调用方法

1
2
p := Person{Name: "Grace", Age: 24}
p.SayHello() // 调用方法

7.3 值接收者 vs 指针接收者

  • 值接收者:方法操作的是结构体的副本,不会修改原始数据。
  • 指针接收者:方法操作的是结构体的指针,可以修改原始数据。
示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
func (p Person) ChangeName(newName string) {
p.Name = newName // 只修改副本,不影响原始数据
}

func (p *Person) UpdateName(newName string) {
p.Name = newName // 修改原始数据
}

p := Person{Name: "Hank", Age: 26}
p.ChangeName("Ivan")
fmt.Println(p.Name) // 输出:Hank

p.UpdateName("Jack")
fmt.Println(p.Name) // 输出:Jack

8. 嵌套结构体

结构体可以嵌套另一个结构体以实现更复杂的数据结构:

1
2
3
4
5
6
7
type Company struct {
Name string
Address Address
}

c := Company{Name: "Tech Corp", Address: Address{City: "San Francisco", State: "CA"}}
fmt.Println(c.Address.City)

9. JSON 与结构体

结构体可以与 JSON 数据相互转换,需使用 encoding/json 包。

9.1 转换为 JSON

1
2
3
4
5
import "encoding/json"

p := Person{Name: "Lily", Age: 22}
jsonData, _ := json.Marshal(p)
fmt.Println(string(jsonData))

9.2 从 JSON 转换为结构体

1
2
3
4
jsonStr := `{"Name": "Mike", "Age": 29}`
var p Person
_ = json.Unmarshal([]byte(jsonStr), &p)
fmt.Println(p)

9.3 使用标签自定义 JSON 字段

1
2
3
4
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
}

10. 结构体的内存布局

结构体的内存分配是连续的,字段的顺序会影响内存对齐和大小。

优化内存对齐

1
2
3
4
5
6
7
8
9
10
11
type Optimized struct {
A int64
B int32
C int16
}

type NonOptimized struct {
B int32
C int16
A int64
}

Optimized 的字段顺序可以减少内存填充,提高内存使用效率。

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

import "fmt"

type person struct {
name string
age int
}

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)
}
1
2
3
4
5
6
7
8
9
$ go run structs.go
{Bob 20}
{Alice 30}
{Fred 0}
&{Ann 40}
&{Jon 42}
Sean
50
51

方法

在Go语言中,结构体(struct)是一种用户定义的数据类型,它由一组字段(fields)组成,字段可以是不同类型的数据。结构体的方法(methods)是与结构体相关的函数,它们能够操作结构体中的数据。

1. 定义结构体

首先,我们需要定义一个结构体。结构体的定义使用关键字 type 来创建,格式如下:

1
2
3
4
5
type StructName struct {
Field1 Type1
Field2 Type2
// ... 更多字段
}

2. 结构体的方法

结构体方法是与结构体类型相关联的函数。Go 语言通过“方法接收器”(method receiver)来将方法与结构体关联。方法接收器可以是结构体类型的指针或值。

方法接收器

  • 值接收器:方法接收器是结构体的值拷贝。
  • 指针接收器:方法接收器是结构体的指针,可以修改结构体的内容。

3. 定义结构体的方法

3.1 使用值接收器

值接收器的好处是可以避免修改原始结构体,适合那些不需要修改数据的方法。

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

import "fmt"

// 定义结构体
type Person struct {
Name string
Age int
}

// 定义一个方法,使用值接收器
func (p Person) Greet() {
fmt.Println("Hello, my name is", p.Name)
}

func main() {
p := Person{Name: "Alice", Age: 30}
p.Greet() // 调用结构体方法
}

3.2 使用指针接收器

指针接收器的好处是可以直接修改结构体的内容,适合需要修改结构体数据的方法。

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

import "fmt"

// 定义结构体
type Person struct {
Name string
Age int
}

// 定义一个方法,使用指针接收器
func (p *Person) CelebrateBirthday() {
p.Age++ // 修改结构体的字段
fmt.Println("Happy Birthday, I am now", p.Age, "years old!")
}

func main() {
p := Person{Name: "Alice", Age: 30}
p.CelebrateBirthday() // 调用结构体方法
fmt.Println("I am now", p.Age, "years old.") // 修改后的值
}

4. 值接收器与指针接收器的区别

  • 值接收器

    :方法会接收结构体的副本,修改副本不会影响原始结构体。

    • 适用于方法不需要修改结构体字段的情况。
    • 如果结构体较小,传递副本开销不大。
  • 指针接收器

    :方法会接收结构体的指针,方法内部修改会影响到原始结构体。

    • 适用于方法需要修改结构体字段的情况。
    • 如果结构体较大,使用指针可以避免拷贝开销。

5. 示例:结构体和方法结合的复杂应用

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

import "fmt"

// 定义一个结构体
type Rectangle struct {
Width, Height float64
}

// 使用值接收器计算矩形的面积
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}

// 使用指针接收器改变矩形的大小
func (r *Rectangle) Scale(factor float64) {
r.Width *= factor
r.Height *= factor
}

func main() {
rect := Rectangle{Width: 3, Height: 4}

// 调用值接收器方法,计算面积
fmt.Println("Area of rectangle:", rect.Area()) // 输出: Area of rectangle: 12

// 调用指针接收器方法,改变矩形大小
rect.Scale(2)

// 输出更新后的矩形
fmt.Println("New dimensions:", rect.Width, "x", rect.Height) // 输出: New dimensions: 6 x 8
}

6. 方法重载与接口

Go语言不像C++或Java那样支持方法重载(即相同名字的多个方法),但可以通过接口来实现类似的效果。通过定义接口类型,可以让不同类型(即使它们有相同的方法签名)在同一方法中调用。

7. 总结

  • 在Go语言中,结构体的方法可以通过值接收器和指针接收器来定义。
  • 值接收器不会修改结构体本身,而指针接收器可以修改结构体。
  • 通常推荐使用指针接收器,特别是在方法可能会修改结构体时,或者结构体较大时,以避免不必要的拷贝。

接口

Go 语言中的接口是一个非常重要的概念,它定义了一组方法的集合,但是并不包含方法的具体实现。接口类型非常灵活,Go 语言的接口使用起来比许多其他编程语言中的接口要简单和直观。下面详细讲解一下 Go 语言接口的各个方面。

1. 接口定义

Go 语言的接口是通过 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
package main

import "fmt"

// 定义一个接口
type Speaker interface {
Speak() // 接口方法
}

type Person struct {
Name string
}

// Person 类型实现了 Speak 方法
func (p Person) Speak() {
fmt.Println("Hello, my name is", p.Name)
}

func main() {
var s Speaker
p := Person{Name: "John"}

// s 被赋值为 Person 类型的实例,Person 实现了 Speak 方法
s = p

s.Speak() // 调用接口方法
}

在这个例子中:

  • Speaker 是一个接口,包含一个 Speak() 方法。
  • Person 类型实现了 Speak() 方法,所以 Person 就实现了 Speaker 接口。

2. 隐式实现

Go 的接口与其他语言不同,不需要显式地声明某个类型实现了某个接口。只要某个类型实现了接口中的所有方法,这个类型就自动实现了这个接口。也就是说,你不需要写类似于 type Person implements Speaker 这样的代码。

1
2
3
4
5
6
7
8
9
10
11
12
type Dog struct{}

func (d Dog) Speak() {
fmt.Println("Woof!")
}

func main() {
var s Speaker
d := Dog{}
s = d // Dog 类型也隐式实现了 Speaker 接口
s.Speak()
}

这里,Dog 类型同样实现了 Speak() 方法,所以它也隐式地实现了 Speaker 接口。

3. 空接口 (interface{})

空接口是 Go 中非常特殊的接口类型。空接口没有任何方法,因此所有的类型都实现了空接口。空接口可以用来表示任何类型的数据。

1
2
3
4
5
6
7
8
9
func Print(v interface{}) {
fmt.Println(v)
}

func main() {
Print(42) // 打印 int
Print("Hello") // 打印 string
Print([]int{1, 2, 3}) // 打印切片
}

4. 接口的类型断言

Go 语言提供了类型断言功能,用于从接口类型转换回具体的类型。类型断言可以检查接口的实际类型,并根据检查结果进行相应的处理。

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

import "fmt"

func Describe(i interface{}) {
switch v := i.(type) { // 类型断言
case int:
fmt.Println("It's an int:", v)
case string:
fmt.Println("It's a string:", v)
default:
fmt.Println("Unknown type")
}
}

func main() {
Describe(42) // It's an int: 42
Describe("hello") // It's a string: hello
Describe(3.14) // Unknown type
}

在上面的例子中,我们使用了 i.(type) 来检查接口 i 的实际类型。switch 语句允许我们根据不同的类型执行不同的代码。

5. 接口的值

接口类型的变量实际上包含了两个信息:

  • 一个指向具体数据的指针
  • 一个指向该数据实现方法的表(即方法集)

如果你给接口赋值一个具体的类型实例,那么接口变量就包含了该类型实例的值和该类型的方法集。

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

import "fmt"

type Animal interface {
Speak() string
}

type Dog struct{}

func (d Dog) Speak() string {
return "Woof"
}

type Cat struct{}

func (c Cat) Speak() string {
return "Meow"
}

func main() {
var animal Animal

animal = Dog{}
fmt.Println(animal.Speak()) // 输出 Woof

animal = Cat{}
fmt.Println(animal.Speak()) // 输出 Meow
}

6. 空接口与反射

空接口 interface{} 通常用于接受任何类型的值,结合反射包 reflect,可以对空接口中的值进行进一步的操作和检查。反射可以用来动态地获取类型信息并执行操作。

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

import (
"fmt"
"reflect"
)

func main() {
var x interface{}
x = 42

// 使用反射获取类型信息
t := reflect.TypeOf(x)
fmt.Println("Type:", t) // Type: int

v := reflect.ValueOf(x)
fmt.Println("Value:", v) // Value: 42
}

7. 接口的组合

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

import "fmt"

type Animal interface {
Speak() string
}

type Walker interface {
Walk() string
}

type Dog struct{}

func (d Dog) Speak() string {
return "Woof"
}

func (d Dog) Walk() string {
return "Walking on four legs"
}

type DogWalker interface {
Animal
Walker
}

func main() {
var dog DogWalker = Dog{}
fmt.Println(dog.Speak()) // 输出 Woof
fmt.Println(dog.Walk()) // 输出 Walking on four legs
}

8. 接口的使用场景

接口通常用于以下几种场景:

  • 多态性:不同类型实现相同接口,允许在不同类型之间进行操作,而不关心具体类型。
  • 解耦:接口使得类型间的关系更加松散,允许通过接口而不是具体实现来设计和测试代码。
  • 扩展性:通过定义接口,可以为现有系统添加新的功能而不修改已有代码。

总结

Go 语言的接口非常灵活,支持隐式实现、多态、组合等特性,可以使代码更加简洁、可扩展。通过空接口,我们能够处理不同类型的数据,结合反射等技术,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"
"math"
)

type geometry interface {
area() float64
perim() float64
}

type rect struct {
width, height float64
}
type circle struct {
radius float64
}

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)
}
1
2
3
4
5
6
7
$ go run interfaces.go
{3 4}
12
14
{5}
78.53981633974483
31.41592653589793

Embedding

Go 语言中的 Embedding(嵌入)是指在一个类型中嵌入另一个类型,常常用于实现代码复用、继承和接口组合等功能。Go 语言没有传统的类继承机制,而是通过嵌入其他类型(通常是结构体或接口)来实现类似的功能。嵌入使得一个类型能够访问另一个类型的方法和字段,但它并不会强制要求显示继承关系,这使得 Go 在设计上更简洁和灵活。

1. 结构体嵌入

结构体嵌入是 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
package main

import "fmt"

// 定义一个结构体
type Address struct {
City, State string
}

// 定义另一个结构体,嵌入 Address 结构体
type Person struct {
Name string
Age int
Address // Person 结构体嵌入 Address
}

func main() {
// 创建一个 Person 实例
p := Person{
Name: "John",
Age: 30,
Address: Address{
City: "New York",
State: "NY",
},
}

// 直接访问嵌入的 Address 字段
fmt.Println(p.Name) // John
fmt.Println(p.Age) // 30
fmt.Println(p.City) // New York
fmt.Println(p.State) // NY
}

在上面的例子中,Person 结构体嵌入了 Address 结构体,因此 Person 类型拥有了 Address 类型的所有字段(CityState)。可以直接通过 p.Cityp.State 访问嵌入的字段。

2. 方法的继承

嵌入不仅仅是让一个结构体继承另一个结构体的字段,还可以让一个结构体继承另一个结构体的方法。被嵌入的结构体的方法可以被外部结构体直接调用。

示例:方法继承

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

import "fmt"

// 定义一个结构体
type Address struct {
City, State string
}

// 为 Address 定义一个方法
func (a Address) FullAddress() string {
return a.City + ", " + a.State
}

// 定义另一个结构体,嵌入 Address 结构体
type Person struct {
Name string
Age int
Address // 嵌入 Address
}

func main() {
// 创建一个 Person 实例
p := Person{
Name: "John",
Age: 30,
Address: Address{
City: "New York",
State: "NY",
},
}

// 直接调用嵌入的 Address 的方法
fmt.Println(p.FullAddress()) // New York, NY
}

在这个例子中,Person 结构体继承了 Address 的方法 FullAddress(),因此你可以通过 p.FullAddress() 来调用 Address 结构体的方法。嵌入的结构体的所有方法都会自动被嵌入到外部结构体中,无需显式继承。

3. 方法重写

在 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
package main

import "fmt"

// 定义一个结构体
type Animal struct{}

func (a Animal) Speak() {
fmt.Println("Animal speaks")
}

// 定义另一个结构体,嵌入 Animal 结构体
type Dog struct {
Animal // 嵌入 Animal
}

func (d Dog) Speak() {
fmt.Println("Woof!")
}

func main() {
d := Dog{}
d.Speak() // Woof!
}

在这个例子中,Dog 结构体嵌入了 Animal 结构体,并且重写了 Speak() 方法。因此,调用 d.Speak() 时,输出的是 Woof! 而不是 Animal speaks

4. 接口的嵌入

接口类型也可以嵌套其他接口,从而形成更复杂的接口组合。通过接口嵌入,你可以组合多个接口的行为,形成更强大的抽象。

示例:接口嵌入

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

import "fmt"

type Speaker interface {
Speak() string
}

type Eater interface {
Eat() string
}

type Person struct {
Name string
}

// Person 实现了 Speaker 接口
func (p Person) Speak() string {
return "Hello!"
}

// Person 实现了 Eater 接口
func (p Person) Eat() string {
return "Eating..."
}

// 定义一个新的接口,组合 Speaker 和 Eater 接口
type Human interface {
Speaker
Eater
}

func main() {
p := Person{Name: "John"}

// 由于 Person 实现了 Speaker 和 Eater 接口,所以它也实现了 Human 接口
var h Human = p

fmt.Println(h.Speak()) // Hello!
fmt.Println(h.Eat()) // Eating...
}

在这个例子中,Human 接口嵌入了 SpeakerEater 接口,因此任何实现了这两个接口的方法的类型(如 Person)也自动实现了 Human 接口。这样,你可以组合多个接口,从而使得类型具有更多的行为。

5. 匿名字段

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 Address struct {
City, State string
}

// 定义另一个结构体,匿名嵌入 Address
type Person struct {
Name string
Age int
Address // 匿名嵌入
}

func main() {
p := Person{
Name: "John",
Age: 30,
Address: Address{
City: "New York",
State: "NY",
},
}

// 通过匿名嵌入字段直接访问
fmt.Println(p.Name) // John
fmt.Println(p.City) // New York
fmt.Println(p.State) // NY
}

在这个例子中,Address 作为匿名字段直接嵌入了 Person 结构体。Person 结构体可以直接访问 Address 的字段 CityState

6. 嵌入的使用场景

  • 代码复用:通过嵌入类型,可以在不继承的情况下重用另一个类型的方法和字段。
  • 实现接口:通过嵌入结构体或接口,可以实现多个接口,增强类型的灵活性。
  • 接口组合:通过嵌套多个接口,能够实现更复杂的接口设计。
  • 简化结构体设计:匿名字段和嵌入使得结构体设计更简洁,代码更易于维护。

总结

Go 语言的嵌入(Embedding)是实现代码复用、模拟继承以及接口组合的重要手段。通过嵌入,Go 不仅避免了传统继承模型中的复杂性,同时又能够灵活地组合和扩展现有类型的功能。嵌入可以显著提高代码的简洁性和可维护性,是 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 base struct {
num int
}

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

type container struct {
base
str string
}

func main() {

co := container{
base: base{
num: 1,
},
str: "some name",
}

fmt.Printf("co={num: %v, str: %v}\n", co.num, co.str)

fmt.Println("also num:", co.base.num)

fmt.Println("describe:", co.describe())

type describer interface {
describe() string
}

var d describer = co
fmt.Println("describer:", d.describe())
}
1
2
3
4
5
$ go run embedding.go
co={num: 1, str: some name}
also num: 1
describe: base with num=1
describer: base with num=1

泛型

Go语言的泛型(Generics)是在 Go 1.18 版本引入的一个重要特性,允许你编写更具通用性和复用性的代码。在没有泛型之前,Go语言的类型系统要求程序员在许多情况下编写重复的代码,而泛型使得函数、数据结构和算法能够适应不同类型,而无需手动复制代码。

下面我会简略而全面地讲解 Go 语言泛型的核心知识点。

1. 泛型基础

泛型的核心思想是让函数、类型或数据结构在编写时不指定特定的类型,而是在调用时由用户指定类型。Go通过类型参数(type parameters)来实现这一点。

在Go语言中,泛型的语法是通过 type 关键字来声明类型参数。

2. 定义泛型函数

我们可以定义一个泛型函数,接受一个类型参数,并在函数体内根据类型参数进行操作。

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

import "fmt"

// 定义一个泛型函数,T表示类型参数
func Print[T any](value T) {
fmt.Println(value)
}

func main() {
Print(42) // 输出: 42
Print("Hello") // 输出: Hello
}

在这个例子中,T 是一个类型参数,它可以是任何类型(由 any 关键字表示,anyinterface{} 的别名)。Print 函数接收任何类型的值并打印它。

3. 泛型类型约束

Go语言的泛型允许为类型参数添加约束,确保类型参数符合某些条件。通过接口来实现约束。例如,我们可以要求类型参数必须是数字类型。

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

import "fmt"

// 定义一个只接受数字类型的泛型函数
func Add[T int | int64 | float64](a, b T) T {
return a + b
}

func main() {
fmt.Println(Add(1, 2)) // 输出: 3
fmt.Println(Add(1.5, 2.5)) // 输出: 4
fmt.Println(Add(int64(10), int64(20))) // 输出: 30
}

在这个例子中,Add 函数的类型参数 T 被约束为 intint64float64 之一。

4. 泛型数据结构

泛型不仅可以用于函数,还可以用于数据结构(如数组、切片、映射等)。通过泛型数据结构,我们可以避免为每种类型编写重复的代码。

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

import "fmt"

// 定义一个泛型栈(Stack)类型
type Stack[T any] struct {
items []T
}

func (s *Stack[T]) Push(item T) {
s.items = append(s.items, item)
}

func (s *Stack[T]) Pop() T {
if len(s.items) == 0 {
var zero T
return zero
}
item := s.items[len(s.items)-1]
s.items = s.items[:len(s.items)-1]
return item
}

func main() {
// 操作 int 类型的栈
intStack := Stack[int]{}
intStack.Push(1)
intStack.Push(2)
fmt.Println(intStack.Pop()) // 输出: 2

// 操作 string 类型的栈
stringStack := Stack[string]{}
stringStack.Push("hello")
stringStack.Push("world")
fmt.Println(stringStack.Pop()) // 输出: world
}

5. anyinterface{}

在 Go 泛型中,any 是一种通用类型,表示任何类型,它是 interface{} 的别名。在 Go 1.18 版本及之后,你可以用 any 来表示可以接受任何类型的类型参数。

1
2
3
4
5
6
7
8
// 两者是等价的
func Print[T any](value T) {
fmt.Println(value)
}

func Print[T interface{}](value T) {
fmt.Println(value)
}

6. 泛型约束类型的组合

Go 支持组合多个约束,可以将多个接口约束组合在一起,从而使得类型参数更为灵活。

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

import "fmt"

// 定义一个接受可比较类型的泛型函数
type Comparable interface {
int | float64 | string
}

func Max[T Comparable](a, b T) T {
if a > b {
return a
}
return b
}

func main() {
fmt.Println(Max(1, 2)) // 输出: 2
fmt.Println(Max(1.5, 2.5)) // 输出: 2.5
fmt.Println(Max("apple", "banana")) // 输出: banana
}

7. 类型推断

在调用泛型函数时,Go会自动推断类型参数。如果没有显式指定类型,Go会根据传入的参数类型推断出类型。

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

import "fmt"

func Print[T any](value T) {
fmt.Println(value)
}

func main() {
Print(10) // Go 推断 T 为 int
Print("Hello") // Go 推断 T 为 string
}

8. 使用泛型接口

你也可以为接口定义泛型,使得接口更加灵活和可重用。

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 Stringer[T any] interface {
String() string
}

type Person struct {
Name string
}

func (p Person) String() string {
return "Name: " + p.Name
}

func PrintString[T Stringer[T]](s T) {
fmt.Println(s.String())
}

func main() {
p := Person{Name: "John"}
PrintString(p) // 输出: Name: John
}

9. 总结

Go 语言的泛型使得编写更加通用和可重用的代码成为可能,它通过类型参数和约束来实现这一点。泛型能够提高代码的灵活性和可维护性,减少代码重复,但同时也需要保持类型的安全性。

泛型的核心语法和概念包括:

  • 使用 type 定义类型参数。
  • 使用 anyinterface{} 来表示可以接受任何类型。
  • 通过类型约束限制类型参数的范围。
  • 使用泛型数据结构(如栈、队列等)来实现通用的数据结构。

你可以使用泛型来优化你的代码,避免编写大量的重复逻辑,特别是在需要处理多种类型时。