Golang中混合使用指针和值接收器有必要吗?

Golang中混合使用指针和值接收器有必要吗? 不推荐混合使用指针接收者和值接收者,我一直遵循这条规则。但现在我遇到了一个情况,感觉几乎必须混合使用,否则在我看来代码会变得很丑陋:

示例1:混合使用

type bitBoard uint64

// 这里我们需要指针接收者,因为我们要改变值
func (b *bitBoard) set(pos int) {
	*b |= bitBoard(uint64(1) << uint(pos))
}

// 值接收者,我们不改变值
func (b bitBoard) String() string {
	result := strings.Repeat("0", 64) + fmt.Sprintf("%b", b)
	return result[len(result)-64:]
}

func main() {
	b1 := bitBoard(12)
	b1.set(8)
	b2 := bitBoard(10)

	// 混合使用指针和值接收者时有效
	fmt.Println((b2 & b1).String())
}

在这种情况下,set() 方法需要指针接收者,但 String() 不需要。

如果我尝试遵循“停止混合使用指针和值接收者”的规则,我需要这样做:

type bitBoard uint64

// 这里我们需要指针接收者,因为我们要改变值
func (b *bitBoard) set(pos int) {
	*b |= bitBoard(uint64(1) << uint(pos))
}

// 指针接收者,即使我们不改变值
func (b *bitBoard) String() string {
	result := strings.Repeat("0", 64) + fmt.Sprintf("%b", *b)
	return result[len(result)-64:]
}

func main() {
	b1 := bitBoard(12)
	b1.set(8)
	b2 := bitBoard(10)

	// 使用指针接收者,我们现在需要创建一个临时变量
	b3 := b2 & b1
	fmt.Println(b3.String())
}

这对我来说有点丑陋,必须在打印之前创建一个临时变量来存储 bitBoard。在我的例子中,我有10多个需要打印的 bitBoard,所以会有很多额外的变量。

这当然是因为我想在打印两个棋盘之前对它们执行位操作(p & b)。

有没有更好的方法来实现这一点,这样我就不需要混合使用指针和值接收者,也不需要创建临时变量?或者我应该接受“不混合”规则并不是一个严格的规则,并在上述情况下继续使用混合方式?

Go Playground

Go Playground - The Go Programming Language


更多关于Golang中混合使用指针和值接收器有必要吗?的实战教程也可以访问 https://www.itying.com/category-94-b0.html

3 回复

当然,我怎么没想到呢? 😊

谢谢…

更多关于Golang中混合使用指针和值接收器有必要吗?的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


抱歉代码格式不太理想,但或许你可以尝试类似这样的做法?

package main

import (
	"fmt"
)

type bitBoard struct {
	b uint64
}

func (b *bitBoard) set(pos int) {
	b.b |= 1 << pos
}

func (b *bitBoard) String() string {
	return fmt.Sprintf("%064b", b.b)
}

func (b *bitBoard) And(o *bitBoard) *bitBoard {
	return &bitBoard{b.b & o.b}
}

func Newbb(val uint64) *bitBoard {
	return &bitBoard{val}
}

func main() {
	b1 := Newbb(12)
	b1.set(8)
	b2 := Newbb(10)
	b3 := b2.And(b1)
	fmt.Println(b3.String())
	fmt.Println(b1.And(b2).String())
}

在Go语言中,混合使用指针和值接收器是完全合理的,特别是在你描述的场景中。Go语言规范并没有禁止这种混合使用,而是建议在类型的方法集内保持一致性以提高代码的可预测性。但在实际开发中,根据方法的功能需求选择接收器类型是更重要的。

在你的bitBoard示例中,set()方法需要修改接收器,因此必须使用指针接收器。而String()方法只是读取数据,使用值接收器是更自然的选择。强制让String()也使用指针接收器会导致不必要的解引用操作,并且会使代码更复杂。

以下是几个关键点:

  1. 方法集的一致性:虽然保持一致性有助于代码维护,但不应以牺牲代码清晰度为代价。你的混合使用方案更符合每个方法的实际需求。

  2. 临时变量问题:使用指针接收器的String()方法确实会导致需要创建临时变量,这在你的场景中会降低代码的可读性。

  3. 更好的实现方案:你可以考虑以下两种方案:

方案A:保持混合使用(推荐)

type bitBoard uint64

func (b *bitBoard) set(pos int) {
    *b |= 1 << pos
}

func (b bitBoard) String() string {
    // 更高效的实现
    var sb strings.Builder
    sb.Grow(64)
    for i := 63; i >= 0; i-- {
        if b>>i&1 == 1 {
            sb.WriteByte('1')
        } else {
            sb.WriteByte('0')
        }
    }
    return sb.String()
}

func main() {
    b1 := bitBoard(12)
    b1.set(8)
    b2 := bitBoard(10)
    
    // 直接使用表达式,无需临时变量
    fmt.Println((b2 & b1).String())
}

方案B:使用辅助函数避免混合

type bitBoard uint64

func (b *bitBoard) set(pos int) {
    *b |= 1 << pos
}

func (b *bitBoard) String() string {
    return bitBoardToString(*b)
}

// 独立的辅助函数
func bitBoardToString(b bitBoard) string {
    var sb strings.Builder
    sb.Grow(64)
    for i := 63; i >= 0; i-- {
        if b>>i&1 == 1 {
            sb.WriteByte('1')
        } else {
            sb.WriteByte('0')
        }
    }
    return sb.String()
}

func main() {
    b1 := bitBoard(12)
    b1.set(8)
    b2 := bitBoard(10)
    
    // 仍然需要临时变量
    b3 := b2 & b1
    fmt.Println(b3.String())
}

方案A明显更简洁,因为它允许直接在表达式上调用String()方法,而无需创建临时变量。

结论:在你的场景中,混合使用指针和值接收器是完全合理的。Go语言的灵活性允许我们根据方法的具体需求选择最合适的接收器类型。保持代码的清晰性和表达力比严格遵循"不混合"规则更重要。你的第一种实现方案(混合使用)是更好的选择。

回到顶部