Go语言
开始
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 | # 开发时快速测试代码 |
开始的模板:
1 | package main |
💡Go语言使用大括号定义代码块,左大括号必须与函数声明在同一行,这是Go的强制代码风格。
输入输出:
1 | var name string |
基本语法
变量与常量
◆声明变量
方式 1:完整声明
1 | // var 变量名 类型 = 值 |
方式 2:类型推导
1 | // var 变量名 = 值 |
方式 3:简短声明
1 | // 变量名 := 值 |
编译器自动推导类型,代码最简洁。但只能在函数内部使用,包级变量必须用 var 声明。
包级变量 : 写在函数外面、最顶层的变量。:= 左侧必须是至少一个新变量,不能全是已声明变量(比如 a, b := 10, 20 中,只要有一个新变量就可以)。
◆多变量声明
1 | var x, y, z int = 1, 2, 3 // 同类型多变量 |
Go 会分别推导两个变量的类型(name 为 string,age 为 int),是函数内多变量声明的常用方式。
◆常量声明
1 | // const 常量名 = 值 |
iota:const 里的行号计数器
- 从 0 开始
- 每一行 +1
- 省略写法自动继承
iota - 中间插常量,iota 仍然继续计数
◆变量零值
Go 中声明但未赋值的变量会被赋予零值:
int/float:0/0.0bool:falsestring:""- 指针 / 切片 / 映射:
nil
nil 是 “引用类型” 的零值,代表没初始化、没指向任何东西。
💡Go 强制要求声明的变量必须被使用,否则编译报错。
变量命名的三个限制条件:
- 只能使用52个大小写英文字母、0~9十个阿拉伯数字和一个下划线
_; - 变量名不能以数字开头;
- 不能使用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 | package main |
len(str) 获得字符串的长度(按字节算) (中文占 3 字节)len(runes) 获得rune的字符长度(按字符个数算)
◆字符类型
Go 字符有两种类型:
| 类型 | 本质 | 用途 | 取值范围 |
|---|---|---|---|
byte |
uint8 的别名 |
表示单个 ASCII 字符(英文字母、数字、基础符号) | 0~255 |
rune |
int32 的别名 |
表示单个 Unicode 字符(包括中文、emoji 等) | 0~0x10FFFF |
1 | // 1. 显式声明为 byte 类型 |
输出:
c1 = uint8 // 也就是 byte
c2 = int32 // 也就是 rune
类型转换
Go 是强类型语言:不同类型不能直接运算,必须手动转。
不同类型不能运算
strconv 包
匿名变量
匿名变量:用下划线 _ 代替变量名,用来 “扔掉” 不需要的值。
- 不用写变量名
- 不占用内存
- 不会报 “变量未使用” 错误
什么时候用??
1)函数返回多个值,只要一个,扔掉其他
Go 很多函数返回 (结果,错误)。
只要结果、不想管错误,就用_扔掉错误。
2)多变量赋值,只想要几个、剩下扔掉
3)循环 / 遍历,只要值、不要索引
语句
◆switch语句
Go的switch case匹配成功后自动 break,不会穿透到下一个 case/default。
而像C语言,必须自己写break,否则会继续向下执行。
1 | day := 3 |
➢在默认情况下,每个case语句的末尾都隐含有一个break语句。
➢因此匹配成功后就不再检验剩余的case语句和default语句。
➢如果想继续执行后面的case语句,可以使用关键字fallthrough。
1 | switch 2 { |
◆循环语句
1.Go 只有 for,没有 while/do-while。
1 | // 标准for |
goto是一种 无条件跳转语句。(尽量少用)
1 | Loop: |
goto End可以用于快速跳出多层循环。
break结束当前整个循环,继续外层循环;而 break + label 则是跳出到指定代码块。
1 | Outer: |
continue跳过本次循环,进入下一轮;continue + label 则跳到外层循环。
1 | Outer: |
2.for-range 遍历数组 / 切片 / 字符串 / 映射
for‑range 是 Go 专门用来遍历 “集合类” 数据的语法糖。
示例:
1 | nums := []int{10, 20, 30} |
需要特别注意:value只是原始数据的副本。
此时nums的值仍为[10,20,30],不会改变。
如何修改真正的值:
1 | nums := []int{10, 20, 30} |
函数
基础
注意:Go的 main() 没有返回值,不需要return 0。
1 | //关键字func 函数名add 传入参数() 返回类型int |
函数也是一种“类型”。
1 | fmt.Printf("%T\n", add) |
➢形参是实参的一个拷贝,对形参进行修改不会影响实参。
➢但是,如果实参是引用类型,如指针、切片slice、投影map等类型,则实参可能会被形参修改。
1 | // z是*int:int类型的指针参数 |
Lambda函数(匿名函数)
没有函数名的函数。
特点:
- 没有函数名
- 可以在定义时直接使用
- 可以赋值给变量
1 | // 定义并立即调用Lambda函数 |
匿名函数是否可复用?依赖变量
闭包
闭包 = 函数 + 被引用的外部变量
数组
数组的声明
数组是一段固定长度的、同类型元素的连续内存空间。
1 | // 1️⃣声明但不初始化 |
⚠️ 数组长度 [5]是类型的一部分。[5]int和 [10]int是完全不同的类型。
默认值:
- 数值类型默认值是
0 - 字符串数组默认值是
"" - 布尔数组默认值是
false
...表示让编译器自己数长度。
数组的基本操作
✅ 访问和修改a[num]:通过下标索引访问。
✅ 遍历数组
1 | // for - range 循环 |
数组是值类型
值类型:复制的是数据本身。
引用类型:复制的是地址 / 描述信息。
1 | a := [3]int{1, 2, 3} |
✅ 数组赋值是完整拷贝
✅ 修改副本不会影响原数组
1 | func change(arr [3]int) { |
❌ 如果数组很大,拷贝成本很高。实际开发中几乎不会直接用数组做参数。✅而是用 切片(slice)。
切片(Slice)
切片的创建
◆切片的三要素
slice {
ptr -> 指向底层数组
len -> 当前元素个数
cap -> 最大可用容量
}
◆切片的最大特点
- 没有固定长度
- 动态扩容
- 引用类型(不是值类型)
1 | // 1️⃣从数组或切片生成:左闭右开 |
| 类型 | JSON | 语义 |
|---|---|---|
| nil | null | “没有这个字段” |
| [] | [] | “有字段,但没数据” |
◆多维切片
1 | matrix := [][]int{ |
切片的基本操作
◆追加元素(append)
1 | s := []int{1, 2} |
◆截取切片(子切片)
1 | s := []int{1, 2, 3, 4, 5} |
◆直接赋值 与 copy()函数
1 | s := make([]int, 5) |
t := s只复制切片头(ptr/len/cap),不复制数据,所以t和s共用同一个底层数组;copy(c, s)会复制真正的数据,c拥有自己的底层数组。
◆删除切片的元素
1 | a := []int{10,20,30,40} |
◆切片遍历 for-range
1 | s := []int{2, 1, 3} |
◆切片的扩容机制
当append超过 cap时:
- 申请一块更大的新数组
- 拷贝旧数据
- 切片指向新数组
| 对比 |
数组 |
切片 |
|---|---|---|
| 长度 | 固定 | 动态 |
| 类型 | [n]T |
[]T |
| 传参 | 拷贝 | 引用 |
| 底层 | 自身 | 数组 |
| 使用 | 很少 | 极多 |
总结:
➢切片本质是数组的一段引用。
➢我们可以用make创建,用append增长,用copy控制是否共享,用切片操作调整范围,用range遍历数据。
投影(Map)
1 | m := make(map[string]int) // 必须make之后才能使用 |
map是引用类型。
1 | m1 := make(map[string]int) |
访问不存在的key会返回0。
判断key是否存在?
1 | v, ok := m["xxx"] |
删除
1 | delete(m, "xxx") |
删除不存在的 key:不报错
列表(List)
list 的特点:
- 任意位置插入
- 任意位置删除
- 不需要整体移动数据
1 | import "container/list" |
结构体
结构体的定义
1 | type Point struct { |
另一种创建方式:
1 | p2 := &Point{} // 指针类型 |
类型:*Point(指针类型)
内存:在堆上分配结构体实例,返回指针
访问字段:p2.X、p2.Y(Go 自动解引用)
适用场景:需要修改原始数据、结构体较大、需要共享数据
Go没有传统类(class)
struct + 方法 ≈ 类
匿名结构体与匿名字段
匿名结构体:
1 | emp := struct { |
特点:
- 不复用
- 只在当前作用域使用
匿名字段:没有字段名,类型本身就是字段名。
1 | p := Person{ |
嵌套结构体
字段提升:把嵌套结构体里的字段,当成外层结构体自己的字段来用。
1 | type Address struct { |
✅ 只有匿名字段才会发生字段提升。
1 | type Person struct { |
结构体与函数
1 | dumpling := Dumpling{ |
✅ 结构体可以包含函数。
1 | type Task struct { |
✅ 结构体可以作为参数传递。
✅ 整个结构体被完整复制一份。
✅ 函数里用的是副本。
| 问题 | 后果 |
|---|---|
| 结构体很大 | 复制成本高,性能差 |
| 修改结构体 | 改的是副本,原数据不变 |
解决方案:传指针。
效果:
- 只复制地址。
- 不复制整个结构体。
- 可以修改原数据。
⭐为什么还能直接用 .访问字段?
1 | func printInfo(emp *Employee) { |
emp 不是指针吗?
不应该写成(*emp).firstName吗?
✅ Go 编译器帮你自动解引用。
1 | // 等价写法 |
方法 = 绑定在结构体上的函数
接口
接口的实现
不需要写 implements,自动匹配接口。
接口的作用:定义扩展边界、控制代码结构
1 | // Payer 支付接口 |
| 代码 |
含义 |
|---|---|
type Payer |
定义一个新类型 |
interface |
这是一个接口 |
Pay(amount float64) error |
必须有一个“支付方法” |
GetBalance() float64 |
必须有一个“查询余额方法” |
1️⃣支付宝实现接口
1 | // Alipay 支付宝结构体 |
2️⃣微信支付实现接口
1 | // WechatPay 微信支付结构体 |
1 | // 通用结账函数 - 接受任何实现了 Payer 接口的类型 |
❓为什么需要接口
让“变化的部分”不影响“稳定的部分”。Checkout函数不关心具体类型,只依赖接口能力。因此,它可接收 Alipay、 Wechat等未来任何支付方式。(即新增支付方式,不需要修改Checkout)
如果没有接口:
1 | func CheckoutWithAlipay(a Alipay, amount float64) { ... } |
每增加一种支付方式,就要新写一个函数。逻辑重复
且无法统一扩展。
有两个致命问题:
- 重复代码:支付逻辑写两遍,后面要加云闪付、PayPal,就得再写一遍几乎一样的函数,维护起来像噩梦。
- 无法单元测试:
CheckoutWithAlipay直接依赖Alipay结构体,而Alipay.Pay里要连真实的支付宝接口、查余额、走网络请求。而你写单元测试的时候,根本不想真的发请求、真的扣钱,但这个函数里直接写死了用Alipay,你没法替换成一个 “假的支付器” 来测。
所以,接口的优点:
- 解耦与抽象
- 便于测试
🪄AI编程设计
第一步:先想清哪些是稳定的逻辑,哪些是会变化的。
第二步:先设计接口。把“变化的部分”抽象成接口Payer。
第三步:让AI实现:帮我实现一个Alipay、再实现一个WechatPay。
第四步:让AI扩展而不是修改你已有的代码。“新增一个UnionPay,实现这个接口。”而不是“帮我修改Checkout支持银联”。
空接口
Go 语言中空接口 interface{} 的 “万能容器” 特性:
- 一个空接口变量,可以在运行时先后保存不同类型的值(int、string 等)。
- 它会自动记录当前保存的值的类型和数据,
%T格式化符可以帮我们看到它的动态类型变化。
1 | package main |
类型断言
Go规则: 只要结构体实现了接口的所有方法,这个结构体实例可以直接赋值给对应接口类型变量。
1 | type Person interface { |
原因:p 的静态类型是 Person(接口),接口只约定方法,没有字段。
解决方案:类型断言—— 从接口变量中还原出原始具体类型。
⭐为什么不直接写p := &Human{"Hui"}?:= 自动推导类型,p 的 静态类型是 Human(结构体指针),它根本不是接口变量。
类型断言作用: 提取接口变量内部存储的原始具体类型,恢复类型权限(访问结构体独有字段、独有方法)。
(1)不安全写法
1 | package main |
如果断言失败(底层不是目标类型),程序直接触发 panic 崩溃。
(2)安全写法
1 | package main |
特点:失败不崩溃,通过 ok 分支做容错处理。
◆结构体类型断言
1 | package main |
1 | package main |
自定义类型定义:
- 基于 Go 原生的
int和string,定义了两个新的自定义类型:myInt和myString。 - 它们本质上还是
int和string,但属于不同的类型,可以为它们添加方法。
什么是多态?
多态的核心思想是:同一接口(或父类)的变量,在运行时可以表现出不同的行为。
同一个接口变量 i
- 保存
myInt时,调用f()表现myInt的行为 - 保存
myString时,调用f()表现myString的行为
在 Go 中,多态是通过接口 + 隐式实现来实现的,这段代码完全体现了这一点。
排序
系统内置排序
Go 标准库 sort 专门提供排序能力。
1 | package main |
sort.IntSlice(切片):把普通切片(这里是[]int切片)包装成实现排序接口的类型。sort.Reverse():反转比较规则(不是翻转数组),并且它接收的和返回的都是接口类型。sort.Sort():要求参数 必须实现sort.Interface接口。原地排序,直接修改传入的集合,无返回值。
自定义排序
如果需要复杂规则(如按字符串长度、自定义结构体),需要让类型实现 sort.Interface 接口,该接口包含 3 个方法:
Len() int:返回元素个数。Swap(i, j int):交换下标 i、j 元素。Less(i, j int) bool:核心,判断i是否应该排在j前面。
1 | package main |



