Golang基础
计划
Golang 语言基础能力
- Golang 基础语法与语言原理
- Golang 面向对象与函数设计
- Golang 并行模型与协程机制
- Golang 实用标准困 & 实战项目
- 深入底层原理与性能优化
计算机基础能力
- Linux
- 数据结构
- 算法
- 设计模式
- 网络
开发进阶能力
- 容器化
- Kubernetes
- Docker
- etcd
- leetcode
- 链表
- 哈希表
- 树
- 动态规划
- 栈
- 排序
- 图
- 数组
- 分布式
- zookeeper
- 消息
- kafka:高吞吐量的分布式发布订阅消息系统,在 kafka 集群中也用到了 zookeeper 来实现分布式能力。kafka 在处理大量数据的场景下能够发挥巨大作用,例如收集日志,传递消息时,kafka 每秒可以处理几十万条消息,同时可以横向扩展集群能力。
数据库能力掌握
- redis:内存型键值数据库(Key-Value Store),运行于内存中,支持持久化(RDB/AOF 和 主从复制、集群模式,支持 发布/订阅(Pub/Sub)、分布式锁。使用场景:缓存、队列、限流、分布式锁、排行榜。
- mysql:关系型数据库(Relational Database,RDBMS),采用结构化表(表格),使用 SQL 语言, 支持 ACID 事务、数据约束、索引、外键,数据安全性强,广泛用于企业级系统,丰富的生态。使用场景:网站后台、电商系统、ERP/CRM。
- ElasticSearch:文档型数据库(全文搜索引擎,基于 Lucene),数据以 JSON 文档形式存储,支持全文检索,超强的搜索能力,分布式存储与查询,可横向扩展,实时索引,适合日志系统、搜索服务。使用场景:搜索引擎、日志分析、指标监控。
1、基础语法与语言原理
1.1、包、变量、常量、类型、函数、控制流
1.1.1、包(Packages)
Go 的代码必须放在包(package)中。每个 .go 文件都必须指定一个 package 名。
导入包:
import "fmt"
import (
"os"
"math"
)
实例:
package main
import "fmt"
func main() {
fmt.Println("Hello, Go Package!")
}
[!NOTE]
1、为什么 Go 的代码必须放在包(package)里?
- 为了支持模块化开发
Go 是一个专注于构建可维护、大规模项目的语言。使用包(Package)可以将相关功能代码组织起来,实现高内聚、低耦合,也方便多人协作和代码复用。
- 所有 Go 代码必须属于某个包
Go 的构建体系(
go build、go run等)是基于包来组织的,而不是文件。每个.go文件都必须属于一个明确的package,这样 Go 才知道:
- 这个文件该如何编译
- 它暴露了哪些对外接口(导出函数、类型等)
- 是否包含可执行程序的入口(
main.main())2、Go 是怎么通过包来确定“这个文件该如何编译、它暴露了哪些对外接口(导出函数、类型等)、是否包含可执行程序的入口”这些东西的?
1、Go 如何根据包确定“这个文件该如何编译”?(看
package声明,按包为单位打包)当你执行:
go buildGo 会:
- 查找当前目录下的所有
.go文件- 要求这些文件的
package声明必须一致- 把这些文件作为一个整体的包(package)进行编译
例如,一个
utils包下有:utils/ ├── string_util.go → package utils └── math_util.go → package utilsGo 会将这两个文件组合为一个
utils包来编译,不会单独编译某一个文件。2、Go 如何判断它暴露了哪些“对外接口”(导出内容)?(Go 通过 首字母大小写 来决定是否导出)
在 Go 中,没有类似 Java 的
public/private关键字。Go 的导出规则非常简单:首字母大写的标识符是导出的(Exported),对其他包可见;小写则是私有的,仅限包内访问。
// utils/math_util.go package utils func Add(a, b int) int { // 导出的函数 return a + b } func subtract(a, b int) int { // 包内部私有 return a - b }其他包只能调用
utils.Add,不能访问subtract。3、Go 如何判断“是否是可执行程序的入口”?(package main
且有func main() )Go 通过包名
main和函数main()识别入口
- 包名必须是
main- 必须有一个无参数的
func main()满足这两个条件,Go 才会把它编译成可执行文件。
示例:
package main import "fmt" func main() { fmt.Println("Hello from entry point") }一旦你写的是
package utils、package http、package yourlib之类,即使有main()函数也不会被当作入口处理。
[!IMPORTANT]
深入分析 Go 的编译流程与包结构
整体流程图:(确定编译范围、导出符号、依赖关系解析、构建入口点、缓存和链接)
源码目录结构 ↓ ┌──────────┐ │ go build │ ←─── 你执行构建命令 └──────────┘ ↓ 🔍 模块加载(go.mod) ↓ 📦 包扫描与依赖解析 ↓ 🧩 AST 构建 + 类型检查 + 符号导出 ↓ 🔗 静态链接各个包为最终产物 ↓ 🧪 生成二进制可执行程序1️⃣ Go 编译器(
go build)如何知道构建哪些文件?
- 它首先查看当前目录下所有
.go文件- 然后检查
package xxx声明是否一致- 如果一致,视为一个“包”,整体作为一个编译单元
- 所以不能在同一目录下有多个包
如果你这么写:
// file1.go package foo // file2.go package bar就会报错:
found packages foo and bar in <same directory>2️⃣ Go 如何解析 import 和依赖关系?
当你写下:
import "yourapp/utils"Go 会按照以下顺序查找:
📍 非标准包(即自定义包):
- 查看当前
go.mod中module yourapp- 将
import "yourapp/utils"解析为项目中的./utils/目录- 然后加载那个目录下所有
.go文件(必须同属一个package utils)📚 标准库(如
fmt、net/http):
- 编译器内置搜索路径
- 不需要
go.mod🔗 外部模块(如
github.com/gin-gonic/gin):
go.mod会记录依赖版本- 下载到
$GOPATH/pkg/mod/或go.sum缓存目录3️⃣ 编译器构建了什么?
每个包在构建时,Go 会:
✅ 解析语法(Parser → AST)
构建语法树(AST),进行词法和语法分析
✅ 类型检查
利用类型系统确保所有变量、接口、函数、结构体符合规则,构建类型图
✅ 生成中间代码
Go 会生成 SSA(Static Single Assignment)中间表示,用于优化和代码生成
✅ 标识导出符号
根据首字母大写规则,标记哪些是exported symbols
所有导出的函数、类型、常量都会被写入
.a静态包文件中,供其他包导入4️⃣ 包构建缓存(.a 文件)
Go 构建时会生成一个
.a文件,类似静态库,比如:$GOPATH/pkg/mod/github.com/gin-gonic/gin@v1.8.2/gin.a这个
.a文件中包含了:
- 编译后的符号信息
- 导出接口
- 类型定义和方法
- 可重用中间代码
这样即使主程序只改了一行,其他包不会重复编译,大大加快了构建速度。
5️⃣
package main特例:可执行文件的入口点在 Go 的编译器里,会特别处理
package main:
- 检查是否有
main.main()函数- 若有,则构建最终的
.exe或 Linux 可执行文件- 否则报错:
go build: no main function defined in main package这个机制确保 Go 项目只能有一个执行入口
6️⃣ init 函数的作用与执行顺序
除了
main()外,init()也会自动执行:func init() { fmt.Println("初始化资源") }init 执行顺序:
- 先执行依赖包的
init()(按照 import 顺序)- 然后执行当前包的
init()- 最后是
main.main()例如:
import ( "lib/a" "lib/b" )如果两个包都有
init(),执行顺序为:a.init() → b.init() → main.init() → main.main()🔍 例子:构建一个简单的项目结构
myapp/ ├── go.mod → module github.com/user/myapp ├── main.go → package main ├── config/ │ └── config.go → package config └── utils/ └── math.go → package utilsmain.go:
package main import ( "github.com/user/myapp/config" "github.com/user/myapp/utils" ) func main() { config.InitConfig() result := utils.Add(1, 2) fmt.Println(result) }
- Go 会从
main开始编译,依赖config和utils包- 每个子包编译后生成
.a缓存- 链接所有导出的符号后生成一个单一的可执行文件
| 命令 | 介绍 |
|---|---|
| go mod init | 初始化项目依赖,生成go.mod文件 |
| go mod download | 根据go.mod文件下载依赖 |
| go mod tidy | 比对项目文件中引入的依赖与go.mod进行比对 |
| go mod graph | 输出依赖关系图 |
| go mod edit | 编辑go.mod文件 |
| go mod vendor | 将项目的所有依赖导出至vendor目录 |
| go mod verify | 检验一个依赖包是否被篡改过 |
| go mod why | 解释为什么需要某个依赖 |
1.1.2、变量(Variables)
声明方式:
var a int = 10 // 显式声明
var b = "hello" // 自动推导类型
c := 3.14 // 简洁声明,用于 函数内部 快速声明并初始化变量,不能用于函数外部。
多变量声明:
var x, y int = 1, 2
var (
name string
age int
)
零值规则(Zero value):
未赋值变量会被赋予零值:
- 数字 →
0 - 字符串 →
"" - 布尔 →
false - 指针/引用类型 →
nil
[!NOTE]
为什么需要
var?
动作 示例 意义 声明(define) var a int创建新变量 赋值(assign) a = 10修改已有变量的值 如果没有
var,你写a = 10,Go 无法知道你是:
- 创建一个新变量?
- 还是修改一个已有的变量?
说白话,var 声明"这是一个变量"。
1.1.3、常量(Constans)
声明方式:
const Pi = 3.14
const pi float64 = 3.14159
const (
A = 1
B = 2
)
类型推导:
const c = 10 // 无具体类型,直到使用才确定
iota 用法(用于生成自增枚举值,常用于状态码、位标志等定义):
const (
a = iota // 0
b = iota // 1
c // 2,隐式为 iota
)
[!NOTE]
Go 中有一个特殊的常量生成器:
iota,常用于枚举类型的生成。const ( A = iota // 0 B // 1 C // 2 )
iota是在每个 const 声明块内从0开始,每次递增 1 的标识符补充:多个 const 块中的 iota 是独立的
[!IMPORTANT]
1、常量必须有值,也可以通过省略写法间接获得值。
批量声明中可以省略值,但不是没值,而是继承上一个值
const ( A = 100 B // 自动继承 A 的值,等价于 B = 100 C // 同样是 100 )2、常量的值是只读的,一经声明就不可再更改。
1.1.4、基本类型(Types)
| 类型 | 描述 |
|---|---|
int |
整数 |
float32/64 |
浮点数 |
string |
字符串 |
bool |
布尔值 |
interface() |
空接口,表示可以存储任何类型 |
*T |
指向类型T的指针类型 |
[n]T |
固定大小为n的数组,元素类型为T |
[]T |
切片类型,元素类型为T |
map[K]V |
映射,键类型为 K,值类型为 V |
chan T |
通道类型,元素类型为T |
rune |
Unicode 字符(int32) |
byte |
uint8 的别名 |
类型转换(强转):
var x int = 42
var y float64 = float64(x)
[!NOTE]
1、Go 不允许隐式类型转换
var a int = 10 var b float64 = a // 报错:cannot use a (type int) as type float64必须显式转换:
var b float64 = float64(a) // ✅ 正确2、转换是复制,不会改变原值类型
3、浮点数转整数会截断小数
4、转换不代表语义正确
var x int = 97 var ch byte = byte(x) // ✅ 转换成功 fmt.Println(ch) // 输出: a(因为 ASCII 97 是 'a')5、字符串与数字不能直接转换
var s string = "123" var n int = int(s) // ❌ 报错:cannot convert s (type string) to type int必须使用标准库函数
import "strconv" var s string = "123" n, _ := strconv.Atoi(s) // ✅ 正确:字符串转整数6、切片之间不能直接转换(除非底层类型相同),需要手动循环转换
🧠 总结一句话:
Go 中类型转换必须显式,不允许隐式转换,不改变原类型,浮点转整数会截断,字符串和数字不能直接转换,切片转换更要谨慎。
1.1.5、函数(Functions)
func 函数名(参数列表) 返回值列表 {
// 函数体
}
基本语法
func add(x int, y int) int {
return x + y
}
多返回值
func swap(a, b string) (string, string) {
return b, a
}
命名返回值
func split(sum int) (x, y int) {
x = sum * 4 / 9
y = sum - x
return // 自动返回
}
可变参数
func sum(nums ...int) int {
total := 0
for _, v := range nums {
total += v
}
return total
}
[!NOTE]
函数也可以像变量那样被传递、赋值、作为参数返回等。可以把函数当作“值”来操作。
闭包(Closure)
闭包是函数式编程中的一个重要概念,指的是 函数与其引用环境变量的组合。简单来说,闭包是指一个函数能够访问并操作函数外部的变量,甚至在外部函数执行完毕后,仍然可以使用这些变量。
// 外部函数返回一个闭包函数
func adder(x int) func(int) int {
return func(y int) int {
return x + y // 这里的 x 来自外部函数 adder
}
}
func main() {
add5 := adder(5) // 创建一个闭包,x = 5。
// 这里是把x=5传递到adder函数中,同时返回值便成为一个匿名函数func(y int) int { return 5 + y }
fmt.Println(add5(10)) // 输出:15,5 + 10。
// 这里是将10值赋予到匿名函数中,func(10 int) int { return 5 + 10}
add10 := adder(10) // 创建另一个闭包,x = 10
fmt.Println(add10(5)) // 输出:15,10 + 5
}
匿名函数(Anonymous Functions)
匿名函数,顾名思义,就是没有名字的函数。它在很多情况下是临时定义并立即执行的函数。
package main
import "fmt"
func main() {
// 定义并立即调用匿名函数
func() {
fmt.Println("This is an anonymous function!")
}() // 这里的 () 表示函数立即执行
// 也可以将匿名函数赋值给变量
greet := func(name string) {
fmt.Println("Hello, " + name)
}
greet("Alice") // 输出:Hello, Alice
}
递归(Recursion)
递归是指一个函数直接或间接地调用自身。递归可以用来处理 分治问题,它将一个问题拆分成多个子问题,然后逐步解决每个子问题,最终合并得到整体问题的解决方案。
package main
import "fmt"
// 计算阶乘的递归函数
func factorial(n int) int {
if n == 0 {
return 1 // 基本情况,终止递归
}
return n * factorial(n-1) // 递归调用
}
func main() {
result := factorial(5)
fmt.Println("Factorial of 5 is:", result) // 输出:120
}
递归实现 Fibonacci
package main
import "fmt"
// fib 递归实现斐波那契数列
func fib(n int) int {
if n <= 1 {
return n
}
return fib(n-1) + fib(n-2)
}
func main() {
for i := 0; i < 10; i++ {
fmt.Printf("%d ", fib(i))
}
}
[!NOTE]
1、关于函数的引用
函数需要 大写字母开头,才能被外部访问。Go 语言的规则是,只有 导出的函数、变量、类型(即大写开头的)才可以在包外部使用。
2、关于函数的命名
同一个包内的所有函数、变量、类型必须有唯一的名称(注意是同一个包内)
3、关于函数的执行
- 只有
main包能够作为程序的入口执行。- 在一个项目里可以有多个
main包,但是它们必须位于不同的目录中,每个 main 包都会有自己的独立入口4、关于闭包
当一个函数被定义在另一个函数内部时,它就可以引用外部函数的变量。
Go 中的函数本身是 第一类对象,可以像其他类型一样传递、返回等。
闭包可以让我们定义 “记住” 外部函数的状态 并随时访问和修改。
常见应用:
延迟执行:闭包可以延迟执行函数,并且能够保持状态。
数据封装:使用闭包来创建私有数据,外部无法直接访问这些数据,只能通过闭包提供的函数访问。
回调函数:闭包常用于回调函数的实现,可以传递和修改一些状态。
5、关于匿名函数
Go 允许你定义没有名称的函数,并且可以直接调用这些函数。匿名函数可以传递给其他函数,或者作为闭包使用。
匿名函数通常用于 一次性的操作,例如在回调函数中、或者传递给其他函数进行处理时。
常见应用:
- 即时回调:在一些需要传递函数参数的地方,使用匿名函数来处理回调逻辑。
- 内联函数:当一个函数只被使用一次时,使用匿名函数可以减少定义冗余。
- 作为参数传递:很多情况下,匿名函数作为参数传递给其他函数,处理特定的任务。
6、关于递归
递归函数通常有两个部分:递归终止条件 和 递归调用。递归调用会不断缩小问题规模,直到达到终止条件为止。
在 Go 中,递归函数与其他语言类似,只有当递归条件不再成立时才会停止调用自己。
常见应用:
- 树的遍历:递归广泛应用于树形结构的遍历、搜索等操作(例如深度优先遍历)。
- 分治算法:许多经典的分治算法(如归并排序、快速排序)都依赖于递归来拆分问题并合并结果。
- 动态规划问题:一些动态规划问题可以通过递归的方式来定义状态和递推公式。
1.1.6、控制流(Control Flow)
if/else
条件必须是布尔值true/flase
if x > 0 {
fmt.Println("Positive")
} else if x == 0 {
fmt.Println("Zero")
} else {
fmt.Println("Negative")
}
可以在 if中先执行初始化语句
if n := len(name); n > 3 {
fmt.Println("名字太长")
}
switch(无 break)
switch day := 3; day {
case 1:
fmt.Println("Monday")
case 2, 3:
fmt.Println("Tuesday or Wednesday")
default:
fmt.Println("Other day")
}
switch 默认自动break,如果需要继续执行下一个case,要用fallthrough
switch 支持类型判断
var i interface{} = 10
switch v := i.(type) {
case int:
fmt.Println("int 类型", v)
case string:
fmt.Println("string 类型", v)
default:
fmt.Println("未知类型")
}
for(唯一循环结构)
for i := 0; i < 10; i++ {
fmt.Println(i)
}
// 支持 while 风格
for x < 10 {
x++
}
// 支持无限循环
for {
// break or return needed
}
break / continue / goto
goto Label
Label:
fmt.Println("Jumped here!")
1.2、数组、切片、Map、字符串处理原理
1.2.1、数组(Array)
定义:
var 数组变量名 [元素数量]T
特性:
- 固定大小:一旦声明,数组的大小不可改变。
- 元素类型相同:数组中的元素类型必须相同。
- 值类型:数组是值类型,当你将数组赋给另一个数组时,实际上是进行了一次 复制,而不是引用。
声明和初始化:
// 声明数组并初始化
var arr [3]int = [3]int{1, 2, 3}
// 使用简洁声明方式
arr2 := [3]int{1, 2, 3}
// 数组长度可以通过 len() 获取
fmt.Println(len(arr)) // 输出: 3
访问数组元素:
arr := [3]int{1, 2, 3}
fmt.Println(arr[0]) // 输出: 1
数组的传递:
func modifyArray(arr [3]int) {
arr[0] = 99
}
arr := [3]int{1, 2, 3}
modifyArray(arr)
fmt.Println(arr) // 输出: [1 2 3],原数组没有变化
多维数组:
var matrix [2][2]int
matrix[0][0] = 1
matrix[0][1] = 2
matrix[1][0] = 3
matrix[1][1] = 4
fmt.Println(matrix) // 输出: [[1 2] [3 4]]
1.2.2、切片(Slice)
定义:
var name []T
特性:
- 动态大小:切片的大小可以动态变化。
- 引用类型:切片是引用类型,多个切片指向同一块底层数组,修改一个切片会影响其它切片。
声明和初始化:
// 基本声明方式
var slice []int // 声明切片,默认值为 nil
// 创建并初始化切片
slice := []int{1, 2, 3}
// 通过 make 函数创建切片
slice2 := make([]int, 3) // 创建长度为 3 的切片,元素值为 0
slice3 := make([]int, 3, 5) // 创建长度为 3,容量为 5 的切片
切片的操作:
// 获取切片长度和容量
slice := []int{1, 2, 3, 4}
fmt.Println(len(slice)) // 输出: 4
fmt.Println(cap(slice)) // 输出: 4,切片容量和长度相同
// 切片的切片
newSlice := slice[1:3] // 取出从索引 1 到 3 之间的切片 [2 3]
// 切片追加元素
slice = append(slice, 5) // [1 2 3 4 5]
1.2.3、Map
定义:
map[KeyType]ValueType
特性:
- 无序:Map 是无序的,因此不能通过索引访问元素。
- 动态大小:Map 的大小可以动态变化。
- 键值对:Map 存储的是键值对,键必须是可比较的(例如字符串、数字等)。
- 引用类型:Map 是引用类型,修改一个 Map 会影响其它引用相同 Map 的变量。
声明和初始化:
// 创建一个 map
var m map[string]int // 声明一个空的 map
// 使用 make 函数初始化 map
m := make(map[string]int)
// 初始化并赋值
m := map[string]int{
"Alice": 30,
"Bob": 25,
}
// 或者使用简洁方式
m := map[string]int{"Alice": 30, "Bob": 25}
Map操作:
// 添加元素
m["Charlie"] = 35
// 获取元素
age := m["Alice"] // 获取 "Alice" 对应的值
// 检查键是否存在
value, exists := m["Bob"]
if exists {
fmt.Println("Found Bob:", value)
} else {
fmt.Println("Bob not found")
}
// 删除元素
delete(m, "Charlie")
Map的遍历:
for key, value := range m {
fmt.Println(key, value)
}
1.2.4、字符串(String)
字符串是值类型
str := "Hello"
str[0] = 'h' // 编译时错误,字符串是不可变的
字符串的基本操作:
// 获取字符串长度(字节数)
str := "Hello"
fmt.Println(len(str)) // 输出: 5
// 字符串拼接
greeting := "Hello, " + "World!" // "Hello, World!"
// 字符串转换为切片
bytes := []byte(str) // 字符串转换为字节切片
// 字符串切片
substr := str[1:4] // 获取子串 "ell"
常用字符串函数:
import "strings"
// 字符串包含
strings.Contains(str, "lo") // 返回 true
// 字符串替换
strings.Replace(str, "o", "0", -1) // 替换所有 "o" 为 "0"
// 字符串分割
strings.Split(str, ",") // 返回切片 ["Hello" " World!"]
// 字符串转大写/小写
strings.ToUpper(str) // "HELLO"
strings.ToLower(str) // "hello"
// 去除前后空格
strings.TrimSpace(" Hello ") // "Hello"
[!NOTE]
数组:大小固定、元素类型相同,使用时要注意值传递的问题。
切片:动态大小、引用类型,常常作为数组的替代,切片会直接指向底层数组,修改会影响到其它切片。
Map:无序、动态大小,快速查找和删除元素的工具。
字符串:不可变类型,支持丰富的字符串处理函数,和切片/字节数组的转换非常方便。
练习
1、创建一个字符串切片,存储多个名字,并按字母顺序打印所有名字
package main
import (
"fmt"
"sort"
)
func main() {
names := []string{"Alice", "Bob", "Charlie", "David", "Eve"}
// 排序
sort.Strings(names)
// 打印所有名字
for _, name := range names {
fmt.Println(name)
}
}
// 输出结果:
// jack
// jaki
// jay
// smith
// wilson
2、用切片实现一个栈(后进先出)
package main
import "fmt"
type Stack []int
// 入栈
func (s *Stack) Push(x int) {
*s = append(*s, x)
}
// 出栈
func (s *Stack) Pop() int {
if len(*s) == 0 {
fmt.Println("Stack is empty")
return -1
}
n := len(*s) - 1
item := (*s)[n]
*s = (*s)[:n]
return item
}
// 打印栈内容
func (s *Stack) Print() {
fmt.Println(*s)
}
func main() {
stack := &Stack{}
// 入栈
stack.Push(1)
stack.Push(2)
stack.Push(3)
// 打印栈
stack.Print()
// 出栈
fmt.Println(stack.Pop())
// 打印栈
stack.Print()
}
// 输出结果
// [1 2 3]
// 3
// [1 2]
[!NOTE]
关于“这里为什么需要用到指针?”
Stack本质上是一个 切片(slice),而切片在 Go 中是一种 引用类型,很多人会以为 “既然切片是引用类型,就不需要再用指针传递了”。但实际上——这里用指针是为了修改原切片变量本身(栈的状态)。
3、用 map 实现一个简单的电话号码簿,提供添加、查找和删除电话号码的功能
package main
import "fmt"
// 电话簿类型
type PhoneBook map[string]string
// 添加电话号码
func (pb *PhoneBook) Add(name, number string) {
(*pb)[name] = number
}
// 查找电话号码
func (pb *PhoneBook) Find(name string) string {
if number, os := (*pb)[name]; os {
return number
}
return "电话号码未找到"
}
// 删除电话号码
func (pb *PhoneBook) Remove(name string) {
delete(*pb, name)
}
func main() {
phoneBook := make(PhoneBook)
// 添加电话号码
phoneBook.Add("Alice", "123-456-7890")
phoneBook.Add("Bob", "987-654-3210")
// 查找电话号码
fmt.Println("Alice's phone:", phoneBook.Find("Alice"))
fmt.Println("Bob's phone:", phoneBook.Find("Bob"))
// 删除电话号码
phoneBook.Remove("Alice")
// 查找删除后的号码
fmt.Println("Alice's phone:", phoneBook.Find("Alice"))
}
// 输出结果
// Alice's phone: 123-456-7890
// Bob's phone: 987-654-3210
// Alice's phone: 电话号码未找到
1.3、匿名函数、闭包、defer执行顺序
1.3.1、匿名函数(Anonymous / Literal Function)
匿名函数,通常用于短小的逻辑,或者即时使用。
package main
import "fmt"
func main() {
// 定义并立即调用匿名函数
result := func(x, y int) int {
return x * y
}(3, 5)
fmt.Println("3 * 5 =", result)
// 将匿名函数赋给变量
g := func(s string) {
fmt.Println("Hello,", s)
}
g("Alice")
}
1.3.2、闭包(Closure)
闭包是函数 + 它捕获的环境(外部变量) 的组合。也就是说,匿名函数可以引用它所在作用域以外的变量,这些变量就是“自由变量”(free variables),闭包能够在其内部访问这些自由变量。
package main
import "fmt"
func Adder(base int) func(int) int {
return func(delta int) int {
base += delta // 引用了 outer 的 base 变量
return base
}
}
func main() {
f := Adder(10)
fmt.Println(f(5)) // 输出 15
fmt.Println(f(3)) // 输出 18,base 被持续“记住”
}
[!NOTE]
这个例子中,
Adder返回一个匿名函数,这个匿名函数引用了base。每次调用f(...),它都会在原来base的基础上累加 delta,并保留修改结果。这个就是闭包的典型应用 —— 用闭包保持状态。我们来详细解读一下
main函数执行过程,首先是f := Adder(10),Adder(10)返回的是一个函数,不是函数的执行结果,因此这一步相当于:f = func(delta int) int { base += delta // base 被捕获,初始值为10 return base }后续两次
fmt.Println(f(5))其实就是向f函数里面传入了两次参数还有一个重点就是,闭包不是捕获变量的值,而是捕获变量本身,多次调用操作的是同一个内存位置。所以闭包能够“记住”状态。
1.3.3、defer的执行顺序 与 与return的交互机制
defer 是 Go 中一个非常有特色的关键字,用于延迟执行某个函数调用,直到包含它的函数返回之前。常用于资源释放、清理操作、unlock、关闭句柄等。
defer 的基本特性和执行顺序:
defer后的函数调用会被推入一个栈(内部结构),在函数 return 或达到函数末尾时按 后进先出(LIFO) 的顺序执行。- 参数在
defer声明时就会被求值(即使defer延迟到函数结束后执行)。也就是说,defer fmt.Println(i)中i的值是在defer被注册那一刻确定的,而不是在defer执行时再去取。
func foo() {
x := 10
defer fmt.Println(x) // 这里会把 x 的当前值 10 记录下来
x = 20
return
}
// 输出:10
defer 与 return 之间的顺序:
这是一个经典的坑 — 多个 defer、返回值命名或不命名时的行为要细心区分。
执行顺序总览
- 执行
return语句的第一步 —— 将返回值(无论命名或匿名)赋值或准备好。 - 执行
defer注册的函数(按照 LIFO 顺序)。 - 最后真正返回调用点。也就是说,
defer的执行在 return 的“赋值”之后、真正返回之前。
区分 无名返回值 vs 命名返回值:
无名返回值:
func f1() int {
i := 0
defer func() {
i++
fmt.Println("defer, i =", i)
}()
return i // 这里 i 的值被拷贝给返回值 tmp
}
// 输出 defer, i = 1
// 最终返回值 f1() == 0
命名返回值:
func f2() (i int) {
defer func() {
i++
fmt.Println("defer, i =", i)
}()
return 1
}
// 输出 defer, i = 2
// 最终返回值 f2() == 2
多个 defer 的执行顺序(LIFO):
func example() {
defer fmt.Println("defer #1")
defer fmt.Println("defer #2")
defer fmt.Println("defer #3")
fmt.Println("In function")
}
// 输出结果
// defer #3
// defer #2
// defer #1
[!NOTE]
defer 与闭包结合的复杂情形:
当
defer后面跟的是一个匿名函数(闭包),要注意闭包内部的变量捕获,以及参数求值时机。func test() { i := 0 defer func() { fmt.Println("defer:", i) }() i = 10 return }输出是
defer: 10,因为匿名函数捕获的是变量i的引用,defer注册的时候并不把i的值固定,而是存储一个闭包,闭包真正执行时读取的是最新值。但是,如果在
defer中使用匿名函数时传参,会把当时的值拷贝进参中:func test2() { i := 0 defer func(x int) { fmt.Println("defer with param:", x) }(i) i = 10 return }
1.4、指针与内存传递机制
声明与初始化
声明一个指针变量但不初始化:
var p *int // p 的零值是 nil
取地址:
x := 100
p := &x // p 指向 x 的地址
Go语言中的指针不能进行偏移和运算,因此Go语言中的指针操作非常简单,我们只需要记住两个符号:&(取地址)和*(根据地址取值)。
取地址操作符&和取值操作符*是一对互补操作符,&取出地址,*根据地址取出地址指向的值。
变量、指针地址、指针变量、取地址、取值的相互关系和特性如下:
- 对变量进行取地址(&)操作,可以获得这个变量的指针变量。
- 指针变量的值是指针地址。
- 对指针变量进行取值(*)操作,可以获得指针变量指向的原变量的值。
在Go语言中对于引用类型的变量,我们在使用的时候不仅要声明它,还要为它分配内存空间,否则我们的值就没办法存储。而对于值类型的声明不需要分配内存空间,是因为它们在声明的时候已经默认分配好了内存空间。要分配内存,就引出来今天的new和make。 Go语言中new和make是内建的两个函数,主要用来分配内存。
new 函数创建指针(相当于分配零值变量并返回其地址):
p := new(int) // p 是 *int,指向一个值为 0 的 int
``` :contentReference[oaicite:9]{index=9}
make也是用于内存分配的,区别于new,它只用于slice、map以及channel的内存创建,而且它返回的类型就是这三个类型本身,而不是他们的指针类型,因为这三种类型就是引用类型,所以就没有必要返回他们的指针了。make函数的函数签名如下:
func make(t Type, size ...IntegerType) Type
[!NOTE]
1、为什么要用指针?
传值 vs 传引用(指针)
- 在 Go 中,函数参数默认是值传递(pass by value): 当你把一个变量传入函数时,函数收到的是变量 副本。修改这个副本不会影响原变量。
- 如果你希望函数内部修改原变量,或者避免复制大型数据结构(比如结构体、大数组)带来的开销,就可以传一个指针,让函数接收变量的地址,从而修改或操作原变量。
2、 内存传递机制、指针与大型数据结构
✅ 对结构体或者大块数据的效率考虑
如果一个函数参数是一个结构体(尤其大结构体),按值传递会导致函数内部拷贝整个结构体。使用指针则避免了这种拷贝、节省内存和时间。 Stack Overflow
✅ 切片、Map 等引用类型与指针的区别
- 切片(slice)、Map、channel 均为 引用类型,背后还有指向数据结构的指针。即使你不显式使用指针,传递这些类型时实际上是传递它们的描述符/头信息(包含指向底层数据的指针)而非全部数据。
- 比如传递
[]T时,函数接收的是 slice 的 “头” 信息(指针、长度、容量)拷贝,但底层数组并未复制。使用*[]T(指向 slice 的指针)时,意味着你可能修改 slice 头结构(比如长度、容量)本身。这个区别在性能/修改语义上值得理解。3、指针必须注意的几个“坑”与原理细节
⚠️ nil 指针
如果你声明一个指针,但没有把它指向一个合法地址,则其值为
nil。解引用nil指针会导致运行时 panic。⚠️ 不支持指针运算
Go 不允许对指针进行像 C 中的
p++或p-2这样的运算。这样做可提高安全性。⚠️ 返回局部变量地址是安全的
在很多语言返回局部变量的地址可能导致悬垂指针(dangling pointer),但在 Go 里,由于逃逸分析和垃圾回收机制的存在,这是安全的。
⚠️ 并发与内存模型
如果多个 goroutine 共同访问、修改指针指向的数据,必须加锁或使用 channel、atomic 操作,否则会出现数据竞争(data race)。Go 的内存模型中强调:无数据竞争的程序表现为顺序一致。
2、面向对象与函数式设计
2.1、struct 和方法(值接收器 vs 指针接收器)
2.1.1、结构体
结构体是 Go 中的一种复合数据类型,它可以将多个不同类型的变量(字段)组合在一起,形成一个新的类型。
声明结构体
// 声明一个结构体类型
type Person struct {
Name, City string
Age int
}
// type 类型名 struct {
// 字段名 字段类型
// 字段名 字段类型
// …
// }
2.1.2、结构体实例化
创建结构体实例
// 创建结构体实例(字面量)
p := Person{"Alice", 30} // 顺序方式
p := Person{Name: "Bob", Age: 25} // 字段名方式
p := new(Person) // 使用 new 返回指向结构体的指针
访问结构体字段
fmt.Println(p.Name) // 访问结构体字段
p.Age = 35 // 修改字段值
匿名结构体
Go 允许结构体有匿名字段(即不指定字段名,直接指定类型),这种方式类似于嵌入式结构体,可以实现继承的效果。
type Animal struct {
Species string
}
type Dog struct {
Animal // 匿名字段
Breed string
}
d := Dog{Animal: Animal{Species: "Canine"}, Breed: "Bulldog"}
fmt.Println(d.Species) // 输出: Canine
创建指针类型结构体
还可以通过使用new关键字对结构体进行实例化,得到的是结构体的地址。
var p2 = new(person)
fmt.Printf("%T\n", p2) //*main.person
fmt.Printf("p2=%#v\n", p2) //p2=&main.person{name:"", city:"", age:0}
需要注意的是在Go语言中支持对结构体指针直接使用.来访问结构体的成员。
var p2 = new(person)
p2.name = "小王子"
p2.age = 28
p2.city = "上海"
fmt.Printf("p2=%#v\n", p2) //p2=&main.person{name:"小王子", city:"上海", age:28}
取结构体的地址实例化
使用&对结构体进行取地址操作相当于对该结构体类型进行了一次new实例化操作。
p3 := &person{}
fmt.Printf("%T\n", p3) //*main.person
fmt.Printf("p3=%#v\n", p3) //p3=&main.person{name:"", city:"", age:0}
p3.name = "七米"
p3.age = 30
p3.city = "成都"
fmt.Printf("p3=%#v\n", p3) //p3=&main.person{name:"七米", city:"成都", age:30}
p3.name = "七米"其实在底层是(*p3).name = "七米",这是Go语言帮我们实现的语法。
2.1.3、结构体初始化
没有初始化的结构体,其成员变量都是对应其类型的零值。
type person struct {
name string
city string
age int8
}
func main() {
var p4 person
fmt.Printf("p4=%#v\n", p4) //p4=main.person{name:"", city:"", age:0}
}
使用键值对初始化
使用键值对对结构体进行初始化时,键对应结构体的字段,值对应该字段的初始值。
p5 := person{
name: "小王子",
city: "北京",
age: 18,
}
fmt.Printf("p5=%#v\n", p5) //p5=main.person{name:"小王子", city:"北京", age:18}
也可以对结构体指针进行键值对初始化,例如:
p6 := &person{
name: "小王子",
city: "北京",
age: 18,
}
fmt.Printf("p6=%#v\n", p6) //p6=&main.person{name:"小王子", city:"北京", age:18}
当某些字段没有初始值的时候,该字段可以不写。此时,没有指定初始值的字段的值就是该字段类型的零值。
p7 := &person{
city: "北京",
}
fmt.Printf("p7=%#v\n", p7) //p7=&main.person{name:"", city:"北京", age:0}
使用值的列表初始化
初始化结构体的时候可以简写,也就是初始化的时候不写键,直接写值:
p8 := &person{
"沙河娜扎",
"北京",
28,
}
fmt.Printf("p8=%#v\n", p8) //p8=&main.person{name:"沙河娜扎", city:"北京", age:28}
使用这种格式初始化时,需要注意:
- 必须初始化结构体的所有字段。
- 初始值的填充顺序必须与字段在结构体中的声明顺序一致。
- 该方式不能和键值初始化方式混用。
2.1.4、结构体内存布局
结构体占用一块连续的内存。
type test struct {
a int8
b int8
c int8
d int8
}
n := test{
1, 2, 3, 4,
}
fmt.Printf("n.a %p\n", &n.a)
fmt.Printf("n.b %p\n", &n.b)
fmt.Printf("n.c %p\n", &n.c)
fmt.Printf("n.d %p\n", &n.d)
// 输出
// n.a 0xc0000a0060
// n.b 0xc0000a0061
// n.c 0xc0000a0062
// n.d 0xc0000a0063
空结构体
空结构体不占用内存
var v struct{}
fmt.Println(unsafe.Sizeof(v)) // 0
构造函数
Go语言的结构体没有构造函数,我们可以自己实现。 例如,下方的代码就实现了一个person的构造函数。 因为struct是值类型,如果结构体比较复杂的话,值拷贝性能开销会比较大,所以该构造函数返回的是结构体指针类型。
func newPerson(name, city string, age int8) *person {
return &person{
name: name,
city: city,
age: age,
}
}
// 调用构造函数
p9 := newPerson("张三", "沙河", 90)
fmt.Printf("%#v\n", p9) //&main.person{name:"张三", city:"沙河", age:90}
2.1.5、方法和接收者
Go语言中的方法(Method)是一种作用于特定类型变量的函数。这种特定类型变量叫做接收者(Receiver)。接收者的概念就类似于其他语言中的this或者 self。
方法的定义格式如下:
func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) {
函数体
}
// 接收者变量:接收者中的参数变量名在命名时,官方建议使用接收者类型名称首字母的小写,而不是self、this之类的命名。例如,Person类型的接收者变量应该命名为 p,Connector类型的接收者变量应该命名为c等。
// 接收者类型:接收者类型和参数类似,可以是指针类型和非指针类型。
// 方法名、参数列表、返回参数:具体格式与函数定义相同。
举例:
//Person 结构体
type Person struct {
name string
age int8
}
//NewPerson 构造函数
func NewPerson(name string, age int8) *Person {
return &Person{
name: name,
age: age,
}
}
//Dream Person做梦的方法
func (p Person) Dream() {
fmt.Printf("%s的梦想是学好Go语言!\n", p.name)
}
func main() {
p1 := NewPerson("小王子", 25)
p1.Dream()
}
指针类型的接收者
指针类型的接收者由一个结构体的指针组成,由于指针的特性,调用方法时修改接收者指针的任意成员变量,在方法结束后,修改都是有效的。这种方式就十分接近于其他语言中面向对象中的this或者self。 例如我们为Person添加一个SetAge方法,来修改实例变量的年龄。
// SetAge 设置p的年龄
// 使用指针接收者
func (p *Person) SetAge(newAge int8) {
p.age = newAge
}
调用该方法:
func main() {
p1 := NewPerson("小王子", 25)
fmt.Println(p1.age) // 25
p1.SetAge(30)
fmt.Println(p1.age) // 30
}
值类型的接收者
当方法作用于值类型接收者时,Go语言会在代码运行时将接收者的值复制一份。在值类型接收者的方法中可以获取接收者的成员值,但修改操作只是针对副本,无法修改接收者变量本身。
// SetAge2 设置p的年龄
// 使用值接收者
func (p Person) SetAge2(newAge int8) {
p.age = newAge
}
func main() {
p1 := NewPerson("小王子", 25)
p1.Dream()
fmt.Println(p1.age) // 25
p1.SetAge2(30) // (*p1).SetAge2(30)
fmt.Println(p1.age) // 25
}
[!NOTE]
什么时候应该使用指针类型接收者?
- 需要修改接收者中的值
- 接收者是拷贝代价比较大的大对象
- 保证一致性,如果有某个方法使用了指针接收者,那么其他的方法也应该使用指针接收者。
2.1.6、任意类型添加方法
在Go语言中,接收者的类型可以是任何类型,不仅仅是结构体,任何类型都可以拥有方法。 举个例子,我们基于内置的int类型使用type关键字可以定义新的自定义类型,然后为我们的自定义类型添加方法。
注意事项: 非本地类型不能定义方法,也就是说我们不能给别的包的类型定义方法。
//MyInt 将int定义为自定义MyInt类型
type MyInt int
//SayHello 为MyInt添加一个SayHello的方法
func (m MyInt) SayHello() {
fmt.Println("Hello, 我是一个int。")
}
func main() {
var m1 MyInt
m1.SayHello() //Hello, 我是一个int。
m1 = 100
fmt.Printf("%#v %T\n", m1, m1) //100 main.MyInt
}
2.1.7、嵌套结构体
一个结构体中可以嵌套包含另一个结构体或结构体指针,如下示例。
//Address 地址结构体
type Address struct {
Province string
City string
}
//User 用户结构体
type User struct {
Name string
Gender string
Address Address
}
func main() {
user1 := User{
Name: "小王子",
Gender: "男",
Address: Address{
Province: "山东",
City: "威海",
},
}
fmt.Printf("user1=%#v\n", user1)//user1=main.User{Name:"小王子", Gender:"男", Address:main.Address{Province:"山东", City:"威海"}}
}
嵌套匿名字段
上面user结构体中嵌套的Address结构体也可以采用匿名字段的方式,例如:
//Address 地址结构体
type Address struct {
Province string
City string
}
//User 用户结构体
type User struct {
Name string
Gender string
Address //匿名字段
}
func main() {
var user2 User
user2.Name = "小王子"
user2.Gender = "男"
user2.Address.Province = "山东" // 匿名字段默认使用类型名作为字段名
user2.City = "威海" // 匿名字段可以省略
fmt.Printf("user2=%#v\n", user2) //user2=main.User{Name:"小王子", Gender:"男", Address:main.Address{Province:"山东", City:"威海"}}
}
[!NOTE]
Go 编译器在编译时,内部处理机制会“展开”成:
type User struct { Name string Gender string Address Address Province string // 逻辑上被“提升”了(但物理上仍属于 Address) City string // 同样被“提升” }⚠️ 注意:这里的“展开”只是编译器的语义理解,
实际上Province和City仍然是Address里的字段,
只是你可以在外层通过简写访问。
嵌套结构体的字段名冲突
嵌套结构体内部可能存在相同的字段名。在这种情况下为了避免歧义需要通过指定具体的内嵌结构体字段名。
//Address 地址结构体
type Address struct {
Province string
City string
CreateTime string
}
//Email 邮箱结构体
type Email struct {
Account string
CreateTime string
}
//User 用户结构体
type User struct {
Name string
Gender string
Address
Email
}
func main() {
var user3 User
user3.Name = "沙河娜扎"
user3.Gender = "男"
// user3.CreateTime = "2019" //ambiguous selector user3.CreateTime
user3.Address.CreateTime = "2000" //指定Address结构体中的CreateTime
user3.Email.CreateTime = "2000" //指定Email结构体中的CreateTime
}
2.1.8、结构体的“继承”
Go语言中使用结构体也可以实现其他编程语言中面向对象的继承。
//Animal 动物
type Animal struct {
name string
}
func (a *Animal) move() {
fmt.Printf("%s会动!\n", a.name)
}
//Dog 狗
type Dog struct {
Feet int8
*Animal //通过嵌套匿名结构体实现继承
}
func (d *Dog) wang() {
fmt.Printf("%s会汪汪汪~\n", d.name)
}
func main() {
d1 := &Dog{
Feet: 4,
Animal: &Animal{ //注意嵌套的是结构体指针
name: "乐乐",
},
}
d1.wang() //乐乐会汪汪汪~
d1.move() //乐乐会动!
}
结构体中字段大写开头表示可公开访问,小写表示私有(仅在定义当前结构体的包中可访问)。
2.1.9、结构体与JSON序列化
JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。JSON键值对是用来保存JS对象的一种方式,键/值对组合中的键名写在前面并用双引号""包裹,使用冒号:分隔,然后紧接着值;多个键值之间使用英文,分隔。
//Student 学生
type Student struct {
ID int
Gender string
Name string
}
//Class 班级
type Class struct {
Title string
Students []Student
}
func main() {
c := &Class{
Title: "101",
Students: make([]Student, 0, 200),
}
for i := 0; i < 10; i++ {
stu := Student{
Name: fmt.Sprintf("stu%02d", i),
Gender: "男",
ID: i,
}
c.Students = append(c.Students, stu)
}
//JSON序列化:结构体-->JSON格式的字符串
data, err := json.Marshal(c)
if err != nil {
fmt.Println("json marshal failed")
return
}
fmt.Printf("json:%s\n", data)
//JSON反序列化:JSON格式的字符串-->结构体
str := `{"Title":"101","Students":[{"ID":0,"Gender":"男","Name":"stu00"},{"ID":1,"Gender":"男","Name":"stu01"},{"ID":2,"Gender":"男","Name":"stu02"},{"ID":3,"Gender":"男","Name":"stu03"},{"ID":4,"Gender":"男","Name":"stu04"},{"ID":5,"Gender":"男","Name":"stu05"},{"ID":6,"Gender":"男","Name":"stu06"},{"ID":7,"Gender":"男","Name":"stu07"},{"ID":8,"Gender":"男","Name":"stu08"},{"ID":9,"Gender":"男","Name":"stu09"}]}`
c1 := &Class{}
err = json.Unmarshal([]byte(str), c1)
if err != nil {
fmt.Println("json unmarshal failed!")
return
}
fmt.Printf("%#v\n", c1)
}
2.1.10、结构体标签(Tag)
在 Go 中,Tag(结构体标签) 是定义在结构体字段上的一段 元信息(metadata),用于在运行时通过反射(reflection)被读取,让外部库或框架知道“这个字段该怎么处理”。
简单来说:
Tag 是写在结构体字段定义后面的“一小段字符串”,用来告诉程序“这个字段该如何被解释或使用”。
具体的格式如下:
`key1:"value1" key2:"value2"`
基本语法:
type Person struct {
Name string `json:"name" xml:"name" db:"username"`
Age int `json:"age"`
}
注意事项: 为结构体编写Tag时,必须严格遵守键值对的规则。结构体标签的解析代码的容错能力很差,一旦格式写错,编译和运行时都不会提示任何错误,通过反射也无法正确取值。例如不要在key和value之间添加空格。
例如我们为Student结构体的每个字段定义json序列化时使用的Tag:
//Student 学生
type Student struct {
ID int `json:"id"` //通过指定tag实现json序列化该字段时的key
Gender string //json序列化是默认使用字段名作为key
name string //私有不能被json包访问
}
func main() {
s1 := Student{
ID: 1,
Gender: "男",
name: "沙河娜扎",
}
data, err := json.Marshal(s1)
if err != nil {
fmt.Println("json marshal failed!")
return
}
fmt.Printf("json str:%s\n", data) //json str:{"id":1,"Gender":"男"}
}
Tag 的作用举例:
- JSON 序列化控制
- ORM(数据库映射)
- Web 表单或参数绑定
[!IMPORTANT]
结构体和方法补充知识点
因为slice和map这两种数据类型都包含了指向底层数据的指针,因此我们在需要复制它们时要特别注意。我们来看下面的例子:
type Person struct { name string age int8 dreams []string } func (p *Person) SetDreams(dreams []string) { p.dreams = dreams } func main() { p1 := Person{name: "小王子", age: 18} data := []string{"吃饭", "睡觉", "打豆豆"} p1.SetDreams(data) // 你真的想要修改 p1.dreams 吗? data[1] = "不睡觉" fmt.Println(p1.dreams) // ? } // 输出结果 // [吃饭 不睡觉 打豆豆]⚙️ 为什么会这样?
从表面看,你可能以为:
“我只是修改了
data,没动p1.dreams啊!”但实际上,
p1.dreams和data在底层 共享同一块数组内存。📚 原理详解:Go 中 slice 的“引用特性”
在 Go 中,切片
[]T不是简单的数组,它其实是一个结构体头(slice header),内部包含三部分内容:type slice struct { Data uintptr // 指向底层数组的指针 Len int // 当前长度 Cap int // 容量 }当你写:
p1.dreams = data这只是复制了 slice header(三个字段),其中
Data字段(底层数组指针)还是同一个地址。但这三个字段中的
Data指针仍然指向同一底层数组!也就是说:p1.dreams 和 data 共用同一片底层数组。
⚠️ 问题总结
从语法上看,这段代码完全没错、能正常运行。
但语义上存在隐患:
p1.dreams引用了外部传入切片的底层数组。
如果调用者修改了切片内容,Person内部数据也会被意外改变。这种问题在真实业务中非常危险,因为会导致:
- 数据被意外篡改;
- 并发读写冲突;
- 对象封装性被破坏。
✅ 正确做法(拷贝切片)
要让
Person拥有自己独立的数据副本,
应该在SetDreams中做一次“深拷贝”:func (p *Person) SetDreams(dreams []string) { // 创建一个新的切片副本 p.dreams = make([]string, len(dreams)) copy(p.dreams, dreams) }这样:
p.dreams有自己的底层数组;- 后续修改外部
data不会影响p1.dreams。
2.2、接口(interface)机制原理
2.2.1、接口类型
接口是一种由程序员来定义的类型,一个接口类型就是一组方法的集合,它规定了需要实现的所有方法。
相较于使用结构体类型,当我们使用接口类型说明相比于它是什么更关心它能做什么。
接口定义
type 接口类型名 interface{
方法名1( 参数列表1 ) 返回值列表1
方法名2( 参数列表2 ) 返回值列表2
…
}
其中:
- 接口类型名:Go语言的接口在命名时,一般会在单词后面添加
er,如有写操作的接口叫Writer,有关闭操作的接口叫closer等。接口名最好要能突出该接口的类型含义。 - 方法名:当方法名首字母是大写且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问。
- 参数列表、返回值列表:参数列表和返回值列表中的参数变量名可以省略。
举个例子,定义一个包含Write方法的Writer接口。
type Writer interface{
Write([]byte) error
}
接口代表 “能做什么” 而不是 “是什么”——这是典型的结构化类型(structural typing)而非名义类型(nominal typing)机制。
实现接口的条件
接口就是规定了一个需要实现的方法列表,在 Go 语言中一个类型只要实现了接口中规定的所有方法,那么我们就称它实现了这个接口。
比如,这里定义的Singer接口类型,它包含一个Sing方法。
// Singer 接口
type Singer interface {
Sing()
}
有一个Bird 结构体类型如下。
type Bird struct {}
因为Singer接口只包含一个Sing方法,所以只需要给Bird结构体添加一个Sing方法就可以满足Singer接口的要求。
// Sing Bird类型的Sing方法
func (b Bird) Sing() {
fmt.Println("汪汪汪")
}
这样就称为Bird实现了Singer接口。
为什么要使用接口?
现在假设我们的代码世界里有很多小动物,下面的代码片段定义了猫和狗,它们饿了都会叫。
package main
import "fmt"
type Cat struct{}
func (c Cat) Say() {
fmt.Println("喵喵喵~")
}
type Dog struct{}
func (d Dog) Say() {
fmt.Println("汪汪汪~")
}
func main() {
c := Cat{}
c.Say()
d := Dog{}
d.Say()
}
这个时候又跑来了一只羊,羊饿了也会发出叫声。
type Sheep struct{}
func (s Sheep) Say() {
fmt.Println("咩咩咩~")
}
我们接下来定义一个饿肚子的场景。
// MakeCatHungry 猫饿了会喵喵喵~
func MakeCatHungry(c Cat) {
c.Say()
}
// MakeSheepHungry 羊饿了会咩咩咩~
func MakeSheepHungry(s Sheep) {
s.Say()
}
接下来会有越来越多的小动物跑过来,我们的代码世界该怎么拓展呢?
在饿肚子这个场景下,我们可不可以把所有动物都当成一个“会叫的类型”来处理呢?当然可以!使用接口类型就可以实现这个目标。 我们的代码其实并不关心究竟是什么动物在叫,我们只是在代码中调用它的Say()方法,这就足够了。
我们可以约定一个Sayer类型,它必须实现一个Say()方法,只要饿肚子了,我们就调用Say()方法。
type Sayer interface {
Say()
}
然后我们定义一个通用的MakeHungry函数,接收Sayer类型的参数。
// MakeHungry 饿肚子了...
func MakeHungry(s Sayer) {
s.Say()
}
我们通过使用接口类型,把所有会叫的动物当成Sayer类型来处理,只要实现了Say()方法都能当成Sayer类型的变量来处理。
var c cat
MakeHungry(c)
var d dog
MakeHungry(d)
在电商系统中我们允许用户使用多种支付方式(支付宝支付、微信支付、银联支付等),我们的交易流程中可能不太在乎用户究竟使用什么支付方式,只要它能提供一个实现支付功能的Pay方法让调用方调用就可以了。
再比如我们需要在某个程序中添加一个将某些指标数据向外输出的功能,根据不同的需求可能要将数据输出到终端、写入到文件或者通过网络连接发送出去。在这个场景下我们可以不关注最终输出的目的地是什么,只需要它能提供一个Write方法让我们把内容写入就可以了。
Go语言中为了解决类似上面的问题引入了接口的概念,接口类型区别于我们之前章节中介绍的那些具体类型,让我们专注于该类型提供的方法,而不是类型本身。使用接口类型通常能够让我们写出更加通用和灵活的代码。
面向接口编程
在下面的代码示例中,我们的电商系统最开始只设计了支付宝一种支付方式:
type ZhiFuBao struct {
// 支付宝
}
// Pay 支付宝的支付方法
func (z *ZhiFuBao) Pay(amount int64) {
fmt.Printf("使用支付宝付款:%.2f元。\n", float64(amount/100))
}
// Checkout 结账
func Checkout(obj *ZhiFuBao) {
// 支付100元
obj.Pay(100)
}
func main() {
Checkout(&ZhiFuBao{})
}
随着业务的发展,根据用户需求添加支持微信支付。
type WeChat struct {
// 微信
}
// Pay 微信的支付方法
func (w *WeChat) Pay(amount int64) {
fmt.Printf("使用微信付款:%.2f元。\n", float64(amount/100))
}
在实际的交易流程中,我们可以根据用户选择的支付方式来决定最终调用支付宝的Pay方法还是微信支付的Pay方法。
// Checkout 支付宝结账
func CheckoutWithZFB(obj *ZhiFuBao) {
// 支付100元
obj.Pay(100)
}
// Checkout 微信支付结账
func CheckoutWithWX(obj *WeChat) {
// 支付100元
obj.Pay(100)
}
实际上,从上面的代码示例中我们可以看出,我们其实并不怎么关心用户选择的是什么支付方式,我们只关心调用Pay方法时能否正常运行。这就是典型的“不关心它是什么,只关心它能做什么”的场景。
在这种场景下我们可以将具体的支付方式抽象为一个名为Payer的接口类型,即任何实现了Pay方法的都可以称为Payer类型。
// Payer 包含支付方法的接口类型
type Payer interface {
Pay(int64)
}
此时只需要修改下原始的Checkout函数,它接收一个Payer类型的参数。这样就能够在不修改既有函数调用的基础上,支持新的支付方式。
2.2.2、值接受者和指针接收者
我们定义一个Mover接口,它包含一个Move方法。
// Mover 定义一个接口类型
type Mover interface {
Move()
}
值接收者实现接口
我们定义一个Dog结构体类型,并使用值接收者为其定义一个Move方法。
// Dog 狗结构体类型
type Dog struct{}
// Move 使用值接收者定义Move方法实现Mover接口
func (d Dog) Move() {
fmt.Println("狗会动")
}
此时实现Mover接口的是Dog类型。
var x Mover // 声明一个Mover类型的变量x
var d1 = Dog{} // d1是Dog类型
x = d1 // 可以将d1赋值给变量x
x.Move()
var d2 = &Dog{} // d2是Dog指针类型
x = d2 // 也可以将d2赋值给变量x
x.Move()
从上面的代码中我们可以发现,使用值接收者实现接口之后,不管是结构体类型还是对应的结构体指针类型的变量都可以赋值给该接口变量。
指针接收者实现接口
我们再来测试一下使用指针接收者实现接口有什么区别。
// Cat 猫结构体类型
type Cat struct{}
// Move 使用指针接收者定义Move方法实现Mover接口
func (c *Cat) Move() {
fmt.Println("猫会动")
}
此时实现Mover接口的是*Cat类型,我们可以将*Cat类型的变量直接赋值给Mover接口类型的变量x。
var c1 = &Cat{} // c1是*Cat类型
x = c1 // 可以将c1当成Mover类型
x.Move()
但是不能给将Cat类型的变量赋值给Mover接口类型的变量x。
// 下面的代码无法通过编译
var c2 = Cat{} // c2是Cat类型
x = c2 // 不能将c2当成Mover类型
由于Go语言中有对指针求值的语法糖,对于值接收者实现的接口,无论使用值类型还是指针类型都没有问题。但是我们并不总是能对一个值求址,所以对于指针接收者实现的接口要额外注意。
2.2.3、类型与接口的关系
一个类型实现多个接口
一个类型可以同时实现多个接口,而接口间彼此独立,不知道对方的实现。例如狗不仅可以叫,还可以动。我们完全可以分别定义Sayer接口和Mover接口,具体代码示例如下。
// Sayer 接口
type Sayer interface {
Say()
}
// Mover 接口
type Mover interface {
Move()
}
Dog既可以实现Sayer接口,也可以实现Mover接口。
type Dog struct {
Name string
}
// 实现Sayer接口
func (d Dog) Say() {
fmt.Printf("%s会叫汪汪汪\n", d.Name)
}
// 实现Mover接口
func (d Dog) Move() {
fmt.Printf("%s会动\n", d.Name)
}
同一个类型实现不同的接口互相不影响使用。
var d = Dog{Name: "旺财"}
var s Sayer = d
var m Mover = d
s.Say() // 对Sayer类型调用Say方法
m.Move() // 对Mover类型调用Move方法
多种类型实现同一接口
Go语言中不同的类型还可以实现同一接口。例如在我们的代码世界中不仅狗可以动,汽车也可以动。我们可以使用如下代码体现这个关系。
// 实现Mover接口
func (d Dog) Move() {
fmt.Printf("%s会跑\n", d.Name)
}
// Car 汽车结构体类型
type Car struct {
Brand string
}
// Move Car类型实现Mover接口
func (c Car) Move() {
fmt.Printf("%s速度70迈\n", c.Brand)
}
这样我们在代码中就可以把狗和汽车当成一个会动的类型来处理,不必关注它们具体是什么,只需要调用它们的Move方法就可以了。
var obj Mover
obj = Dog{Name: "旺财"}
obj.Move()
obj = Car{Brand: "宝马"}
obj.Move()
// 结果
// 旺财会跑
// 宝马速度70迈
一个接口的所有方法,不一定需要由一个类型完全实现,接口的方法可以通过在类型中嵌入其他类型或者结构体来实现。
// WashingMachine 洗衣机
type WashingMachine interface {
wash()
dry()
}
// 甩干器
type dryer struct{}
// 实现WashingMachine接口的dry()方法
func (d dryer) dry() {
fmt.Println("甩一甩")
}
// 海尔洗衣机
type haier struct {
dryer //嵌入甩干器
}
// 实现WashingMachine接口的wash()方法
func (h haier) wash() {
fmt.Println("洗刷刷")
}
2.2.4、接口组合
接口与接口之间可以通过互相嵌套形成新的接口类型,例如Go标准库io源码中就有很多接口之间互相组合的示例。
// src/io/io.go
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
type Closer interface {
Close() error
}
// ReadWriter 是组合Reader接口和Writer接口形成的新接口类型
type ReadWriter interface {
Reader
Writer
}
// ReadCloser 是组合Reader接口和Closer接口形成的新接口类型
type ReadCloser interface {
Reader
Closer
}
// WriteCloser 是组合Writer接口和Closer接口形成的新接口类型
type WriteCloser interface {
Writer
Closer
}
2.3、类型断言 vs 类型转换
2.3.1、类型断言
类型断言是 Go 中一种非常常见的操作,它允许你从接口类型中获取具体类型的值。通过类型断言,你可以确认一个接口变量的实际类型,并将其转换为该类型的值。
类型断言语法
value := x.(T)
x是一个接口类型,T是你想要转换成的目标类型。- 如果
x是T类型,value就是x中实际值的副本。 - 如果
x不是T类型,类型断言会导致运行时 panic。
安全类型断言
为了避免 panic,可以使用 comma ok 语法来安全地执行类型断言:
value, ok := x.(T)
ok是一个布尔值,表示类型断言是否成功。- 如果
x的实际类型是T,ok会是true,value是转换后的值。 - 如果
x的实际类型不是T,ok会是false,value是T类型的零值。
package main
import "fmt"
type Animal interface {
Speak() string
}
type Dog struct{}
func (d Dog) Speak() string {
return "Woof!"
}
func main() {
var animal Animal = Dog{}
// 安全类型断言
if dog, ok := animal.(Dog); ok {
fmt.Println("Dog says:", dog.Speak()) // 输出: Dog says: Woof!
} else {
fmt.Println("Not a Dog")
}
// 不安全类型断言(会 panic)
// dog := animal.(Dog) // 如果 animal 不是 Dog 类型,这里会 panic
}
典型用法
- 类型断言广泛用于从接口类型转换到具体类型。
- 在实际开发中,常见的场景是
interface{}(空接口)接收不同类型的数据,然后通过类型断言获取具体类型值。
2.3.2、类型转换
类型转换是指将一个变量从一种类型转换成另一种类型。Go 中的类型转换与其他语言(如 C/C++)有所不同,Go 不支持隐式类型转换,必须明确指定类型。
类型转换语法
value := T(x)
x是你要转换的值,T是目标类型。T(x)语法用于将x转换为T类型。
类型转换要求:
- 相同底层类型:两者必须有相同的底层类型,才能进行转换。
- 不同底层类型:即使它们都是同样的结构体或整数类型,也不能直接进行类型转换,必须通过显式的转换。
package main
import "fmt"
func main() {
var x int = 42
var y float64 = float64(x) // 将 int 转换为 float64
fmt.Println(y) // 输出: 42
}
| 特点 | 类型断言 | 类型转换 |
|---|---|---|
| 作用 | 将接口类型的变量转换为具体类型 | 将一种类型的值转换为另一种类型 |
| 适用场景 | 用于接口类型转换 | 用于基本数据类型或结构体类型之间的转换 |
| 语法 | x.(T) |
T(x) |
| 错误处理 | 如果断言失败,会导致运行时 panic(除非使用 comma ok) |
如果类型不匹配,会报编译错误 |
| 灵活性 | 可用于接口类型的动态类型转换,适用于实现多态的场景 | 适用于已知的类型之间转换 |
2.4、空接口、反射机制
2.4.1、空接口
空接口的定义与用途
- 空接口是一个不包含任何方法的接口类型,表示 任何类型 的值都可以赋给它。
- 在 Go 中,空接口是通用的,几乎所有类型都实现了空接口。比如,所有的结构体、数组、切片、Map、函数、基本类型等都可以赋值给空接口。
- 由于 Go 中所有类型都实现了空接口,所以空接口常用来接收各种不同类型的值,常见于函数参数、返回值、Map 的值等场景。
示例:使用空接口接收不同类型的值
package main
import "fmt"
func PrintValue(value interface{}) {
fmt.Println(value)
}
func main() {
PrintValue(42) // int 类型
PrintValue("Hello") // string 类型
PrintValue(3.14) // float64 类型
}
在上面的代码中,PrintValue 函数接受了 interface{} 类型的参数,允许传入任何类型的值。
空接口与 interface{} 的常见场景
- 函数参数类型:通过空接口接收各种类型的值,便于函数的通用化。
- 结构体中的字段:可以在结构体中使用空接口来存储不同类型的数据。
- Map 的值类型:可以使用空接口来创建通用的
map[string]interface{}类型的 Map。
示例:空接口在 Map 中的应用
package main
import "fmt"
func main() {
data := map[string]interface{}{
"name": "John",
"age": 30,
"active": true,
}
fmt.Println(data)
}
空接口的类型断言
空接口类型的值在使用时经常需要通过类型断言来获取其具体类型。类型断言的语法为:
value, ok := x.(T)
- 如果
x的类型是T,ok为true,value为转换后的值。 - 如果
x不是T,ok为false,value为T类型的零值。
示例:空接口的类型断言
package main
import "fmt"
func main() {
var value interface{} = "Hello, Go!"
// 类型断言,将 interface{} 转换为 string 类型
str, ok := value.(string)
if ok {
fmt.Println("String value:", str) // 输出: String value: Hello, Go!
} else {
fmt.Println("Not a string!")
}
}
2.4.2、反射
反射的基本概念
反射是 Go 中的一种能力,可以让程序在运行时检查和操作对象的类型和数据。通过反射,程序可以动态地获取对象的类型、字段、方法等信息。
在Go语言的反射机制中,任何接口值都由是一个具体类型和具体类型的值两部分组成的。 在Go语言中反射的相关功能由内置的reflect包提供,任意接口值在反射中都可以理解为由reflect.Type和reflect.Value两部分组成,并且reflect包提供了reflect.TypeOf和reflect.ValueOf两个函数来获取任意对象的Value和Type。
TypeOf
在Go语言中,使用 reflect.TypeOf() 函数可以获得任意值的类型对象(reflect.Type),程序通过类型对象可以访问任意值的类型信息。
package main
import (
"fmt"
"reflect"
)
func reflectType(x interface{}) {
v := reflect.TypeOf(x)
fmt.Printf("type:%v\n", v)
}
func main() {
var a float32 = 3.14
reflectType(a) // type:float32
var b int64 = 100
reflectType(b) // type:int64
}
在反射中关于类型还划分为两种:类型(Type)和种类(Kind)。因为在Go语言中我们可以使用type关键字构造很多自定义类型,而种类(Kind)就是指底层的类型,但在反射中,当需要区分指针、结构体等大品种的类型时,就会用到种类(Kind)。 举个例子,我们定义了两个指针类型和两个结构体类型,通过反射查看它们的类型和种类。
package main
import (
"fmt"
"reflect"
)
type myInt int64
func reflectType(x interface{}) {
t := reflect.TypeOf(x)
fmt.Printf("type:%v kind:%v\n", t.Name(), t.Kind())
}
func main() {
var a *float32 // 指针
var b myInt // 自定义类型
var c rune // 类型别名
reflectType(a) // type: kind:ptr
reflectType(b) // type:myInt kind:int64
reflectType(c) // type:int32 kind:int32
type person struct {
name string
age int
}
type book struct{ title string }
var d = person{
name: "沙河小王子",
age: 18,
}
var e = book{title: "《跟小王子学Go语言》"}
reflectType(d) // type:person kind:struct
reflectType(e) // type:book kind:struct
}
Go语言的反射中像数组、切片、Map、指针等类型的变量,它们的.Name()都是返回空。
在reflect包中定义的Kind类型如下:
type Kind uint
const (
Invalid Kind = iota // 非法类型
Bool // 布尔型
Int // 有符号整型
Int8 // 有符号8位整型
Int16 // 有符号16位整型
Int32 // 有符号32位整型
Int64 // 有符号64位整型
Uint // 无符号整型
Uint8 // 无符号8位整型
Uint16 // 无符号16位整型
Uint32 // 无符号32位整型
Uint64 // 无符号64位整型
Uintptr // 指针
Float32 // 单精度浮点数
Float64 // 双精度浮点数
Complex64 // 64位复数类型
Complex128 // 128位复数类型
Array // 数组
Chan // 通道
Func // 函数
Interface // 接口
Map // 映射
Ptr // 指针
Slice // 切片
String // 字符串
Struct // 结构体
UnsafePointer // 底层指针
)
ValueOf
reflect.ValueOf()返回的是reflect.Value类型,其中包含了原始值的值信息。reflect.Value与原始值之间可以互相转换。
reflect.Value类型提供的获取原始值的方法如下:
| 方法 | 说明 |
|---|---|
| Interface() interface {} | 将值以 interface{} 类型返回,可以通过类型断言转换为指定类型 |
| Int() int64 | 将值以 int 类型返回,所有有符号整型均可以此方式返回 |
| Uint() uint64 | 将值以 uint 类型返回,所有无符号整型均可以此方式返回 |
| Float() float64 | 将值以双精度(float64)类型返回,所有浮点数(float32、float64)均可以此方式返回 |
| Bool() bool | 将值以 bool 类型返回 |
| Bytes() []bytes | 将值以字节数组 []bytes 类型返回 |
| String() string | 将值以字符串类型返回 |
通过反射获取值,示例:
func reflectValue(x interface{}) {
v := reflect.ValueOf(x)
k := v.Kind()
switch k {
case reflect.Int64:
// v.Int()从反射中获取整型的原始值,然后通过int64()强制类型转换
fmt.Printf("type is int64, value is %d\n", int64(v.Int()))
case reflect.Float32:
// v.Float()从反射中获取浮点型的原始值,然后通过float32()强制类型转换
fmt.Printf("type is float32, value is %f\n", float32(v.Float()))
case reflect.Float64:
// v.Float()从反射中获取浮点型的原始值,然后通过float64()强制类型转换
fmt.Printf("type is float64, value is %f\n", float64(v.Float()))
}
}
func main() {
var a float32 = 3.14
var b int64 = 100
reflectValue(a) // type is float32, value is 3.140000
reflectValue(b) // type is int64, value is 100
// 将int类型的原始值转换为reflect.Value类型
c := reflect.ValueOf(10)
fmt.Printf("type c :%T\n", c) // type c :reflect.Value
}
通过反射设置变量的值,示例:
// 需要注意函数参数传递的是值拷贝,必须传递变量地址才能修改变量值。而反射中使用专有的 Elem() 方法来获取指针对应的值。
package main
import (
"fmt"
"reflect"
)
func reflectSetValue1(x interface{}) {
v := reflect.ValueOf(x)
if v.Kind() == reflect.Int64 {
v.SetInt(200) //修改的是副本,reflect包会引发panic
}
}
func reflectSetValue2(x interface{}) {
v := reflect.ValueOf(x)
// 反射中使用 Elem()方法获取指针对应的值
if v.Elem().Kind() == reflect.Int64 {
v.Elem().SetInt(200)
}
}
func main() {
var a int64 = 100
// reflectSetValue1(a) //panic: reflect: reflect.Value.SetInt using unaddressable value
reflectSetValue2(&a)
fmt.Println(a)
}
isNil() 和 isValid()
isNil()
IsNil() 报告v持有的值是否为nil。v持有的值的分类必须是通道、函数、接口、映射、指针、切片之一;否则IsNil函数会导致panic。
func (v Value) IsNil() bool
isValid()
IsValid()返回v是否持有一个值。如果v是Value零值会返回假,此时v除了IsValid、String、Kind之外的方法都会导致panic。
func (v Value) IsValid() bool
示例:
IsNil()常被用于判断指针是否为空;IsValid()常被用于判定返回值是否有效。
func main() {
// *int类型空指针
var a *int
fmt.Println("var a *int IsNil:", reflect.ValueOf(a).IsNil())
// nil值
fmt.Println("nil IsValid:", reflect.ValueOf(nil).IsValid())
// 实例化一个匿名结构体
b := struct{}{}
// 尝试从结构体中查找"abc"字段
fmt.Println("不存在的结构体成员:", reflect.ValueOf(b).FieldByName("abc").IsValid())
// 尝试从结构体中查找"abc"方法
fmt.Println("不存在的结构体方法:", reflect.ValueOf(b).MethodByName("abc").IsValid())
// map
c := map[string]int{}
// 尝试从map中查找一个不存在的键
fmt.Println("map中不存在的键:", reflect.ValueOf(c).MapIndex(reflect.ValueOf("娜扎")).IsValid())
}
结构体反射
任意值通过reflect.TypeOf()获得反射对象信息后,如果它的类型是结构体,可以通过反射值对象(reflect.Type)的NumField()和Field()方法获得结构体成员的详细信息。
reflect.Type中与获取结构体成员相关的的方法如下表所示。
| 方法 | 说明 |
|---|---|
| Field(i int) StructField | 根据索引,返回索引对应的结构体字段的信息。 |
| NumField() int | 返回结构体成员字段数量。 |
| FieldByName(name string) (StructField, bool) | 根据给定字符串返回字符串对应的结构体字段的信息。 |
| FieldByIndex(index []int) StructField | 多层成员访问时,根据 []int 提供的每个结构体的字段索引,返回字段的信息。 |
| FieldByNameFunc(match func(string) bool) (StructField,bool) | 根据传入的匹配函数匹配需要的字段。 |
| NumMethod() int | 返回该类型的方法集中方法的数目 |
| Method(int) Method | 返回该类型方法集中的第i个方法 |
| MethodByName(string)(Method, bool) | 根据方法名返回该类型方法集中的方法 |
StructField类型用来描述结构体中的一个字段的信息。
type StructField struct {
// Name是字段的名字。PkgPath是非导出字段的包路径,对导出字段该字段为""。
// 参见http://golang.org/ref/spec#Uniqueness_of_identifiers
Name string
PkgPath string
Type Type // 字段的类型
Tag StructTag // 字段的标签
Offset uintptr // 字段在结构体中的字节偏移量
Index []int // 用于Type.FieldByIndex时的索引切片
Anonymous bool // 是否匿名字段
}
当我们使用反射得到一个结构体数据之后可以通过索引依次获取其字段信息,也可以通过字段名去获取指定的字段信息。
type student struct {
Name string `json:"name"`
Score int `json:"score"`
}
func main() {
stu1 := student{
Name: "小王子",
Score: 90,
}
t := reflect.TypeOf(stu1)
fmt.Println(t.Name(), t.Kind()) // student struct
// 通过for循环遍历结构体的所有字段信息
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
fmt.Printf("name:%s index:%d type:%v json tag:%v\n", field.Name, field.Index, field.Type, field.Tag.Get("json"))
}
// 通过字段名获取指定结构体字段信息
if scoreField, ok := t.FieldByName("Score"); ok {
fmt.Printf("name:%s index:%d type:%v json tag:%v\n", scoreField.Name, scoreField.Index, scoreField.Type, scoreField.Tag.Get("json"))
}
}
接下来编写一个函数printMethod(s interface{})来遍历打印s包含的方法。
// 给student添加两个方法 Study和Sleep(注意首字母大写)
func (s student) Study() string {
msg := "好好学习,天天向上。"
fmt.Println(msg)
return msg
}
func (s student) Sleep() string {
msg := "好好睡觉,快快长大。"
fmt.Println(msg)
return msg
}
func printMethod(x interface{}) {
t := reflect.TypeOf(x)
v := reflect.ValueOf(x)
fmt.Println(t.NumMethod())
for i := 0; i < v.NumMethod(); i++ {
methodType := v.Method(i).Type()
fmt.Printf("method name:%s\n", t.Method(i).Name)
fmt.Printf("method:%s\n", methodType)
// 通过反射调用方法传递的参数必须是 []reflect.Value 类型
var args = []reflect.Value{}
v.Method(i).Call(args)
}
}
[!NOTE]
反射是把双刃剑
反射是一个强大并富有表现力的工具,能让我们写出更灵活的代码。但是反射不应该被滥用,原因有以下三个。
- 基于反射的代码是极其脆弱的,反射中的类型错误会在真正运行的时候才会引发panic,那很可能是在代码写完的很长时间之后。
- 大量使用反射的代码通常难以理解。
- 反射的性能低下,基于反射实现的代码通常比正常代码运行速度慢一到两个数量级。

浙公网安备 33010602011771号