变量、常量、数据类型
1. 语法讲解
| 概念 | 是什么 | 解决什么问题 | 语法示例 |
|---|---|---|---|
| 变量 | 运行时可改的数据容器 | 存储中间结果、传递数据 | var n int = 10 |
| 常量 | 编译时确定且不可改 | 统一配置、防止魔数 | const pi = 3.14 |
| 基本类型 | 语言内置的单一值类型 | 决定内存大小与操作 | int float64 bool string |
| 复合类型 | 由基本类型组合而成 | 描述复杂实体 | []int map[string]int struct{} |
| 类型转换 | 显式把 A 类型变成 B 类型 | 避免类型不匹配编译错 | float64(age) |
2. 其他用法与总结
其他用法
| 用法 | 是什么 | 为什么 | 30 秒代码 |
|---|---|---|---|
匿名变量 _ |
占位符,丢弃值 | 避免“未使用”编译错;多返回值时干净 | _, err := fmt.Println("ok") |
| 平行赋值 | 同时给多变量赋值 | 无需临时变量即可交换;一键解包 | a, b = b, a |
| 常量表达式 | 常量间运算 | 魔数收敛,一改全改;编译期计算零运行时开销 | const MB = 1024 * 1024 |
| 新类型 vs 别名 | type Meter float64 创建新类型;type Second = float64 仅别名 |
强类型防止单位混淆;编译器帮你找 bug | func distance(d Meter) 传错类型就报错 |
| range遍历 | rune = Unicode 码点 |
按“字符”而非字节遍历,汉字不劈断 | for _, r := range "你好" { fmt.Printf("%c\n", r) } |
总结
- 变量:三种写法,记得零值。
i. 标准写法var s1 string = "hello"
ii. 类型推导var s1 = "hello"
iii. 简写s1 := "hello" - 常量:
const + iota做枚举,编译期确定。 - 类型:
- 基本类型决定内存大小和可进行的操作;
- 复合类型描述复杂结构;
- 自定一类型防止混淆
- 转换必须显式。
- 技巧:
_丢弃、平行赋值、常量表达式、新类型防混淆、rune 遍历 Unicode。
1 | type Person struct { |
String() 使用的是值接收者,String2() 使用的是指针接收者。加 * 是为了传递内存地址而不是复制整个数据。
一、 语法层面的区别(最直观的差异)
func (p Person) String():p是调用该方法时原对象的一个副本(Copy)。在String()内部修改p.Name,不会影响原来的Person对象。func (p *Person) String2():p是原对象在内存中的地址(指针)。在String2()内部修改p.Name,会直接改变原来的Person对象。
二、 调用机制的区别(Go 语言的语法糖)
在实际写代码时,你可能会发现无论用值还是指针,好像都能调用成功:
1 | p := Person{Name: "张三", Age: 18} |
为什么 p.String2() 不报错?
因为 Go 语言有自动取地址的机制。当使用值 p 调用指针接收者方法 String2() 时,Go 会自动把它转换为 (&p).String2()。
反之,如果是用指针 ptr := &p 调用值接收者 String(),Go 会自动解引用:(*ptr).String()。
但是,接口实现(Interface)不吃这一套!
这是最关键的考点:
Person类型的值,既拥有String()方法,也拥有String2()方法(因为编译器帮你自动转换了)。- 但是,如果有一个接口
type I interface { String() string },只有Person类型(值) 实现了这个接口。 - 如果是接口
type I2 interface { String2() string },只有*Person类型(指针) 实现了这个接口。把Person的值赋给I2会直接报编译错误。
三、 为什么要这么设计?(应用场景与最佳实践)
为什么有时候加 *,有时候不加?在工程实践中,有一条铁律:
👉 如果方法内部需要修改接收者的状态,必须用指针接收者(*T)。
👉 如果接收者是一个大的结构体(包含很多字段),为了性能避免数据拷贝,建议用指针接收者(*T)。
👉 如果接收者是基本类型、小结构体,且不需要修改原对象,用值接收者(T)更安全。
结合你图片里的代码:
关于
String()(值接收者):
这个方法只是用来格式化输出字符串(类似 Java 的toString()),它不需要修改Person的Name或Age。所以用值接收者是合理的,保证了该方法的“只读”特性。关于
String2()(指针接收者):
这个方法里面的逻辑和String()一模一样,并没有修改任何字段。所以单从这个方法的功能看,用指针接收者是多余的(除非Person结构体极其庞大,为了避免每次调用拷贝内存)。
结论
只需要读值的时候就用值传递
如果需要修改值进行写入时就用指针传递
函数
1. 语法讲解
Go语言函数的基本语法:1
2
3
4func 函数名(参数列表) 返回值列表 {
// 函数体
return 返回值
}
注意区别给结构体加方法的语法:
1 | type 结构体名 struct {} |
特点:
支持多返回值
支持命名返回值
支持可变参数
函数可以作为值传递
匿名函数与闭包:1
2
3
4
5
6// 匿名函数
func(参数列表) 返回值列表 {
// 函数体
}
// 闭包:函数内部定义的函数,可以访问外部函数的变量
2. 应用场景
普通函数:用户注册验证、数据格式转换
多返回值函数:数据库操作(返回结果和错误)
匿名函数:并发任务、排序比较函数
闭包:计数器、缓存实现、中间件
指针
1. 语法讲解
Go语言指针的基本概念:
- 内存地址:每个变量在内存中都有唯一的地址
- 指针变量:存储另一个变量内存地址的变量
- 指针操作:
1 | var p *int // 声明指针 |
指针作为函数参数:
- 值传递:函数内修改不影响原变量
- 指针传递:函数内修改影响原变量
2. 应用场景
- 指针传递:大对象传递(避免值拷贝开销)
- 修改原变量:函数内需要修改调用者的变量
- 动态内存分配:构建复杂数据结构(链表、树等)
- 节省内存:避免大数据结构的重复拷贝
错误处理
Go语言的错误处理机制:
- error类型:内置接口类型
1 | type error interface { |
- defer语句:延迟执行,后进先出
1 | defer func() { |
- panic/recover机制:
1 | // 触发panic |
2. 应用场景
- error类型:数据库连接失败、文件读写错误
- defer语句:资源清理(关闭文件、释放锁、数据库连接)
- panic/recover:处理不可恢复的错误(如数组越界) 仅针对当前协程