数据类型
数据类型大致分为:布尔型、数字类型、字符串类型、派生
布尔
类型:bool
类型的值只能为 true 或者 false
数字类型
整型
| 类型 | 信息 | 范围 |
|---|---|---|
uint8 | 无符号 8 位整型 | 0 ~ 255 |
uint16 | 无符号 16 位整型 | 0 ~ 65535 |
uint32 | 无符号 32 位整型 | 0 ~ 4294967295 |
uint64 | 无符号 64 位整型 | 0 ~ 18446744073709551615 |
int8 | 有符号 8 位整型 | -128 ~ 127 |
int16 | 有符号 16 位整型 | -32768 ~ 32767 |
int32 | 有符号 32 位整型 | -2147483648 ~ 2147483647 |
int64 | 有符号 64 位整型 | -9223372036854775808 ~ 9223372036854775807 |
浮点/复数型
| 类型 | 信息 |
|---|---|
float32 | IEEE-754 32位浮点型数 |
float64 | IEEE-754 64位浮点型数 |
complex64 | 32 位实数和虚数 |
complex128 | 64 位实数和虚数 |
复数是将两个不同维度的数打包成
a+bi形式的组合数,a 是实部、b 是虚部,i 是虚部标记且人为规定i²=-1(解决负数开平方无解问题);Go中用complex(实部,虚部)创建复数,real()/imag()提取实虚部,多用于科学计算/信号处理
其他类型
| 类型 | 信息 |
|---|---|
byte | 类似 uint8 |
rune | 类似 int32 |
uint | 32 或 64 位 |
int | 与 uint 一样大小 |
uintptr | 无符号整型,用于存放指针 |
字符串
类型:string
字符串就是一串固定长度的字符连接起来的字符序列;Go 的字符串是由单个字节连接起来的;Go 语言的字符串的字节使用 UTF-8 编码标识 Unicode 文本
派生
指针类型
指针类型(Pointer)是存储另一个变量内存地址的类型,通过指针可间接访问和修改目标变量的值,通过 & 和 * 进行操作
&:取地址符,用于获取变量的内存地址,是定义指针时关联目标变量的核心符号
*:取值符,通过指针存储的内存地址获取或修改目标变量的值,是操作指针指向数据的核心符号
package main
import "fmt"
func main() {
// 基础使用
num := 10
var p *int = &num
fmt.Println("变量值:", num) // 输出 10
fmt.Println("变量地址:", &num) // 输出num的内存地址
fmt.Println("指针存储的地址:", p) // 输出与&num一致的地址
fmt.Println("通过指针取值:", *p) // 输出 10
*p = 20
fmt.Println("修改后变量值:", num) // 输出 20
// 函数传参场景
a := 10
changeValue(a)
fmt.Println("传值后:", a) // 输出 10(原变量未变)
changeValueByPtr(&a)
fmt.Println("传指针后:", a) // 输出 100(原变量被修改)
}
// 传值方式:修改的是副本,原变量不变
func changeValue(num int) {
num = 100
}
// 传指针方式:直接修改原变量
func changeValueByPtr(num *int) {
*num = 100
}多级指针
使用 * 的可以成为指针,但是 * 不是只能有一个。如:二级指针(**T)、三级指针(***T)、N级指针([*n]T)
本质上多级指针就是 指向指针的指针 核心作用就是 间接操作目标变量的地址
import "fmt"
func main() {
// 目标变量
var a int = 100
// 一级指针:存储a的地址
var p1 *int = &a
// 二级指针:存储p1的地址
var p2 **int = &p1
fmt.Println("---------- 地址与值 ----------")
fmt.Printf("a的值:%d,a的地址:%p\n", a, &a) // 100, 0xc000016088
fmt.Printf("p1的值:%p,p1的地址:%p\n", p1, &p1) // 0xc000016088, 0xc00000e030
fmt.Printf("p2的值:%p,p2的地址:%p\n", p2, &p2) // 0xc00000e030, 0xc00000e038
fmt.Println("---------- 解引用 ----------")
fmt.Println("*p2(解引用p2,得到p1的值):", *p2) // 0xc000016088(等价于p1)
fmt.Println("**p2(解引用两次,得到a的值):", **p2) // 100(等价于a)
// 通过二级指针修改a的值
**p2 = 200
fmt.Println("修改后a的值:", a) // 200
}数组类型
数组是一组长度固定并且相同数据类型的一组有序元素的组合
语法:var 数组名称 [数组大小]数组类型
// 先声明后使用
var number [3]int
number[0] = 10
number[1] = 20
number[2] = 30初始化
直接在数组中初始化的时候,如果不确定数组的长度可以用在 数组大小 的位置直接使用 ... 来进行替代,编译器会自动根据元素的个数来推断数组的大小
// 声明的时候直接初始化
var number = [3]int{10, 20, 30}在初始化的时候是可以根据下表来让指定位置的元素初始化
// 将索引为 1、3、4 的元素初始化
balance := [5]int{
1: 2,
3: 7,
4: 9,
}在访问使用的时候以 数组名[下标] 格式使用
多维数组
语法:var 数组名称 [一维大小][二维大小][n维大小]数组类型
可以把一维数组是一个直线,二维数组是有了横竖两条线形成了一个坐标系
切片类型
基于数组实现,但是解决了数组长度固定的缺点。他不是独立的数据结构,是对底层数组的引用。包含三个核心参数
| 属性 | 作用 |
|---|---|
| 指针(ptr) | 指向底层数组的起始位置 |
| 长度(len) | 切片中当前可用元素的个数(len(s) 获取) |
| 容量(cap) | 切片从起始指针到底层数组末尾的元素个数(cap(s) 获取,≥ len) |
扩容机制
切片的动态本质就是扩容,当 append 导致 len > cap 时,Go 会自动创建新的底层数组,并将原数组数据拷贝到新数组,然后返回指向新数组的切片
扩容规则(Go 1.18+)
- 如果新容量 ≤ 256:扩容后容量 = 原容量 × 2
- 如果新容量 > 256:扩容后容量 = 原容量 × 1.25(逐步降低扩容倍数,避免内存浪费)
- 最终容量会向上取整到 “内存对齐” 的大小(比如 8/16/32…,不同平台略有差异)
创建方式
基于数组创建
arr := [5]int{10, 20, 30, 40, 50}
s1 := arr[1:3] // 左闭右开,取索引1、2 → [20,30]
s2 := arr[:3] // 省略左边界,从0开始 → [10,20,30]
s3 := arr[2:] // 省略右边界,到数组末尾 → [30,40,50]
s4 := arr[:] // 取整个数组 → [10,20,30,40,50]
// [20 30] 2 4(cap从索引1到数组末尾共4个元素)
fmt.Println(s1, len(s1), cap(s1))字面量创建
s := []int{1, 2, 3, 4} // 注意:没有长度 → 是切片,不是数组
fmt.Println(s, len(s), cap(s)) // [1 2 3 4] 4 4make函数创建(指定长度/容量)
s1 := make([]int, 3) // len=3, cap=3,初始值为0 → [0 0 0]
s2 := make([]int, 3, 5) // len=3, cap=5,初始值为0 → [0 0 0]
fmt.Println(s6, len(s1), cap(s1)) // [0 0 0] 3 3
fmt.Println(s7, len(s2), cap(s2)) // [0 0 0] 3 5操作
查询与修改和操作普通数组一致,都是通过下标来获取和修改
var s []int = make([]int, 1)
s[0] = 1 // 修改
println(s[0]) // 查询添加元素:append函数(切片动态扩容的核心)
s := make([]int, 3, 5) // len=3, cap=5 → [0,0,0]
// 添加1个元素:len<cap,直接追加,底层数组不变
s1 := append(s, 1)
fmt.Println(s1, len(s1), cap(s1)) // [0 0 0 1] 4 5
fmt.Printf("s指针:%p,s1指针:%p\n", &s[0], &s1[0]) // 指针相同(同一底层数组)
fmt.Printf("s和s1长度:%d vs %d,容量:%d vs %d\n", len(s), len(s1), cap(s), cap(s1))
// 添加多个元素:len=cap时,触发扩容(新建底层数组)
s2 := append(s1, 2, 3, 4) // 需要len=7 > cap=5
fmt.Println(s2, len(s2), cap(s2)) // [0 0 0 1 2 3 4] 7 10
fmt.Printf("s1指针:%p,s2指针:%p\n", &s1[0], &s2[0]) // 指针不同(不同底层数组)
// 追加另一个切片(...展开)
s3 := []int{5, 6}
s4 := append(s2, s3...)
fmt.Println(s4) // [0 0 0 1 2 3 4 5 6]删除元素:只能通过 append 的拼接进行删除
坑
切片是引用类型,修改会影响原切片 / 底层数组
s1 := []int{1,2,3}
s2 := s1 // 引用同一个底层数组
s2[0] = 100
fmt.Println(s1) // [100 2 3] → s1被修改切片扩容后,原切片和新切片指向不同数组(即 append 返回的切片和原本的切片)
s1 := make([]int, 3, 3) // len=3, cap=3
s2 := append(s1, 1) // 触发扩容,指向新数组
s2[0] = 100
fmt.Println(s1) // [0 0 0] → s1未被修改(数组不同)浅拷贝问题
// 嵌套切片的浅拷贝
s1 := [][]int{{1,2}, {3,4}}
s2 := make([][]int, len(s1))
copy(s2, s1) // copy仅拷贝外层切片,内层切片仍引用同一数组
s2[0][0] = 100
fmt.Println(s1) // [[100 2] [3 4]] → s1被修改
// 解决:深拷贝(手动拷贝内层)
s3 := make([][]int, len(s1))
for i := range s1 {
s3[i] = make([]int, len(s1[i]))
copy(s3[i], s1[i])
}
s3[0][0] = 200
fmt.Println(s1) // [[100 2] [3 4]] → s1未被修改空切片和nil切片
var s1 []int // nil切片:ptr=nil, len=0, cap=0
s2 := []int{} // 空切片:ptr指向空数组, len=0, cap=0
s3 := make([]int, 0) // 空切片:同上
fmt.Println(s1 == nil) // true
fmt.Println(s2 == nil) // false
fmt.Println(len(s1) == 0) // true
fmt.Println(len(s2) == 0) // true结构化类型
结构体(Struct)是核心的复合数据类型,也是实现面向对象编程(封装、组合)的基础
type 结构体名 struct {
字段名1 字段类型1
字段名2 字段类型2
// ...
}初始化
// 学生结构体
type Student struct {
Name string
Age int
Gender string
}字面量初始化(按字段顺序)
p := Student{"张三", 20, "男"}
fmt.Println(p) // 输出:{张三 20 男}键值对初始化
// 全字段赋值
p1 := Student{
Name: "李四",
Age: 25,
Gender: "男",
}
fmt.Println(p1) // 输出:{李四 25 男}
// 部分字段赋值(未指定的字段为零值)
p2 := Student{
Name: "王五",
Age: 30,
}
fmt.Println(p2) // 输出:{王五 30 }new 函数初始化(返回结构体指针)
p3 := new(Student) // p3的类型是 *Student
p3.Name = "赵六" // 指针访问字段用 .(Go简化了语法)
p3.Age = 35
fmt.Println(*p3) // 解引用输出:{赵六 35 }
fmt.Println(p3) // 直接输出指针地址:&{赵六 35 }访问修改字段时:
- 普通结构体变量:
变量名.字段名 - 结构体指针变量:
指针变量.字段名(Go 自动解引用,无需(*指针).字段名)
直接声明使用
// 写法一
var p Student
// 写法二
p := Student{}这两种写方法本质上是一致的,因为结构体的零值不是 nil(结构体指针除外)。在声明变量的时候编译器就会为结构体中的每一个属性设置上对应的零值
type Cat struct {
Name string
Age int
}
func main() {
var c Cat // 默认是 {""(空字符串)0}
c.Name = "小白"
c.Age = 2
}继承
结构体之间可以通过匿名嵌套来实现类似于继承的效果
// 用户结构体
type User struct {
Name string
Age int
}
// 学生结构体
type Student struct {
User // 匿名字段体,直接嵌入User字段体
Gender string
}
func main() {
s := Student{
User: User{
Name: "张三",
Age: 20,
},
Gender: "男",
}
fmt.Println(s.Name) // 直接访问User字段体的Name字段
fmt.Println(s.Age)
}而如果所谓的父类存在的字段而子类又有一个同名字段的时候,此时就是以子类的为主(不管是否初始化)
// 学生结构体
type Student struct {
User // 匿名字段体,直接嵌入User字段体
Name string
Gender string
}
func main() {
s := Student{
User: User{
Name: "张三",
Age: 20,
},
Name: "李四",
Gender: "男",
}
fmt.Println(s.Name) // 李四
}Tag
结构体 Tag 是附着在结构体字段上的键值对格式的元数据它不是字段的值,也不影响字段本身的存储,但是程序可以通过反射(reflect)读取这些元数据,实现自定义逻辑。如:
type User struct {
Name string `json:"username" yaml:"name"` // 反引号里的内容就是Tag
Age int `json:"age,string,omitempty" yaml:"age"`
}
func main() {
t := reflect.TypeOf(User{})
for i := 0; i < t.NumField(); i++ {
field := t.Field(i) // 获取第i个字段
fmt.Printf("字段名:%s\n", field.Name)
fmt.Printf(" JSON Tag:%s\n", field.Tag.Get("json")) // 读取json键的Tag
fmt.Printf(" YAML Tag:%s\n", field.Tag.Get("yaml")) // 读取yaml键的Tag
fmt.Println("-----")
}
}
/* --------- 结果 ---------
字段名:Name
JSON Tag:username
YAML Tag:name
-----
字段名:Age
JSON Tag:age,string,omitempty
YAML Tag:age
-----
*/规则:
- 包裹符号:必须用反引号(`),不能用双引号(双引号会被解析为字符串,破坏 Tag 格式)
- 键值对分隔:多个键值对用空格分隔,一个键的多个值用逗号分隔(无空格)
- 字段可见性:只有首字母大写的字段,Tag 才能被反射读取(小写字段对外不可见)
JSON序列化
结构体的字段名首字母大写才会被序列化
// 学生结构体
type Student struct {
Name string `json:"name"` // json tag:序列化后的字段名
Age int `json:"age"`
Gender string `json:"gender,omitempty"` // omitempty:如果字段值为空,不序列化该字段
}
func main() {
// 序列化:结构体转JSON
s := Student{"李四", 20, "男"}
b, _ := json.Marshal(s)
fmt.Println(string(b))
// 反序列化:JSON转结构体
jsonStr := `{"name":"李四","age":20,"gender":"男"}`
var s2 Student
_ = json.Unmarshal([]byte(jsonStr), &s2)
fmt.Println(s2)
}| 选项 | 作用 | 示例 |
|---|---|---|
| 字段重命名 | 序列化 / 反序列化时,将结构体字段名映射为指定的 JSON 字段名 | json:"user_name" |
- | 忽略该字段(序列化 / 反序列化都不会处理) | json:"-" |
string | 将数值类型(int/float)序列化为 JSON 字符串,反序列化时可从字符串转回数值 | json:"age,string" |
omitempty | 字段为零值(0/"" /nil/false 等)时,序列化时忽略该字段 | json:"email,omitempty" |
| 组合使用 | 多个选项用逗号分隔(无空格) | json:"id,string,omitempty" |
接口类型
接口(interface)用于定义行为的集合,描述了类型必须实现的方法,规定了类型的行为契约。任何其他类型只要实现了这些方法就是实现了这个接口
接口可以将不同的类型绑定到一组公共的方法上,从而实现多态和灵活的设计
接口的零值是 nil,定义方式:type 接口名 interface {}
接口中只能放方法签名
特点
隐式实现:只要一个类型实现了接口要求的所有方法,该类型就自动被认为实现了该接口。没有关键字去显式的声明
// 定义接口:规定一个方法 Speak()
type Animal interface {
Speak() string
}
// 定义狗结构体
type Dog struct{}
// 实现狗的 Speak() 方法
func (d Dog) Speak() string {
return "汪汪汪"
}
// 定义猫结构体
type Cat struct{}
// 实现猫的 Speak() 方法
func (c Cat) Speak() string {
return "喵喵喵"
}
func main() {
var d Animal = Dog{}
Speak(d)
c := Cat{}
Speak(c)
}
// 定义一个函数,参数为 Animal 类型,调用参数的 Speak() 方法
func Speak(a Animal) {
println(a.Speak())
}接口类型变量:接口变量可以存储实现该接口的任意值,实际上包含了两个部分:
- 动态类型:存储实际的值类型
- 动态值:存储具体的值
空接口
空接口:interface {}
空接口等于没有规定任何方法,所以所有类型都实现了空接口。空接口可以装任何数据,类似于Java中的Object类
var a interface{}
a = 10
a = "字符串"
a = 3.14
a = Dog{}组合
接口可以通过嵌套组合,实现更复杂的行为描述
type Reader interface {
Read() string
}
type Writer interface {
Write(data string)
}
type ReadWriter interface {
Reader
Writer
}
type File struct{}
func (f File) Read() string {
return "读取数据"
}
func (f File) Write(data string) {
fmt.Println("写入数据:", data)
}
func main() {
var rw ReadWriter = File{}
fmt.Println(rw.Read())
rw.Write("Hello, World!")
}Map 类型
Map 是一种无序的引用类型键值对的集合,可以像迭代数组和切片一样迭代(Map 是无序的,遍历 Map 返回的键值对的顺序是不确定的)
它是通过 key 来快速检索数据,在获取 Map 的值时,如果键不存在,返回该类型的零值
定义 Map 并初始化:
// 使用 map 关键字
var 变量名 map[key类型]value类型{
K1: V1,
K2: V2,
K3: V3,
}
// 使用 make 函数
变量名 := make(map[key类型]value类型, [初始容量])增删改查:
var m map[string]int = make(map[string]int)
// 添加/修改(如果 Key 不存在添加,存在则修改)
m["k1"] = 1
// 删除
delete(m, "k1")
// 查询
value, exists := m["k1"]在查询的时候会返回两个参数,一个是 key 对应的值,一个是这个值是否存在(布尔类型)。因为如果第一个数据返回的是个 0 值但是无法判断本来就是 0 值还是因为 key 没找到返回的,所以需要第二个值参与判断
遍历 Map:
for k, v := range m {
println(k, v)
}map 类型必须得被初始化,对于没有初始化的map值为
nil,是不能存数据的
自定义类型
关键字:type
封装原生类型
语法:type 自定义类型名 底层类型
它是基于原生类型创建全新的自定义类型,和原类型类型不同但底层结构一致,而且还能够为自定义类型绑定方法(原生类型不能直接绑方法)。核心价值就是把无语义的原生类型变成有业务语义的自定义类型
// 年龄类型定义
type Age int
// 方法:判断是否成年
func (a Age) IsAdult() bool {
return a >= 18
}
func main() {
var a Age = 20
if a.IsAdult() {
println("成年")
} else {
println("未成年")
}
}绑定方法
方法关联(绑定)指的是:把一个函数和某个类型(自定义类型 / 结构体 / 指针等)关联起来,使得该类型的实例可以通过 实例/方法名 () 的方式调用这个函数。他的核心就是 方法接收者(Receiver) 本质就是 方法通过接收者归属到某个类型
语法:func 方法接收者 方法名() {}
// 值接收者(传递自己的副本)
func (t T) 方法名() {}
// 引用接收者(传递自己的实例)
func (t *T) 方法名() {}值接收者:传递一个副本,副本的变化不会影响到主体
引用接收者:把主体的实例传递过来,任何操作实际上都是对主体进行的
不允许给原生类型直接加方法(避免污染全局类型),只有通过
type定义的命名类型才能绑定方法
别名
语法:type 别名 = 原类型
他能为现有类型起一个外号,原类型和别名是同一个类型,可直接互相赋值(经典写法:函数式编程)
// 给 函数类型 起别名
type CalcFunc = func(int, int) int
func main() {
// 用别名定义函数变量
var add CalcFunc = func(a, b int) int {
return a + b
}
println(add(3,4))
}定义结构体
语法:type 结构体名 struct { 字段名 类型; ... }
详情:结构化类型
定义接口
语法:type 接口名 interface { 方法名(参数) 返回值; ... }
详情:接口类型
变量
声明变量的是使用 var 关键字
// 一般声明
var [标识符] [数据类型]
// 多个声明
var [标识符1], [标识符2], [标识符3] [数据类型]
// 一般声明赋值
var [标识符] [数据类型] = [数据值]
// 多个声明赋值
var [标识符1], [标识符2], [标识符3] [数据类型] = [数据值1], [数据值2], [数据值3]
// 多个声明(一般用于全局变量)
var (
[标识符1] [数据类型1]
[标识符2] [数据类型2]
)也可以不写数据类型程序会自己推断
var [标识符] = [数据值]注意:对于声明的变量必须要使用,否则会爆出编译异常
变量默认值:
类型 默认值 数字类型 0布尔型 false字符串 ""(空字符串)其他 nil
还有一种写法 [标识符] := [值],他就等价于 var [标识符] [数据类型]
intVal := 1
// 等价于
var value int = 1需要注意的是在使用变量之前变量就已经使用 var 声明过了,那么再次使用 := 声明变量,就产生编译错误
值类型和引用类型
int、float、bool 和 string 都是值类型,使用这些类型的变量直接指向存在内存中的值
// 值类型
var num int = 1当一个值类型的变量赋值给另一个值类型的变量的时候,实际上就是在内存中拷贝了一份数据。两份数据共存,互不打扰。当修改一方的数据另一方的数据不会造成改变
var v1 int = 1
var v2 int = v1但是可以通过指针来达到一方修改另一方也会同步修改的目的
常量
常量是使用 const 关键字来声明,并且值类型仅可以为:布尔型、数字型和字符串
const [常量名] [常量类型:可不写] = [常量值]常量跟变量的定义方式相同,不同的是常量的值必须存在而且不可更改
iota
iota 是预定义常量生成器,仅能在常量声明(const)块中使用,用于自动生成一组连续的整数常量,默认从 0 开始,每新增一行常量声明,iota 的值自动加 1
核心规则:
每一个
const块都会重新初始化iota为 0同一
const块内,iota按行递增,空行/注释行不影响iota计数可通过表达式对
iota进行运算(如加减乘除、位移等),生成非连续的常量值若一行声明多个常量,
iota值相同,下一行才会递增
// 基础示例:连续整数生成
const (
a = iota // a = 0
b = iota // b = 1
c = iota // c = 2
)
// 简化写法:后续常量可省略iota,自动继承递增规则
const (
d = iota // d = 0
e // e = 1
f // f = 2
)
// 运算示例:基于iota生成自定义规则的常量
const (
g = iota * 2 // g = 0*2 = 0
h // h = 1*2 = 2
i // i = 2*2 = 4
)
// 多行声明示例:同一行iota值相同
const (
j, k = iota, iota // j=0, k=0
l, m // l=1, m=1
)
// 跳过值示例:使用下划线跳过不需要的常量
const (
n = iota // n=0
_ // 跳过1
o // o=2
)
// 多const块示例:每个块iota独立初始化
const p = iota // p=0
const (
q = iota // q=0
r // r=1
)类型转换
GO 是强类型语言,不同类型不能直接运算、不能直接赋值,必须显式转换,不能隐式转换
基础类型互转:目标类型(变量)
var a int = 10
var f float64 = float64(a) // 10.0
var b float64 = 3.99
var i int = int(b) // 3(直接截断小数,不会四舍五入)int ==> string 不能直接转换,直接写 string(97) 转换的数据实际上是 ASCII 字符 'a', 想要文本转字符串的话需要靠 strconv 包中的工具函数
import "strconv"
// int ==> string
i := 97
s := strconv.Itoa(i) // "97"
// string ==> int
s := "123"
i, err := strconv.Atoi(s) // 123 ✅strconv 包
该包中的工具函数尽是专门用来做基本类型 ↔ 字符串互相转换的
- Itoa = Integer to ASCII(数字转字符串)
- Atoi = ASCII to Integer(字符串转数字)
- 带 Parse 开头 = 字符串转别的
- 带 Format 开头 = 别的转字符串
int ↔ string
| 函数 | 作用 | 参数 1 | 参数 2 | 参数 3 | 返回值 1 | 返回值 2 |
|---|---|---|---|---|---|---|
| strconv.Itoa(i) | int → string | 要转的 int | - | - | 字符串 | 无 |
| strconv.Atoi(s) | string → int | 数字字符串 | - | - | int | 错误 err |
| strconv.FormatInt(i, base) | int64 → string | int64 数字 | 进制 (10/2/8/16) | - | 字符串 | 无 |
| strconv.ParseInt(s, base, bit) | string → int64 | 数字字符串 | 进制 | 位数 (32/64) | int64 | 错误 err |
float64 ↔ string
| 函数 | 作用 | 参数 1 | 参数 2 | 参数 3 | 参数 4 | 返回值 |
|---|---|---|---|---|---|---|
| strconv.FormatFloat(f, fmt, prec, bit) | float64 → string | 浮点数 | 格式 ('f') | 保留小数位 | 位数 | 字符串 |
| strconv.ParseFloat(s, bit) | string → float64 | 字符串 | 位数 | - | - | float64, err |
bool ↔ string
| 函数 | 作用 | 参数 | 返回值 |
|---|---|---|---|
| strconv.FormatBool(b) | bool → string | true/false | "true"/"false" |
| strconv.ParseBool(s) | string → bool | "true"/"false" | bool, err |
类型断言
断言语法:值, ok := 变量.(目标类型)
// 通过把 interface {} 类型,还原成它原来的真实类型来实现
var a interface{} = 100
i, ok := a.(int) // 开始断言
if ok {
println("是 int 类型:", i)
} else {
println("不是 int")
}在对变量断言类型的时候要尽可能的判断一下 ok 这个返回值,如果不对其进行判断的话要是断言失败程序会直接崩溃
泛型
泛型:Go 1.18 版本正式稳定引入的编译期特性,将类型作为参数传入代码,实现一套逻辑适配多种数据类型,同时保证编译期类型安全,无运行时额外开销
// 函数名[类型参数列表] (入参列表) 返回值列表 { 逻辑 }
func 函数名[T 约束1, K 约束2] (a T, b K) T {
// 函数逻辑
}
// ----------------------------
// 通用加法函数,仅支持int和float64类型
func Add[T int | float64](a, b T) T {
return a + b
}泛型是靠在方法名末尾,参数列表之前使用 [T 约束] 来实现的;多个类型参数用逗号分隔;每个类型参数必须绑定对应的约束
| 术语 | 定义 |
|---|---|
| 类型参数 | 类型的占位符,用[T 约束]格式声明,编译期会被具体类型替换 |
| 类型约束 | 限定类型参数的合法范围,规定该类型必须具备的能力(如支持加减、实现某方法) |
| 实例化 | 编译期用具体类型替换类型参数,生成对应类型的专属代码,无运行时开销 |
| 类型推断 | 编译器根据传入的实参,自动推导出类型参数的具体类型,无需手动显式指定 |
函数实参推断:根据传入的函数实参,自动推导类型参数的具体类型
约束类型推断:根据约束的类型关系,自动推导类型参数
泛型类型
格式:type 类型名[T 约束] 底层类型
// 泛型切片类型
type Slice[T any] []T
// 泛型结构体(键值对)
type Pair[K comparable, V any] struct {
Key K
Value V
}
// 接收者必须声明和类型一致的类型参数
func (接收者 *接收者类型[T]) 方法名(入参) 返回值 {
// 方法逻辑
}核心规则:方法只能使用接收者上声明的类型参数,不能在方法上新增独立的类型参数
类型约束
约束决定了类型参数的合法范围
内置约束
any:等价于interface{},接受任意类型,无限制
comparable:仅接受支持==/!=比较的类型(如 map 的 key 必须是 comparable)
联合类型
用 | 分隔多个合法类型,限定类型参数只能是列出的类型之一
// 仅支持整数类型
func Max[T int | int8 | int16 | int32 | int64](a, b T) T {
if a > b {
return a
}
return b
}接口约束
接口约束规定类型必须实现的方法,只有实现了该接口的类型才能传入
// 定义约束:必须实现String()方法
type Stringer interface {
String() string
}
// 仅接受实现了Stringer接口的类型
func PrintString[T Stringer](v T) {
fmt.Println(v.String())
}标准库
官方提供golang.org/x/exp/constraints包,预定义了常用数值约束
constraints.Ordered:支持</<=/>/>=比较的类型(整数、浮点数、字符串)constraints.Integer:所有整数类型constraints.Float:所有浮点数类型
泛型和接口区别
| 特性 | 泛型 | interface {}(空接口) |
|---|---|---|
| 类型检查时机 | 编译期,提前发现类型错误 | 运行时,类型错误会直接 panic |
| 性能 | 编译期实例化,无装箱拆箱,零运行时开销 | 运行时装箱拆箱、类型断言,有性能损耗 |
| 类型限制 | 可通过约束精准限定类型范围,编译期校验 | 无编译期限制,仅能运行时校验 |
泛型的实例化在编译期完成,运行时不存在泛型的类型信息
