开始

Go(又称 Golang)是谷歌 2007 年设计、2009 年开源的静态强类型、编译型、原生并发编程语言,主打简洁、高效、原生高并发,专为解决 C++ 编译缓慢、并发编程复杂、项目依赖管理混乱等开发痛点而生。

应用场景: 后端高并发服务、区块链开发、云原生底层工具。

语言/框架 RPS(每秒请求数) 内存占用 延迟
Go (Gin) 105,000 190 MB 10 ms
Java (Spring) 40,000 1.4 GB 50 ms
Python (FastAPI) 8,000 1.2 GB 150 ms

同样的 Web 服务,Go 的处理能力是 Java 的 2.6 倍、Python 的 13 倍,内存只用了 Java 的 1/7、Python 的 1/6,延迟也最低。

1
2
3
4
5
6
7
8
9
10
# 开发时快速测试代码
go run start.go

# 先编译生成.exe文件 (构建生产版本)
go build start.go
./start.exe

go fmt # 统一代码风格

go test # 进行单元测试

开始的模板:

1
2
3
4
5
6
package main
import "fmt"

func main() {
    fmt.Println("Hello,go!")
}

💡Go语言使用大括号定义代码块,左大括号必须与函数声明在同一行,这是Go的强制代码风格。

输入输出:

1
2
3
4
5
6
7
8
9
10
11
12
var name string 
var age int
// 输入
fmt.Scan(&name, &age)

// 输出
fmt.Print("Hello") // 不换行
fmt.Println("Hello") // 自动换行(最常用)
fmt.Printf("我%d岁\n", 18) // 格式化输出,像C语言

// 常用格式化符号: %d 整数,%s 字符串,%f 小数,%t true/false
// %.2f 保留2位小数

基本语法

变量与常量

◆声明变量
方式 1:完整声明

1
2
// var 变量名 类型 = 值
var a int = 10

方式 2:类型推导

1
2
// var 变量名 = 值
var b = 20 // 编译器会根据初始值自动推断类型,这里是int

方式 3:简短声明

1
2
// 变量名 := 值
c := 30

编译器自动推导类型,代码最简洁。但只能在函数内部使用,包级变量必须用 var 声明。
包级变量 : 写在函数外面、最顶层的变量。
:= 左侧必须是至少一个新变量,不能全是已声明变量(比如 a, b := 10, 20 中,只要有一个新变量就可以)。

◆多变量声明

1
2
var x, y, z int = 1, 2, 3 // 同类型多变量
name, age := "Alice", 25 // 不同类型多变量(类型推导)

Go 会分别推导两个变量的类型(name 为 string,age 为 int),是函数内多变量声明的常用方式。

◆常量声明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// const 常量名 = 值
const Pi = 3.14159
const MaxSize = 100

// 支持批量声明
const (
Monday = 1
Tuesday = 2
Wednesday = 3
)

//支持 iota 枚举
const (
_ = iota // iota 初始为0,这里用_跳过
A
B
C
) // A=1, B=2, C=3,自动递增,适合定义状态码、枚举值

iota:const 里的行号计数器

  • 从 0 开始
  • 每一行 +1
  • 省略写法自动继承 iota
  • 中间插常量,iota 仍然继续计数

◆变量零值
Go 中声明但未赋值的变量会被赋予零值:

  • int/float:0/0.0
  • bool:false
  • string:""
  • 指针 / 切片 / 映射:nil
    nil 是 “引用类型” 的零值,代表没初始化、没指向任何东西。

💡Go 强制要求声明的变量必须被使用,否则编译报错。

变量命名的三个限制条件:

  1. 只能使用52个大小写英文字母、0~9十个阿拉伯数字和一个下划线_;
  2. 变量名不能以数字开头;
  3. 不能使用Go编译器的关键字

基本数据类型

◆整数类型

类型 占用空间 取值范围
int8 1 字节 -128 ~ 127
int16 2 字节 -32768 ~ 32767
int32 4 字节 约 ±21 亿
int64 8 字节 约 ±922 京
int 平台相关 32/64 位(32 位系统是int32,64 位系统是int64)

日常开发优先用 int,不用自己纠结位数,编译器会适配平台。

◆浮点数类型

类型 占用空间 精度
float32 4 字节 约 6-7 位小数
float64 8 字节 约 15 位小数

默认用 float64,精度更高,避免计算误差。

复数也包括两种:
1.complex64,8个字节
2.complex128,16个字节
举例:3 + 4i
实部real()与虚部imag()

◆布尔类型
只有两个取值:true(真)/false(假)。

💡不能用 0/1 代替 false/true,Go 是强类型语言。

◆字符串类型
用双引号包裹,是不可变类型,一旦创建就不能修改单个字符。
Go 里定义多行字符串,必须用反引号包裹,而不是双引号 "。

用双引号写字符串时,\n、\t、\" 这类转义字符会被解析(比如 \n 会变成换行);但用反引号时,所有转义字符都会被当作普通文本保留,不会解析。

也不是没办法修改字符串:先把字符串转换成修改的 rune 切片,修改完成后再转回字符串。

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

func main() {
str := "Hello"

// 1. 转换为 rune 切片(支持中文/Unicode)
runes := []rune(str) // str复制一份,放到一个新的地方(runes), 不是把原来的字符串拿过来改!
// 2. 修改切片中的字符
runes[0] = 'h'
// 3. 再转回字符串
newStr := string(runes)
fmt.Println(newStr) // 输出:hello
}

len(str) 获得字符串的长度(按字节算) (中文占 3 字节)
len(runes) 获得rune的字符长度(按字符个数算)

◆字符类型
Go 字符有两种类型:

类型 本质 用途 取值范围
byte uint8 的别名 表示单个 ASCII 字符(英文字母、数字、基础符号) 0~255
rune int32 的别名 表示单个 Unicode 字符(包括中文、emoji 等) 0~0x10FFFF
1
2
3
4
5
6
7
// 1. 显式声明为 byte 类型 
var c1 byte = 'A'
// 2. 不写类型,让 Go 自动推断
var c2 = 'B'
// 3. 打印变量的类型
fmt.Printf("c1 = %T\n", c1)
fmt.Printf("c2 = %T\n", c2)

输出:
c1 = uint8 // 也就是 byte
c2 = int32 // 也就是 rune

类型转换

Go 是强类型语言:不同类型不能直接运算,必须手动转。
不同类型不能运算

strconv 包

匿名变量

匿名变量:用下划线 _ 代替变量名,用来 “扔掉” 不需要的值。

  • 不用写变量名
  • 不占用内存
  • 不会报 “变量未使用” 错误

什么时候用??
1)函数返回多个值,只要一个,扔掉其他
Go 很多函数返回 (结果,错误)。
只要结果、不想管错误,就用_扔掉错误。
2)多变量赋值,只想要几个、剩下扔掉
3)循环 / 遍历,只要值、不要索引

语句

◆switch语句

Go的switch case匹配成功后自动 break,不会穿透到下一个 case/default。
而像C语言,必须自己写break,否则会继续向下执行。

1
2
3
4
5
6
7
day := 3
switch day {
case 1: fmt.Println("周一")
case 2: fmt.Println("周二")
case 3: fmt.Println("周三")
default: fmt.Println("其他")
}

➢在默认情况下,每个case语句的末尾都隐含有一个break语句。
➢因此匹配成功后就不再检验剩余的case语句和default语句。
➢如果想继续执行后面的case语句,可以使用关键字fallthrough。

1
2
3
4
5
6
7
8
9
10
switch 2 {
case 2:
fmt.Println("2")
fallthrough // 穿透到下一个case
default:
fmt.Println("default")
}
// 输出:
// 2
// default

◆循环语句

1.Go 只有 for,没有 while/do-while。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 标准for
for i := 1; i <= 10; i++ {
fmt.Println(i)
}
// 三种简写方式
// 1. 只有条件
i := 0
for i < 5 {
fmt.Println(i)
i++
}
// 2. 无限循环
for{
fmt.Println("无限循环")
}
// 3. 省略更新
i := 0
for ; i < 5; {
fmt.Println(i)
i++
}

goto是一种 无条件跳转语句。(尽量少用)

1
2
3
Loop:
fmt.Println("Hello")
goto Loop //程序会不断跳回Loop标号位置执行。

goto End可以用于快速跳出多层循环。

break结束当前整个循环,继续外层循环;而 break + label 则是跳出到指定代码块。

1
2
3
4
5
6
7
8
9
10
Outer:
for i := 0; i < 3; i++ {
for j := 0; j < 3; j++ {
if j == 1 {
break Outer // 直接跳出两层
}
fmt.Println(i, j)
}
}
// 只输出:0 0

continue跳过本次循环,进入下一轮;continue + label 则跳到外层循环。

1
2
3
4
5
6
7
8
9
10
11
12
13
Outer:
for i := 1; i <= 3; i++ {
for j := 1; j <= 3; j++ {
if j == 2 {
continue Outer // 直接跳到外层下一次
}
fmt.Println(i, j)
}
}
// 输出:
// 1 1
// 2 1
// 3 1

2.for-range 遍历数组 / 切片 / 字符串 / 映射
for‑range 是 Go 专门用来遍历 “集合类” 数据的语法糖。
示例:

1
2
3
4
5
nums := []int{10, 20, 30} 
for i, v := range nums {
fmt.Println("索引", i, "值", v)
v += 1 // 试图修改v的值
}

需要特别注意:value只是原始数据的副本。
此时nums的值仍为[10,20,30],不会改变。

如何修改真正的值:

1
2
3
4
nums := []int{10, 20, 30}
for i := range nums{
nums[i] += 1 // 通过索引访问原数据
}

函数

基础

注意:Go的 main() 没有返回值,不需要return 0。

1
2
3
4
5
6
7
8
//关键字func 函数名add 传入参数() 返回类型int 
func add(a int, b int) int {
return a + b
}
//Go支持 多返回值
func f4(x float)(y int, z float32){
return 1, 2
}

函数也是一种“类型”。

1
2
fmt.Printf("%T\n", add)
// func(int, int) int

➢形参是实参的一个拷贝,对形参进行修改不会影响实参。
➢但是,如果实参是引用类型,如指针、切片slice、投影map等类型,则实参可能会被形参修改。

1
2
3
4
5
6
7
8
9
10
11
12
// z是*int:int类型的指针参数
func modify(z *int) {
*z = 20 // *z:解引用,修改z指向的内存里的数据
}

func main(){
var x int = 10
fmt.Printf("%d\n", x) // 第一步打印:10
modify(&x) // &x:取x的内存地址,把地址传给形参z
fmt.Printf("%d\n", x) // 第二步打印:20
}
/* 与C语言一致 */

Lambda函数(匿名函数)

没有函数名的函数。

特点:

  • 没有函数名
  • 可以在定义时直接使用
  • 可以赋值给变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 定义并立即调用Lambda函数
func mian(){
func(x int){
fmt.Println(x)
}(10)
}

// 匿名函数被赋值给变量f,通过f()调用
func mian(){
f := func(x int){
fmt.Println(x)
}
f(10)
}

匿名函数是否可复用?依赖变量

闭包

闭包 = 函数 + 被引用的外部变量

数组

数组的声明

数组是一段固定长度的、同类型元素的连续内存空间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 1️⃣声明但不初始化
var arr1 [5]int
fmt.Println(arr1) // [0 0 0 0 0]

// 2️⃣声明并初始化
arr2 := [3]int{1, 2, 3}

// 3️⃣自动推导长度
arr3 := [...]int{1, 2, 3, 4}
fmt.Println(len(arr3)) // 4

// 4️⃣指定索引初始化
arr4 := [5]int{1: 10, 3: 30}
fmt.Println(arr4) // [0 10 0 30 0]

⚠️ 数组长度 [5]是类型的一部分。[5]int和 [10]int是完全不同的类型。

默认值:

  • 数值类型默认值是 0
  • 字符串数组默认值是 ""
  • 布尔数组默认值是 false

...表示让编译器自己数长度。

数组的基本操作

✅ 访问和修改
a[num]:通过下标索引访问。

✅ 遍历数组

1
2
3
4
// for - range 循环
for index, value := range arr {
fmt.Println(index, value)
}

数组是值类型

值类型:复制的是数据本身。
引用类型:复制的是地址 / 描述信息。

1
2
3
4
5
6
a := [3]int{1, 2, 3}
b := a

b[0] = 100
fmt.Println(a) // [1 2 3] !!!
fmt.Println(b) // [100 2 3]

✅ 数组赋值是完整拷贝
✅ 修改副本不会影响原数组

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

a := [3]int{1, 2, 3}
change(a) // 传的是数组的拷贝!!!
fmt.Println(a) // [1 2 3]

❌ 如果数组很大,拷贝成本很高。实际开发中几乎不会直接用数组做参数。✅而是用 切片(slice)。

切片(Slice)

切片的创建

◆切片的三要素
slice {
ptr -> 指向底层数组
len -> 当前元素个数
cap -> 最大可用容量
}

◆切片的最大特点

  • 没有固定长度
  • 动态扩容
  • 引用类型(不是值类型)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 1️⃣从数组或切片生成:左闭右开
arr := [5]int{1, 2, 3, 4, 5}
s := arr[1:4] // [2 3 4]

// 2️⃣使用make(类型, 长度, 容量)
s := make([]int, 3) // len=3, cap=3
s := make([]int, 3, 5) // len=3, cap=5

// 3️⃣字面量创建
s := []int{1, 2, 3}

// 4️⃣空切片/nil切片
var s []int // nil切片(直接声明不初始化 没有{})
s := []int{} // 空切片(已创建但没有数据 {})
类型 JSON 语义
nil null “没有这个字段”
[] [] “有字段,但没数据”

◆多维切片

1
2
3
4
matrix := [][]int{
{1,2,3},
{4,5},
}

切片的基本操作

◆追加元素(append)

1
2
3
s := []int{1, 2}
s = append(s, 3)
s = append(s, 4, 5)

◆截取切片(子切片)

1
2
s := []int{1, 2, 3, 4, 5}
a := s[1:3] // [2 3]

◆直接赋值 与 copy()函数

1
2
3
4
5
6
7
8
9
s := make([]int, 5)
for i :=0; i<5; i++{
s[i] = i
}

t := s // 直接赋值(引用)

c := make([]int, 5)
copy(c, s) // copy函数(复制)

t := s只复制切片头(ptr/len/cap),不复制数据,所以t和s共用同一个底层数组;copy(c, s)会复制真正的数据,c拥有自己的底层数组。

◆删除切片的元素

1
2
3
4
5
6
7
8
9
10
a := []int{10,20,30,40}
i := 1
// 删除头
a = a[i:]

// 删除尾
a = a[:len(a)-1]

// 删除中间
a = append(a[:i], a[i+1:]...)

◆切片遍历 for-range

1
2
3
4
s := []int{2, 1, 3}
for index, value := range s {
fmt.Println("index: %d, value: %d\n", index, value)
}

◆切片的扩容机制
当append超过 cap时:

  1. 申请一块更大的新数组
  2. 拷贝旧数据
  3. 切片指向新数组
对比
数组
切片
长度 固定 动态
类型 [n]T []T
传参 拷贝 引用
底层 自身 数组
使用 很少 极多

总结:
➢切片本质是数组的一段引用。
➢我们可以用make创建,用append增长,用copy控制是否共享,用切片操作调整范围,用range遍历数据。

投影(Map)

1
2
m := make(map[string]int) // 必须make之后才能使用
m["张三"] = 91

map是引用类型。

1
2
3
4
m1 := make(map[string]int)
m2 := m1
m2["a"] = 1
fmt.Println(m1["a"]) // 1

访问不存在的key会返回0。
判断key是否存在?

1
2
3
4
5
6
v, ok := m["xxx"]
if ok {
fmt.Println("存在", v)
}else{
fmt.Println("不存在")
}

删除

1
delete(m, "xxx")

删除不存在的 key:不报错

列表(List)

list 的特点:

  • 任意位置插入
  • 任意位置删除
  • 不需要整体移动数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import "container/list"
lt := list.New() // 空列表 []

lt.PushBack(1) // 尾插 [1]
lt.PushFront(0) // 头插 [0, 1]

// 遍历list:从节点开始,一步一步往前走
for e := lt.Front(); e != nil; e = e.Next() {
fmt.Println(e.Value)
}

// 在尾部插入 4,并返回这个节点的引用
e := lt.PushBack(4) // [0, 1, 4]
lt.InsertBefore(3, e) // [0, 1, 3, 4]
lt.InsertAfter(5, e) // [0, 1, 3, 4, 5]
lt.Remove(e) // [0, 1, 3, 5]

结构体

结构体的定义

1
2
3
4
5
6
7
type Point struct {
X float32
Y float32
}

p := Point{1.2, 2.6} // 值类型
fmt.Println(p.X, p.Y)

另一种创建方式:

1
2
p2 := &Point{} // 指针类型
p3 := new(Point)

类型:*Point(指针类型)
内存:在堆上分配结构体实例,返回指针
访问字段:p2.X、p2.Y(Go 自动解引用)
适用场景:需要修改原始数据、结构体较大、需要共享数据

Go没有传统类(class)
struct + 方法 ≈ 类

匿名结构体与匿名字段

匿名结构体:

1
2
3
4
5
6
7
emp := struct {
Name string
Age int
}{
Name: "Tom",
Age: 30,
}

特点:

  • 不复用
  • 只在当前作用域使用

匿名字段:没有字段名,类型本身就是字段名。

1
2
3
4
5
6
p := Person{
string: "March"
int: 18
}
// 访问: p.string p.int
// 万一有多个string怎么访问?所以这种方法可读性差,慎用。

嵌套结构体

字段提升:把嵌套结构体里的字段,当成外层结构体自己的字段来用。

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

type Person struct {
Name string
address Address // 嵌套
}

p.Address.City = "北京"
p.City = "成都" // 字段提升? ❌

✅ 只有匿名字段才会发生字段提升。

1
2
3
4
5
6
7
8
type Person struct {
Name string
Address // ✅ 匿名字段
}

p := Person{}

p.City = "成都" // ✅ 字段提升成功

结构体与函数

1
2
3
4
5
6
7
8
9
dumpling := Dumpling{
name: "猪肉饺",
reader: func(s string) string {
return "正在读:" + s
},
}

result := dumpling.reader(dumpling.name)
fmt.Println(result)

✅ 结构体可以包含函数。

1
2
3
4
5
6
7
type Task struct {
title string
}

func printTask(t Task) {
fmt.Println(t.title)
}

✅ 结构体可以作为参数传递。
✅ 整个结构体被完整复制一份。
✅ 函数里用的是副本。

问题 后果
结构体很大 复制成本高,性能差
修改结构体 改的是副本,原数据不变

解决方案:传指针。
效果:

  • 只复制地址。
  • 不复制整个结构体。
  • 可以修改原数据。

⭐为什么还能直接用 .访问字段?

1
2
3
func printInfo(emp *Employee) {
fmt.Println(emp.firstName)
}

emp 不是指针吗?
不应该写成 (*emp).firstName吗?

✅ Go 编译器帮你自动解引用。

1
2
3
// 等价写法
(*emp).firstName
emp.firstName // Go自动帮你转

方法 = 绑定在结构体上的函数

接口

接口的实现

不需要写 implements,自动匹配接口。

接口的作用:定义扩展边界、控制代码结构

1
2
3
4
5
6
7
8
9
10
11
12
// Payer 支付接口
// 任何“能付钱”的类型都应该实现这个接口
type Payer interface {
// Pay 执行支付
// amount: 支付金额
// 返回 error,如果支付失败
Pay(amount float64) error

// GetBalance 查询余额
// 返回当前剩余金额
GetBalance() float64
}
代码
含义
type Payer 定义一个新类型
interface 这是一个接口
Pay(amount float64) error 必须有一个“支付方法”
GetBalance() float64 必须有一个“查询余额方法”

1️⃣支付宝实现接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Alipay 支付宝结构体
type Alipay struct {
balance float64 // 账户余额
}

// Pay 实现 Payer 接口
func (a *Alipay) Pay(amount float64) error {
if a.balance < amount {
return fmt.Errorf("余额不足")
}
a.balance -= amount
return nil
}

// GetBalance 实现 Payer 接口
func (a *Alipay) GetBalance() float64 {
return a.balance
}

2️⃣微信支付实现接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// WechatPay 微信支付结构体
type WechatPay struct {
creditLimit float64 // 授信额度(最多能花多少)
used float64 // 已使用额度(已经花了多少)
}

// Pay 实现 Payer 接口
func (w *WechatPay) Pay(amount float64) error {
if w.used+amount > w.creditLimit {
return fmt.Errorf("超出信用额度")
}
w.used += amount
return nil
}

func (w *WechatPay) GetBalance() float64 {
return w.creditLimit - w.used
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 通用结账函数 - 接受任何实现了 Payer 接口的类型
func Checkout(p Payer, amount float64) {
// 这里的 p 是接口类型,Go 会自动去调用它实际对应的那个对象的 Pay 方法
if err := p.Pay(amount); err != nil {
fmt.Printf("支付失败:%v\n", err)
} else {
// 注意这里调用了 p.GetBalance(),同样是多态调用
fmt.Printf("支付成功,余额/可用额度:%.2f\n", p.GetBalance())
}
}

func main() {
alipay := &Alipay{balance: 500}
wechat := &WechatPay{creditLimit: 1000, used: 200}

Checkout(alipay, 100)
Checkout(wechat, 300)
}

❓为什么需要接口
让“变化的部分”不影响“稳定的部分”。Checkout函数不关心具体类型,只依赖接口能力。因此,它可接收 Alipay、 Wechat等未来任何支付方式。(即新增支付方式,不需要修改Checkout)

如果没有接口:

1
2
func CheckoutWithAlipay(a Alipay, amount float64) { ... } 
func CheckoutWithWechat(w WechatPay, amount float64) { ... }

每增加一种支付方式,就要新写一个函数。逻辑重复
且无法统一扩展。

有两个致命问题:

  • 重复代码:支付逻辑写两遍,后面要加云闪付、PayPal,就得再写一遍几乎一样的函数,维护起来像噩梦。
  • 无法单元测试:CheckoutWithAlipay 直接依赖 Alipay 结构体,而 Alipay.Pay 里要连真实的支付宝接口、查余额、走网络请求。而你写单元测试的时候,根本不想真的发请求、真的扣钱,但这个函数里直接写死了用 Alipay,你没法替换成一个 “假的支付器” 来测。

所以,接口的优点:

  • 解耦与抽象
  • 便于测试

🪄AI编程设计
第一步:先想清哪些是稳定的逻辑,哪些是会变化的。
第二步:先设计接口。把“变化的部分”抽象成接口Payer。
第三步:让AI实现:帮我实现一个Alipay、再实现一个WechatPay。
第四步:让AI扩展而不是修改你已有的代码。“新增一个UnionPay,实现这个接口。”而不是“帮我修改Checkout支持银联”。

空接口

Go 语言中空接口 interface{} 的 “万能容器” 特性:

  • 一个空接口变量,可以在运行时先后保存不同类型的值(int、string 等)。
  • 它会自动记录当前保存的值的类型和数据,%T 格式化符可以帮我们看到它的动态类型变化。
1
2
3
4
5
6
7
8
9
10
11
12
package main
import "fmt"

func main() {
var e interface{}
fmt.Println(e) // <nil>

e = 5
fmt.Printf("值:%v,类型:%T\n", e, e) // 值:5,类型:int
e = "中国梦"
fmt.Printf("值:%v,类型:%T\n", e, e) // 值:中国梦,类型:string
}

类型断言

Go规则: 只要结构体实现了接口的所有方法,这个结构体实例可以直接赋值给对应接口类型变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type Person interface { 
sayHello() string
}
type Human struct {
name string
}
// Human 实现 Person 接口
func (h Human) sayHello() string {
return "Hi, I am " + h.name
}
func main() {
var p Person // 变量p的类型是接口
p = Human{"Hui"} // 把结构体实例赋值给接口变量。
// 接口变量 p 存放 Human 实例
// 正常调用接口方法:p.sayHello() 没问题
// 想直接访问 p.name → 编译报错❌ 接口里面没有 name 字段
}

原因:p 的静态类型是 Person(接口),接口只约定方法,没有字段。
解决方案:类型断言—— 从接口变量中还原出原始具体类型。

⭐为什么不直接写p := &Human{"Hui"}?
:= 自动推导类型,p 的 静态类型是 Human(结构体指针),它根本不是接口变量。

类型断言作用: 提取接口变量内部存储的原始具体类型,恢复类型权限(访问结构体独有字段、独有方法)。

(1)不安全写法

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

func main() {
var i interface{} = 42 // 空接口存 int
val := i.(int) // 断言为 int,成功
fmt.Println(val) // 输出:42

// 错误演示:强行断言为 string
// val2 := i.(string)
// 运行直接 panic:interface conversion: interface {} is int, not string
}

如果断言失败(底层不是目标类型),程序直接触发 panic 崩溃。

(2)安全写法

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

func main() {
var i interface{} = 10

// 尝试断言为 string
value, ok := i.(string)
if !ok {
fmt.Println("类型断言错误!") // 执行此行
} else {
fmt.Println(value)
}

// 正确断言为 int
val2 := i.(int)
fmt.Println(val2) // 输出:10
}

特点:失败不崩溃,通过 ok 分支做容错处理。

◆结构体类型断言

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 Person interface {
sayHello() string
}
// 结构体
type Human struct {
name string
age int
}
// 方法
func (h Human) sayHello() string {
return "Hello"
}

func main() {
var p Person = Human{"Tom", 20} //接口变量

// 断言:接口 p 底层是 Human 类型
h, ok := p.(Human)
if ok {
fmt.Println(h.name, h.age) // 还原成功,访问结构体独有字段
} else {
fmt.Println("类型不匹配")
}
}
// 运行结果:Tom 20
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 myInt int
type myString string

// 接口定义
type A interface {
f() string
}

// 为自定义类型实现接口方法
func (x myInt) f() string {
return "myInt: in f()"
}
func (x myString) f() string {
return "myString: in f()"
}

func main() {
var i A
i = myInt(5) // 接口类型变量 绑定myInt类型的值
fmt.Println(i.f()) // 调用i.f()时,会自动调用myInt上的f()方法

i = myString("hello") // 绑定myString类型的值
fmt.Println(i.f()) // 调用i.f()时,会自动调用myString上的f()方法。
}

// ---------运行结果-------
// myInt: in f()
// myString: in f()

自定义类型定义:

  • 基于 Go 原生的 int 和 string,定义了两个新的自定义类型:myInt 和 myString。
  • 它们本质上还是 int 和 string,但属于不同的类型,可以为它们添加方法。

什么是多态?
多态的核心思想是:同一接口(或父类)的变量,在运行时可以表现出不同的行为。

同一个接口变量 i

  • 保存 myInt 时,调用 f() 表现 myInt 的行为
  • 保存 myString 时,调用 f() 表现 myString 的行为

在 Go 中,多态是通过接口 + 隐式实现来实现的,这段代码完全体现了这一点。

排序

系统内置排序

Go 标准库 sort 专门提供排序能力。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main
import (
"fmt"
"sort"
)

func main() {
// 1. int 升序
ints := []int{4, 2, 1, 3}
sort.Ints(ints)
fmt.Println(ints) // [1 2 3 4]

// 2. float64 升序
floats := []float64{1.2, 3.0, 2.1}
sort.Float64s(floats)
fmt.Println(floats) // [1.2 2.1 3]

ints := []int{4, 2, 1, 3}
// 整数降序
sort.Sort(sort.Reverse(sort.IntSlice(ints)))
fmt.Println(ints) // [4 3 2 1]
}
  • sort.IntSlice(切片):把普通切片(这里是[]int切片)包装成实现排序接口的类型。
  • sort.Reverse():反转比较规则(不是翻转数组),并且它接收的和返回的都是接口类型。
  • sort.Sort():要求参数 必须实现 sort.Interface 接口。原地排序,直接修改传入的集合,无返回值。

自定义排序

如果需要复杂规则(如按字符串长度、自定义结构体),需要让类型实现 sort.Interface 接口,该接口包含 3 个方法:

  1. Len() int:返回元素个数。
  2. Swap(i, j int):交换下标 i、j 元素。
  3. Less(i, j int) bool:核心,判断 i 是否应该排在 j 前面。
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"
"sort"
}
// 将[]string重定义为自定义类型
type ByLength []string

func (s ByLength) Len() int{ // 实现方法len()
return len(s)
}
func (s ByLength) Swap(i, j int){ // 实现方法Swap()
s[i], s[j] = s[j], s[i]
}
func (s ByLength) Less(i, j int) bool { // 实现方法less()
return len(s[i]) < len(s[j])
}

func main() {
words := ByLength{"a", "hello", "hi", "world"}
sort.Sort(words)
fmt.Println(words) // [a hi hello world] 按长度升序
}