Go 学习中的疑问与思考
这篇文档记录学习过程中遇到的具体问题。遇到类似代码时,可以先尝试自己解释,再查看结论。
1. := 如何使用?
:= 是短变量声明,用于在函数内部声明变量并赋值:
name := "张三"
age := 20
它会根据右侧的值推断类型。已经声明的变量重新赋值使用 =:
name = "李四"
同一个作用域内不能重复使用 :=,除非左侧至少有一个新变量:
name := "张三"
name, age := "李四", 20 // 正确,age 是新变量
:= 只能在函数内部使用,包级变量应使用 var。
2. %T 为什么必须配合 Printf?
%T 是格式占位符,fmt.Printf 会解析它:
version := 1.22
fmt.Printf("version=%T\n", version) // version=float64
fmt.Println 不会解析占位符,会把 %T 当作普通文本:
fmt.Println("version=%T", version) // version=%T 1.22
常用占位符:%T 输出类型,%v 输出默认值,%s 输出字符串,%d 输出整数,%f 输出浮点数,%t 输出布尔值。
3. []int 是数组吗?
[]int 是切片,不是固定长度数组:
var array [3]int // 数组,长度固定
var slice []int // 切片,长度可变化
[3]int 的长度是类型的一部分,[3]int 和 [4]int 是不同类型。切片可以使用 append 增长,内部包含指向底层数组的信息、当前长度和容量。
与其他语言粗略类比:Go 的数组接近 C 的 int a[3];Go 的切片更接近 Java 的动态数组使用体验,或 C 中的“指针 + 长度”。但切片本身仍然是 Go 的独立值类型。
Go 的变量声明通常把类型写在变量名之后:
var age int
var numbers []int
创建切片值时,类型写在字面量前面:
numbers := []int{10, 20, 30}
4. make 的作用是什么?
make 用于初始化切片、映射和通道:
numbers := make([]int, 3, 5)
这里切片长度是 3,容量是 5,初始值为 [0 0 0]。len 表示当前元素数量,cap 表示容量。
numbers = append(numbers, 10)
append 才会增加切片长度。容量只是预留空间,不能直接通过 numbers[3] 访问一个尚不存在的元素。
5. numbers[0:2] 包含哪些元素?
切片区间是左闭右开:包含开始索引,不包含结束索引:
numbers := []int{10, 20, 30}
part := numbers[0:2]
fmt.Println(part) // [10 20]
它包含索引 0 和 1,不包含索引 2。因此想选中全部三个元素,应使用 numbers[0:3] 或 numbers[:]。
6. Go 中如何使用 scanf 输入?
Go 中对应的函数是 fmt.Scanf,但它返回的是“成功读取的数量”和错误值,用户输入的值通过指针写入变量:
var score int
_, err := fmt.Scanf("%d", &score)
if err != nil {
fmt.Println("输入错误:", err)
return
}
&score 是把变量地址交给输入函数。第一个返回值通常用 _ 忽略,因为输入一个整数时它的值是 1,不是成绩本身。
初学阶段也可以使用更简单的:
_, err := fmt.Scan(&score)
如果要读取包含空格的一整行,Scan 和 %s 不够用,可以使用 bufio.Reader。
7. switch 的基本语法是什么?
switch day {
case 1, 2, 3, 4, 5:
fmt.Println("工作日")
case 6, 7:
fmt.Println("周末")
default:
fmt.Println("输入无效")
}
Go 的 switch 默认不会自动执行下一个 case,通常不需要 break。也可以省略表达式,直接写条件:
switch {
case score >= 90:
fmt.Println("优秀")
case score >= 60:
fmt.Println("及格")
default:
fmt.Println("不及格")
}
8. 为什么成绩程序输入任何值都显示不及格?
错误代码:
score, err := fmt.Scanf("%d", &score)
fmt.Scanf 的第一个返回值是成功读取的数量。输入一个整数时,score 会被重新赋值为 1,所以判断的不是实际成绩。
正确写法:
var score int
_, err := fmt.Scanf("%d", &score)
if err != nil {
fmt.Println("输入错误:", err)
return
}
同时,成绩区间应覆盖边界值。推荐从高到低判断:
switch {
case score < 0 || score > 100:
fmt.Println("输入范围不合法")
case score >= 90:
fmt.Println("优秀")
case score >= 80:
fmt.Println("良好")
case score >= 60:
fmt.Println("及格")
default:
fmt.Println("不及格")
}
9. & 和 * 分别是什么意思?
&value 获取变量地址,*pointer 读取或修改指针指向的值,而 *int 表示指针类型:
number := 10
pointer := &number
*pointer = 20
操作后 number 也变成 20。
10. Go 是按值传参还是按引用传参?
Go 只有按值传参。传入 int 会复制整数,传入 *int 会复制地址。地址副本仍指向同一个整数,因此函数可以通过它修改原变量。把“传指针”理解成“传入一个地址值的副本”更准确。
11. 什么时候使用指针接收者?
方法需要修改原值时必须使用指针接收者。结构体较大、希望避免每次调用都复制时,也通常使用指针接收者。同一个类型的方法一般保持一致的接收者风格,不要无理由混用。
12. 为什么接口不需要 implements?
Go 使用隐式实现。具体类型只要拥有接口要求的所有方法,就满足接口。这样接口可以由使用方根据自己需要定义,具体类型不必提前依赖接口。
13. 为什么结构体值不能赋给某个接口,指针却可以?
如果接口要求的方法使用指针接收者实现,那么该方法只属于指针类型的方法集:
func (d *Document) Write(content string) {}
此时 *Document 满足包含 Write 的接口,而 Document 值不满足。值接收者方法则同时可以由值和指针调用。
14. any 和 interface{} 有区别吗?
没有行为差异。any 从 Go 1.18 开始是 interface{} 的别名,写法更直观。它可以保存任意类型,但读取具体数据时通常需要类型断言或类型选择。
15. 类型断言为什么建议接收 ok?
text, ok := value.(string)
类型匹配时 ok 为 true;不匹配时 text 得到字符串零值,ok 为 false。如果省略 ok,断言失败会直接 panic。
16. 为什么接口里放了 nil 指针,却不等于 nil?
接口保存动态类型和动态值。把 (*Document)(nil) 放入接口后,动态值是 nil,但动态类型是 *Document,所以接口整体不等于 nil。只有动态类型和动态值都为空,接口才等于 nil。
17. 当前的学习思路
目前采用“章节讲义 + 独立示例 + 自己完成练习 + 记录疑问”的方式。遇到编译错误时,先看错误信息指出的变量、类型或行号,再缩小到最小示例验证;不要只修改到能运行,还要说明错误发生的原因。
下一阶段计划学习错误处理与文件操作。在继续之前,建议完成第五章的交换变量、形状接口和内存文件练习。