开始学习go之旅,带你快速了解go语言[超级详细讲解,持续更新]
package main
import "fmt"
func main() {
fmt.Println("hello world")
}
要运行这个程序,先将将代码放到名为 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
一.变量
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)
}
$ go run variables.go
initial
1 2
true
0
short
二.常量
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))
}
$ go run constant.go
constant
6e+11
600000000000
-0.28470407323754404
三.循环
for
是 Go 中唯一的循环结构。这里会展示for
循环的一些基本使用方式。
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)
}
}
$ go run for.go
1
2
3
7
8
9
loop
1
3
5
四.if/else分支
注意,在 Go 中,条件语句的圆括号不是必需的,但是花括号是必需的。
Go 没有三目运算符, 即使是基本的条件判断,依然需要使用完整的
if
语句。
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")
}
}
$ go run if-else.go
7 is odd
8 is divisible by 4
9 has 1 digit
五.switch分支
switch 是多分支情况时快捷的条件语句。
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")
}
$ 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 个整数的数组可以这样定义:
var arr [5]int // 定义一个长度为 5 的整数数组
也可以在定义时直接初始化数组的元素:
var arr = [3]int{1, 2, 3} // 定义并初始化数组
或者 Go 支持根据初始化值自动推导数组的长度:
var arr = [...]int{1, 2, 3} // 自动推导长度为 3
2. 数组的访问和修改
数组的元素通过索引访问,索引从 0 开始。可以通过下标访问和修改数组的元素:
arr := [3]int{1, 2, 3}
fmt.Println(arr[0]) // 输出 1
arr[1] = 10 // 修改第二个元素
fmt.Println(arr[1]) // 输出 10
3. 数组的长度
数组的长度是固定的,可以使用 len()
函数获取数组的长度:
arr := [3]int{1, 2, 3}
fmt.Println(len(arr)) // 输出 3
4. 数组的传递
在 Go 中,数组是值类型,当你将一个数组作为参数传递给函数时,实际上是传递了该数组的副本。如果你修改了副本中的元素,原始数组不会受到影响。
func modifyArray(arr [3]int) {
arr[0] = 100
}
arr := [3]int{1, 2, 3}
modifyArray(arr)
fmt.Println(arr) // 输出 [1, 2, 3],原始数组没有被修改
如果你希望修改原数组的内容,可以传递指向数组的指针:
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 列的数组:
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 2 3] [4 5 6]]
6. 数组的初始化
数组可以在声明时进行初始化,也可以在定义之后进行逐一赋值。以下是几种初始化数组的方式:
-
声明并初始化:
arr := [3]int{1, 2, 3} // 定义并初始化数组
-
使用
...
自动推导数组长度:arr := [...]int{1, 2, 3} // 自动推导数组长度为 3
-
使用
new
函数创建一个指向数组的指针:arr := new([3]int) // 创建一个指向长度为 3 的数组的指针 arr[0] = 10 fmt.Println(arr) // 输出 &[10 0 0]
7. 数组与切片的区别
数组和切片的区别是一个重要的概念,尤其是 Go
语言的切片(Slice)非常常用。切片是对数组的一个抽象,允许灵活的动态扩展和缩减,而数组的大小是固定的。
- 数组:长度固定,类型和值传递。
- 切片:动态大小,引用传递,可以灵活扩展。
示例,使用切片:
arr := [3]int{1, 2, 3} // 数组
slice := arr[:] // 切片
fmt.Println(slice) // 输出 [1 2 3]
8. 数组的应用场景
数组在 Go 中通常用于以下几种情况:
- 当需要固定大小的数据集合时。
- 需要用到固定大小的内存时(例如嵌入式系统或性能优化的场景)。
- 数组的值传递方式可以避免不必要的内存共享,确保数据不会被意外修改。
总结
Go 语言中的数组是固定长度的,定义时需要指定数组的长度。
- 数组通过下标访问和修改元素,且下标从 0 开始。
- Go 中的数组是值类型,传递数组时会复制副本。如果希望修改原始数组,需要传递指针。
- Go 支持多维数组,可以使用嵌套方式定义。
- 数组与切片不同,切片更灵活且常用于实际编程中。
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)
}
$ 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
函数:slice := make([]int, 5) // 创建一个长度为5的切片
你还可以指定切片的容量:
slice := make([]int, 5, 10) // 创建一个长度为5,容量为10的切片
-
直接使用数组字面量:
slice := []int{1, 2, 3, 4, 5} // 创建一个包含5个元素的切片
-
2. 切片的属性
每个切片都包含三个基本属性:
- 指针(Pointer): 指向底层数组的指针。
- 长度(Length): 切片中元素的数量。
- 容量(Capacity): 切片底层数组的大小,从切片的起始位置到底层数组的末尾。
你可以通过
len()
和cap()
函数获取切片的长度和容量:
fmt.Println(len(slice)) // 输出切片的长度
fmt.Println(cap(slice)) // 输出切片的容量
3. 切片的切割操作
-
切片可以从另一个切片或数组中通过索引和范围来切割:
slice := []int{1, 2, 3, 4, 5} subSlice := slice[1:4] // 获取从索引1到索引3的元素 fmt.Println(subSlice) // 输出: [2 3 4]
你还可以指定切片的容量:
subSlice := slice[1:4:5] // 切片从索引1到索引3,容量为5 fmt.Println(subSlice) // 输出: [2 3 4]
4. 动态扩展切片
-
使用
append
扩展切片:slice := []int{1, 2, 3} slice = append(slice, 4, 5) // 扩展切片 fmt.Println(slice) // 输出: [1 2 3 4 5]
append
函数用于向切片中添加元素,并且如果需要,它会自动调整切片的容量。
5. 删除切片元素
-
删除指定索引的元素:
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. 切片的反转
-
反转切片元素:
for i := 0; i < len(slice)/2; i++ { slice[i], slice[len(slice)-1-i] = slice[len(slice)-1-i], slice[i] }
7. 切片的查找
-
查找指定元素:
found := false for _, v := range slice { if v == target { found = true break } }
8. 切片的分块
-
将切片分割成多个小块:
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. 切片排序
-
对切片进行排序:
import "sort" sort.Ints(slice) // 对整数切片进行升序排序
10. 删除重复元素
-
去除切片中的重复元素:
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
函数
-
复制切片内容:
copy(dst, src) // 将 src 的内容复制到 dst
总结
Go 切片提供了高效且灵活的操作方式,以下是常见的切片操作总结:
- 创建:通过
make
或字面量创建。- 切割:使用切片操作符可以截取切片的部分。
- 扩展:通过
append
函数动态扩展切片。- 删除:通过
append
删除切片中的元素。- 反转:通过交换元素实现反转。
- 查找:通过遍历切片查找元素。
- 分块:将切片分割成多个小块。
- 排序:使用标准库中的
sort
对切片进行排序。- 去重:通过
map
去除切片中的重复元素。- 复制:使用
copy
将一个切片的内容复制到另一个切片中。
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)
}
$ 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
函数
m := make(map[string]int)
上面的代码创建了一个空的 map
,键为 string
类型,值为 int
类型。
使用字面量(literal)创建
m := map[string]int{
"apple": 5,
"banana": 3,
}
上面的代码创建了一个初始包含两个键值对的 map
。
2. 向 map
中添加和更新元素
你可以直接通过键来添加或更新值:
m["apple"] = 10 // 更新值
m["orange"] = 7 // 添加新元素
3. 获取 map
中的值
通过键来访问对应的值:
value := m["apple"]
fmt.Println(value) // 输出 10
4. 删除 map
中的元素
使用 delete
函数来删除指定键的元素:
delete(m, "banana") // 删除键为"banana"的元素
5. 检查键是否存在
通过多返回值的方式来判断键是否存在。如果键存在,第二个返回值为 true
,否则为 false
。
value, ok := m["apple"]
if ok {
fmt.Println("Found:", value)
} else {
fmt.Println("Not found")
}
6. 遍历 map
使用 for
循环来遍历 map
的所有键值对:
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
中元素的数量:
fmt.Println(len(m)) // 输出 map 中的元素个数
9. 空 map
和 nil
map
- 如果
map
没有被初始化(nil
),则它的行为类似于空map
,但是尝试向nil
map
中添加或删除元素会引发运行时错误。- 可以使用
make
函数初始化一个空map
。
示例代码:
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))
}
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)
}
$ 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
遍历数组和切片
数组
arr := [3]int{1, 2, 3}
for index, value := range arr {
fmt.Println(index, value)
}
在遍历数组时,
range
返回两个值:
index
:当前元素的索引。value
:当前元素的值。
切片
slice := []string{"apple", "banana", "cherry"}
for index, value := range slice {
fmt.Println(index, value)
}
对于切片,
range
也返回两个值:
index
:当前元素的索引。value
:当前元素的值。
2. range
遍历 map
在 map
中,range
返回的是键值对:
m := map[string]int{"apple": 5, "banana": 3}
for key, value := range m {
fmt.Println(key, value)
}
对于
map
,range
返回两个值:
key
:当前元素的键。value
:当前元素的值。
注意事项:
map
的遍历顺序是无序的,每次遍历的顺序可能不同。
3. range
遍历字符串
range
也可以用于遍历字符串。它会逐个获取字符串中的 Unicode 字符,而不是按字节遍历:
str := "hello"
for index, runeValue := range str {
fmt.Println(index, runeValue)
}
在遍历字符串时,
range
返回两个值:
index
:当前字符的索引。runeValue
:当前字符的 Unicode 码点值(rune
类型)。
4. 只获取值或索引
你可以通过 _
忽略 range
返回的某些值。
- 只获取索引:
for index := range arr {
fmt.Println(index)
}
- 只获取值:
for _, value := range arr {
fmt.Println(value)
}
5. range
遍历通道(channel)
range
也可以用来遍历通道(channel)中的元素。当通道关闭时,range
循环会停止。
ch := make(chan int, 3)
ch <- 1
ch <- 2
ch <- 3
close(ch)
for value := range ch {
fmt.Println(value)
}
当通道关闭后,range
会自动遍历通道中的所有元素。
6. 示例:综合使用 range
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
是一个高效且灵活的工具,能够简化代码并提高可读性。
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)
}
}
$ 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
关键字来定义函数。
基本函数定义
func add(a int, b int) int {
return a + b
}
这个函数的定义包含了:
func
关键字,表示定义一个函数。add
,函数的名称。(a int, b int)
,是函数的参数列表,表示这个函数接收两个int
类型的参数。int
,是返回值的类型,表示该函数返回一个int
类型的结果。
2. 调用函数
函数定义后,可以通过函数名调用它,并传递参数:
result := add(3, 4)
fmt.Println(result) // 输出 7
3. 函数参数
- 单一参数类型:可以在函数参数中使用相同类型的多个参数,简化代码。
func add(a, b int) int {
return a + b
}
- 可变参数:Go 支持传递可变数量的参数,使用
...
来表示可变参数。
func sum(numbers ...int) int {
total := 0
for _, number := range numbers {
total += number
}
return total
}
调用:
fmt.Println(sum(1, 2, 3, 4)) // 输出 10
4. 返回值
Go 函数可以返回多个值:
func swap(a, b int) (int, int) {
return b, a
}
调用:
x, y := swap(1, 2)
fmt.Println(x, y) // 输出 2 1
5. 命名返回值
Go 允许给返回值命名,返回值就像局部变量一样,可以在函数体内直接使用。这也简化了代码,使得不需要显式地使用
return
语句。
func add(a, b int) (sum int) {
sum = a + b
return // 使用命名返回值
}
6. 函数作为值
Go 允许函数作为值传递。这意味着可以将函数赋值给变量或作为参数传递给其他函数。
将函数赋值给变量
func multiply(a, b int) int {
return a * b
}
var f func(int, int) int = multiply
fmt.Println(f(2, 3)) // 输出 6
函数作为参数
func operate(a, b int, op func(int, int) int) int {
return op(a, b)
}
fmt.Println(operate(2, 3, multiply)) // 输出 6
7. 匿名函数
Go 支持匿名函数,即没有函数名的函数。匿名函数常常作为回调函数或临时使用。
func() {
fmt.Println("Hello from anonymous function!")
}()
8. 函数闭包
Go 支持闭包,闭包是一个函数,它可以“记住”并访问其外部作用域中的变量。即使外部函数返回,闭包仍然可以访问这些变量。
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 支持函数的递归调用,即一个函数在其定义中调用自己。
func factorial(n int) int {
if n == 0 {
return 1
}
return n * factorial(n-1)
}
fmt.Println(factorial(5)) // 输出 120
在这个例子中,factorial
函数调用自己来计算阶乘。
10. 多返回值函数
Go 允许函数返回多个值,常用于处理错误处理等场景。
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 支持将函数作为值传递,允许匿名函数和闭包等特性。
- 函数调用时,可以直接使用返回值,也可以通过传递函数作为参数来进行更高阶的编程。
基础用法:
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)
}
$ go run functions.go
1+2 = 3
1+2+3 = 6
多返回值:
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)
}
$ go run multiple-return-values.go
3
7
7
变参函数:
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...)
}
$ go run variadic-functions.go
[1 2] 3
[1 2 3] 6
[1 2 3 4] 10
闭包:
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())
}
$ go run closures.go
1
2
3
1
递归:
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))
}
$ go run recursion.go
5040
13
十一.指针
在 Go
语言中,指针是存储变量内存地址的类型,它允许程序间接访问变量的值。指针可以非常方便地用于修改函数外部的变量值,以及在内存中操作数据。Go
的指针与 C 语言类似,但它不允许进行指针算术运算,这让它更加安全。
1. 定义和声明指针
在 Go 中,指针的声明方式是使用 *
表示指向某个类型的指针,类型前加 *
表示该变量是该类型的指针。
var ptr *int
这意味着 ptr
是一个指向 int
类型的指针,但它当前没有指向任何具体的内存地址。
2. 获取变量的地址(取地址操作符 &
)
要获取一个变量的内存地址,可以使用 &
操作符,它会返回变量的地址。
x := 10
ptr := &x // ptr 是 x 的指针
fmt.Println(ptr) // 输出 x 的地址
3. 解引用指针(取值操作符 *
)
通过指针访问它所指向的变量的值,使用 *
操作符。这个过程叫做“解引用”。
x := 10
ptr := &x // 获取 x 的地址
fmt.Println(*ptr) // 输出 10,解引用指针,得到 x 的值
4. 指针与变量的关系
&
获取变量的地址。*
解引用指针,获取指针所指向的值。
示例:
x := 10
ptr := &x // 获取 x 的地址
fmt.Println(ptr) // 输出 x 的内存地址
fmt.Println(*ptr) // 输出 10,通过指针访问 x 的值
5. 修改变量的值通过指针
指针允许通过间接访问修改原变量的值。通过解引用指针可以修改它指向的变量。
x := 10
ptr := &x
*ptr = 20 // 修改 x 的值,通过 ptr 指针
fmt.Println(x) // 输出 20
6. 指针作为函数参数
在 Go 中,函数参数是值传递的。如果希望函数修改传入的变量,可以传递变量的指针。这通常用于避免复制大量数据的成本,或允许函数修改传入的参数。
func modifyValue(a *int) {
*a = 20 // 通过指针修改原始值
}
x := 10
modifyValue(&x) // 传递 x 的指针
fmt.Println(x) // 输出 20
7. 指针类型与结构体
结构体也可以使用指针,结构体指针常用于修改结构体的成员,避免复制结构体的副本。
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
来避免解引用空指针。
var ptr *int
if ptr == nil {
fmt.Println("ptr is nil")
}
9. 零值和指针
Go 中,指针的零值是 nil
,表示它没有指向任何有效的内存地址。当一个指针没有显式初始化时,它的默认值就是 nil
。
var ptr *int // ptr 默认为 nil
fmt.Println(ptr) // 输出 nil
10. 指针的应用
- 修改函数外部的变量值:通过指针参数来修改传入的变量。
- 避免大数据的复制:对于大的数据结构(如数组或结构体),可以通过指针传递,避免复制整个数据结构。
- 链表、树等数据结构:指针广泛应用于实现链表、二叉树等动态数据结构。
总结
指针是 Go 语言中的一个重要特性,它让你能够间接操作内存,并通过引用传递修改变量的值。Go 语言中的指针有如下特点:
- 不允许指针算术运算,避免了 C 语言中的很多潜在错误。
- 使用
&
获取变量的地址,使用*
解引用指针。- 可以通过指针修改变量的值,传递指针给函数可以改变函数外的变量。
指针在 Go 中的使用简单而强大,可以提高程序的效率并且节省内存空间。
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)
}
$ go run pointers.go
initial: 1
zeroval: 1
zeroptr: 0
pointer: 0x42131100
十二.字符串和rune类型
字符串(string)
1. 字符串的定义
- 不可变性:字符串是不可变的,即一旦创建后,无法直接修改其中的内容。任何修改操作都会生成一个新的字符串。
- 底层结构:字符串底层是一个只读的字节序列(
[]byte
)。- 编码:通常用来存储 UTF-8 编码的数据,可以包含 ASCII 字符、汉字、特殊符号等。
2. 字符串的常见操作
声明与初始化
- 使用双引号声明普通字符串。
- 使用反引号声明原始字符串,保留原格式,包括换行和特殊字符。
s1 := "hello, world" // 普通字符串
s2 := `hello,
world with "quotes"` // 原始字符串
获取字符串长度
- 使用
len()
获取字符串长度,返回的是字节数而非字符数。
s := "你好"
fmt.Println(len(s)) // 输出:6,因为每个汉字占3个字节
索引访问字符串
- 可以通过索引访问字符串中的字节:
s := "hello"
fmt.Println(s[0]) // 输出:104,'h' 的 ASCII 值
fmt.Printf("%c\n", s[0]) // 输出:h
- 注意:通过索引访问的是单个字节,而不是字符。
字符串拼接
- 使用
+
或fmt.Sprintf()
拼接字符串。
s1 := "hello"
s2 := "world"
s3 := s1 + ", " + s2 + "!"
fmt.Println(s3) // 输出:hello, world!
切片操作
- 支持通过切片截取子字符串,但结果是字节切片,可能导致截断多字节字符。
s := "你好世界"
sub := s[:3] // 截取前3个字节
fmt.Println(sub) // 输出:乱码(截断了“你”)
3. Go标准库中的字符串方法
Go 提供了 strings
包来处理字符串,包括查找、替换、切分等操作。
常用函数
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
:runes := []rune("你好世界") fmt.Println(runes) // [20320 22909 19990 30028]
-
将
[]rune
转为字符串:s := string([]rune{20320, 22909, 19990, 30028}) fmt.Println(s) // 你好世界
逐字符处理
-
使用
for
遍历[]rune
可以逐字符操作:s := "hello, 世界" for i, r := range []rune(s) { fmt.Printf("第%d个字符:%c (Unicode: %U)\n", i, r, r) }
修改字符串
-
字符串不可变,但可以通过
[]rune
修改后重新构造:s := "hello" runes := []rune(s) runes[0] = 'H' s = string(runes) fmt.Println(s) // Hello
统计字符数量
-
使用
len()
获取的是字节数。若要获取字符数,需要将字符串转换为[]rune
: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 访问字符 |
使用场景与注意事项
- 字符串处理:
- 如果主要处理整段文本数据,使用
string
。- 避免直接索引多字节字符,否则可能导致错误。
- 多字节字符处理:
- 如果需要逐字符操作,使用
[]rune
。- 转换为
[]rune
后,才能安全地进行字符级别的索引和修改。- 性能权衡:
- 操作
string
通常更高效,因为底层是只读的[]byte
。- 使用
[]rune
需要额外的内存,适合复杂字符处理场景。
总结
- 字符串(string):主要用于存储和处理文本,适合完整的文本操作。支持丰富的库函数,如查找、替换、切分等。
- Rune:用来处理单个 Unicode 字符,尤其适合多字节字符和逐字符遍历操作。
- 转换技巧:
[]rune
和string
的相互转换,是处理字符和文本的桥梁。
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")
}
}
$ 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
关键字定义,格式如下:
type StructName struct {
Field1 Type1
Field2 Type2
...
}
示例:
type Person struct {
Name string
Age int
}
2. 创建结构体实例
创建结构体实例的方式有以下几种:
2.1 使用结构体字面量
p := Person{"Alice", 30}
fmt.Println(p)
2.2 使用字段名初始化(推荐)
p := Person{Name: "Bob", Age: 25}
fmt.Println(p)
2.3 创建空结构体并赋值
var p Person
p.Name = "Charlie"
p.Age = 35
fmt.Println(p)
2.4 使用 new
创建结构体指针
p := new(Person)
p.Name = "Diana"
p.Age = 40
fmt.Println(*p)
3. 结构体的零值
结构体的零值是其所有字段的零值,例如:
type Example struct {
IntField int
StringField string
}
var e Example
fmt.Println(e) // 输出:{0 ""}
4. 结构体字段的访问与修改
结构体字段可以通过点操作符(.
)访问和修改:
p := Person{Name: "Eve", Age: 28}
fmt.Println(p.Name) // 访问字段
p.Age = 29 // 修改字段
fmt.Println(p.Age)
5. 结构体的比较
两个结构体实例可以直接比较,但前提是它们的所有字段都支持比较操作。
p1 := Person{Name: "Alice", Age: 30}
p2 := Person{Name: "Alice", Age: 30}
fmt.Println(p1 == p2) // 输出:true
6. 匿名字段
结构体支持匿名字段(嵌套结构体),可以通过类型名直接使用:
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)。接收者可以是值类型或指针类型。
func (p Person) SayHello() {
fmt.Printf("Hello, my name is %s\n", p.Name)
}
7.2 调用方法
p := Person{Name: "Grace", Age: 24}
p.SayHello() // 调用方法
7.3 值接收者 vs 指针接收者
- 值接收者:方法操作的是结构体的副本,不会修改原始数据。
- 指针接收者:方法操作的是结构体的指针,可以修改原始数据。
示例:
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. 嵌套结构体
结构体可以嵌套另一个结构体以实现更复杂的数据结构:
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
import "encoding/json"
p := Person{Name: "Lily", Age: 22}
jsonData, _ := json.Marshal(p)
fmt.Println(string(jsonData))
9.2 从 JSON 转换为结构体
jsonStr := `{"Name": "Mike", "Age": 29}`
var p Person
_ = json.Unmarshal([]byte(jsonStr), &p)
fmt.Println(p)
9.3 使用标签自定义 JSON 字段
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
}
10. 结构体的内存布局
结构体的内存分配是连续的,字段的顺序会影响内存对齐和大小。
优化内存对齐
type Optimized struct {
A int64
B int32
C int16
}
type NonOptimized struct {
B int32
C int16
A int64
}
Optimized
的字段顺序可以减少内存填充,提高内存使用效率。
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)
}
$ go run structs.go
{Bob 20}
{Alice 30}
{Fred 0}
&{Ann 40}
&{Jon 42}
Sean
50
51