用 Go 语言写 FizzBuzz 这类练习题时,函数调用与指针解引用经常被混在一起。不少人习惯把一些参数定义成指针,却在调用时传入一个值类型,或者反过来对一个普通整型变量执行星号操作,编译期立刻报出 invalid indirect of 错误。这个错误不是说函数逻辑有问题,而是在调用路径上出现了类型操作错误。理清值类型、指针类型以及方法集之间的关系,不仅能消除这类编译错误,也能帮助我们把 FizzBuzz 的实现写得更清晰、更高效。

一、无效间接引用错误是怎么产生的
Go 语言中星号运算符 * 只用于指针类型解引用。指针本身保存的是另一个变量的内存地址,通过 *p 可以读取或修改该地址上的值。如果变量 p 不是指针类型,比如它是 int、string 或者普通 struct,编译器就无法继续解析 *p,并提示 invalid indirect of p (type int)。这个报错里的 indirect 指的就是解引用操作。
下面这段代码可以复现错误:
package main
import "fmt"
func main() {
n := 10
p := &n
fmt.Println(*p) // 正确:p 是 *int 类型
m := 20
fmt.Println(*m) // 编译错误:invalid indirect of m (type int)
}
第一处 *p 可以正常工作,因为 p := &n 让 p 成为指向 n 的指针。第二处 *m 失败,因为 m 本身就是一个 int 值,而不是地址。对值类型使用星号,就像试图打开一个不是信封的东西,编译阶段就会被拦截。
这种错误还容易出现在函数返回值上。假如有一个函数返回 int,返回结果虽然是值,但有些开发者会误以为可以就地解引用或取地址:
func getValue() int {
return 1
}
func main() {
v := &getValue() // 编译错误:cannot take address of getValue()
}
函数返回值如果没有被变量接收,它是不可寻址的,不能直接取地址;而如果对一个不可寻址的值使用星号,同样可能触发无效间接引用或相关错误。排查这类问题时,先确认变量声明时的类型,再看有没有误用星号或取地址符号,通常能快速定位。
二、函数调用中的值接收者与指针接收者
Go 语言的方法集规则决定了哪些类型可以调用哪些方法。类型 T 的方法集包含所有接收者为 T 的方法;类型 *T 的方法集同时包含接收者为 T 和 *T 的方法。换句话说,指针类型拥有更大的方法集,而值类型只能调用值接收者的方法。
看一个简单的计数器:
type Counter struct {
n int
}
func (c Counter) Get() int {
return c.n
}
func (c *Counter) Inc() {
c.n++
}
func main() {
c := Counter{}
c.Inc() // 编译器会自动取地址,等价于 (&c).Inc()
c.Get() // 直接调用值接收者方法
}
在上面的代码中,c.Inc() 看起来是用值变量调用指针接收者方法,实际上编译器会隐式转换为 (&c).Inc()。这种隐式取地址的前提是变量本身可寻址。如果一个值类型不能取地址,比如复合字面量直接作为接收者,就无法调用指针方法:
func main() {
Counter{}.Inc() // 编译错误:cannot call pointer method Inc on Counter
}
这个错误与无效间接引用虽然提示不同,但根因都指向同一个问题:调用方把值类型和指针类型的边界处理错了。函数参数也一样,如果函数签名要求 *int,调用时传入 int 会提示类型不匹配;如果传入一个值后又在函数体内写 *n,则可能叠加出无效间接引用错误。
避免这类问题的方法很直接:需要修改原对象时使用指针接收者或指针参数;只读取数据时使用值接收者或值参数。调用前确认变量是值还是指针,不要对非指针类型使用星号解引用。
三、FizzBuzz 基础实现中的函数设计问题
FizzBuzz 的逻辑是:遍历 1 到 100,遇到 3 的倍数输出 Fizz,5 的倍数输出 Buzz,同时是 3 和 5 的倍数输出 FizzBuzz,其他数字原样输出。最常见的写法是直接在 main 函数里写 if-else:
func main() {
for i := 1; i <= 100; i++ {
if i%15 == 0 {
fmt.Println("FizzBuzz")
} else if i%3 == 0 {
fmt.Println("Fizz")
} else if i%5 == 0 {
fmt.Println("Buzz")
} else {
fmt.Println(i)
}
}
}
这种写法没有函数封装,也不会触发指针相关错误。但当需求变复杂,比如要把结果返回给调用方、写到文件或者做单元测试时,直接打印的代码就不好复用了。合理的做法是把单个数字的转换逻辑抽成函数:
func fizzbuzz(n int) string {
if n%15 == 0 {
return "FizzBuzz"
}
if n%3 == 0 {
return "Fizz"
}
if n%5 == 0 {
return "Buzz"
}
return strconv.Itoa(n)
}
这个函数接收一个 int 值,返回 string,没有指针参与,自然也不会出现无效间接引用。假如想进一步把规则封装到结构体,并让它支持配置不同倍数和关键词,就要注意方法接收者的选择。下面的设计容易在调用时踩坑:
type FizzBuzz struct {
limit int
}
func (f *FizzBuzz) Run() {
for i := 1; i <= f.limit; i++ {
fmt.Println(convert(i))
}
}
func main() {
FizzBuzz{limit: 100}.Run() // 编译错误:cannot call pointer method Run on FizzBuzz
}
复合字面量 FizzBuzz{limit: 100} 是一个值,而且没有被变量接收,编译器无法取地址,因此不能调用指针接收者方法。修复方式有两种:先用变量保存实例,再调用;或者把 Run 的接收者改成值类型。但值接收者无法修改 limit 字段,如果后续需要在运行时调整范围,还是要用指针接收者加变量保存。
从函数设计的角度看,FizzBuzz 这种转换逻辑更适合采用纯函数方式:输入一个数字,输出一个字符串。纯函数没有副作用,容易测试,也不会因为指针混用而引入编译错误。变量保存加上指针接收者则适合需要维护状态的场景。
四、优化 FizzBuzz 的实现方式
基础实现每次判断都要执行多次取模运算。在 1 到 100 这种小规模下性能差异可以忽略,但如果扩展到百万级甚至更大范围,减少取模次数会带来可见提升。取模运算比简单的比较和字符串拼接更耗时,因此可以用先判断 3 再判断 5 的方式来减少最坏情况下的取模次数:
func fizzbuzzOptimized(n int) string {
if n%3 == 0 {
if n%5 == 0 {
return "FizzBuzz"
}
return "Fizz"
}
if n%5 == 0 {
return "Buzz"
}
return strconv.Itoa(n)
}
这个版本中,当数字不是 3 的倍数时,只需要一次取模;只有当它同时是 3 和 5 的倍数时才执行两次取模。相比先判断 n%15 再分别判断 3 和 5,平均取模次数更低。而且逻辑上仍然清晰。
另一种常见做法是使用 switch 无表达式分支,把多个条件并列:
func fizzbuzzSwitch(n int) string {
switch {
case n%15 == 0:
return "FizzBuzz"
case n%3 == 0:
return "Fizz"
case n%5 == 0:
return "Buzz"
default:
return strconv.Itoa(n)
}
}
switch 版本的可读性更好,条件顺序一目了然,适合团队协作。缺点是每个 case 都会执行一次取模,最坏情况可能执行三次。对于绝大多数业务场景,这种差异远不如代码可读性重要。优化时不要过度追求减少取模,除非确实有性能瓶颈。
也可以用 map 来存储规则,例如 map[int]string{3: "Fizz", 5: "Buzz"},但 map 的遍历顺序不固定,直接拼接会得到不稳定结果。需要对 key 排序后再处理,代码复杂度增加,并不比固定规则版本更好。若规则数量很多,可以考虑切片加排序,而不是硬编码分支。
为了验证优化效果,可以写一个基准测试:
func BenchmarkFizzBuzzOptimized(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = fizzbuzzOptimized(i)
}
}
func BenchmarkFizzBuzzSwitch(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = fizzbuzzSwitch(i)
}
}
运行 go test -bench=. -benchmem 可以对比两个版本的耗时和内存分配。在小规模范围内,字符串拼接和返回值的开销通常比取模更大。把数字转换成字符串使用 strconv.Itoa 会涉及内存分配,如果对性能要求极高,可以使用缓冲区或预分配切片来减少分配。但实践中,先保证正确性和可读性,再针对热点路径做优化。
五、总结:避免无效间接引用与写出清晰函数
无效间接引用错误本质上是类型使用错误。星号运算符只接受指针,取地址符号也只能作用于可寻址的变量。函数调用中遇到此类报错时,优先检查参数类型、返回值类型以及方法接收者类型。不要让值类型和指针类型在参数传递中互相猜测,而是在函数签名中明确表达意图:值参数表示不会修改原数据,指针参数表示函数可能修改原数据。
在 FizzBuzz 这类小练习中,更推荐使用纯函数设计。输入 int 输出 string,不依赖外部状态,不暴露指针细节,调用方不需要关心内部实现。这样的函数天然避免无效间接引用错误,也更容易编写单元测试。若确实需要维护状态,比如可配置上限、自定义规则,再引入结构体和指针接收者,但调用时要保证变量可寻址。
最终,好的 Go 代码不是堆砌技巧,而是把类型边界表达清楚。理解了值、指针和方法集的关系,回头再看 invalid indirect of 这类编译错误,就不用再靠猜了。