《Go语言圣经》Map和结构体

《Go语言圣经》map

禁止取址

  • 禁止取址:map 中的元素不是变量,无法进行取址操作(如 &ages["bob"])。因为 map 可能随元素增加重新分配内存,导致地址失效。

nil map

  • 向 nil map 存入元素会触发 panic(如 ages["carol"] = 21),因此必须先使用 make() 创建 map。

存在性检查

  • map 的下标语法可返回两个值(如 age, ok := ages["bob"]),第二个布尔值 ok 用于判断元素是否存在,常用于条件判断。

实现集合(Set)

  • Go 语言没有内置 Set 类型,但可用 map[string]bool 替代。通过键的唯一性去重,例如 dedup 程序通过 seen[line] = true 标记已存在的行。
func main() {
    seen := make(map[string]bool) // a set of strings
    input := bufio.NewScanner(os.Stdin)
    for input.Scan() {
        line := input.Text()
        if !seen[line] {
            seen[line] = true
            fmt.Println(line)
        }
    }

    if err := input.Err(); err != nil {
        fmt.Fprintf(os.Stderr, "dedup: %v\n", err)
        os.Exit(1)
    }
}

嵌套 map

  • Map的value类型也可以是一个聚合类型,比如是一个map或slice。在下面的代码中,graph 这个 map 的key类型是一个字符串,value类型map[string]bool代表一个字符串集合。从概念上讲,graph将一个字符串类型的key映射到一组相关的字符串集合,它们指向新的graph的key。
  • 其中addEdge函数惰性初始化map是一个惯用方式,也就是说在每个值首次作为key时才初始化。hasEdge函数显示了如何让map的零值也能正常工作;即使from到to的边不存在,graph[from][to]依然可以返回一个有意义的结果。
var graph = make(map[string]map[string]bool)
func addEdge(from, to string) {
    edges := graph[from]
    if edges == nil {
        edges = make(map[string]bool)
        graph[from] = edges
    }
    edges[to] = true
}
func hasEdge(from, to string)bool {
    return graph[from][to]
}

《Go语言圣经》结构体

一、结构体指针的高效应用

在处理大型结构体时,为避免内存复制,通常使用指针传递和返回结构体:

// 通过指针传入结构体,避免值拷贝
func Bonus(e *Employee, percent int) int {
    return e.Salary * percent / 100
}

// 必须使用指针才能修改原始结构体数据
func AwardAnnualRaise(e *Employee) {
    e.Salary = e.Salary * 105 / 100
}

关键点:

  • Go语言函数参数传递均为值拷贝
  • 指针传递避免大数据结构复制开销
  • 修改原结构体必须通过指针实现

二、结构体比较机制

在Go语言里,结构体是否能够进行比较,取决于结构体字段的类型。

可比较的情况

要是结构体的所有字段类型都属于可比较类型(像基本数据类型、指针、数组、接口,还有包含可比较类型的结构体),那么这个结构体就是可比较的。可比较的结构体能够运用==或者!=操作符来比较,比较时会逐个检查结构体里每个字段的值是否相同。

下面来看几个例子:

// 例1:结构体字段全是可比较类型
type Point struct {
    X, Y int
}

func example1() {
    p1 := Point{1, 2}
    p2 := Point{1, 2}
    fmt.Println(p1 == p2) // 输出:true
}

// 例2:嵌套结构体的字段也都是可比较类型
type Coordinate struct {
    Point
    Z int
}

func example2() {
    c1 := Coordinate{Point{1, 2}, 3}
    c2 := Coordinate{Point{1, 2}, 3}
    fmt.Println(c1 == c2) // 输出:true
}

// 例3:匿名结构体,只要字段类型和顺序一样,就可以比较
func example3() {
    a := struct{ X, Y int }{1, 2}
    b := struct{ X, Y int }{1, 2}
    fmt.Println(a == b) // 输出:true
}

不可比较的情况

当结构体里存在不可比较类型的字段(例如切片、映射、函数)时,这个结构体就不可以使用==或者!=操作符进行比较。

看下面的例子:

// 例4:包含切片字段的结构体不可直接比较
type User struct {
    Name string
    Hobbies []string // 切片属于不可比较类型
}

func example4() {
    u1 := User{"Alice", []string{"reading", "swimming"}}
    u2 := User{"Alice", []string{"reading", "swimming"}}
    // fmt.Println(u1 == u2) // 编译会报错:invalid operation: u1 == u2 (struct containing []string cannot be compared)
}

使用 reflect.DeepEqual 进行深度比较

若想比较包含不可比较类型字段的结构体,可以借助reflect.DeepEqual函数。不过要留意,该函数的性能比直接比较操作符要低,而且在比较某些类型(如函数)时可能会产生panic。

示例如下:

import "reflect"

func example5() {
    u1 := User{"Alice", []string{"reading", "swimming"}}
    u2 := User{"Alice", []string{"reading", "swimming"}}
    fmt.Println(reflect.DeepEqual(u1, u2)) // 输出:true
}

总结

  1. 可比较的结构体:当结构体的所有字段都是可比较类型时,结构体可比较。
  2. 不可比较的结构体:若结构体包含不可比较类型的字段,则结构体不可直接比较。
  3. 深度比较方案:对于不可直接比较的结构体,可使用reflect.DeepEqual进行比较,但要谨慎使用。

建议在性能敏感的场景中,尽量避免使用reflect.DeepEqual,优先考虑使用可比较类型的结构体。

三、结构体嵌入机制深度解析

1. 基本嵌入语法

通过匿名字段实现结构体嵌入:

type Point struct{ X, Y float64 }

// 嵌入Point结构体
type ColoredPoint struct {
    Point      // 匿名字段,仅声明类型
    Color color.RGBA
}

// 使用示例
var cp ColoredPoint
cp.X = 1          // 直接访问嵌入字段
cp.Point.X = 1    // 等价访问方式

特性总结:

  • 嵌入类型的所有字段和方法被提升为外部类型
  • 支持直接访问和显式访问两种方式
  • 编译时自动生成访问包装函数
2. 方法"继承"机制

嵌入类型的方法会被自动提升:

// Point类型的方法
func (p Point) Distance(q Point) float64 {
    return math.Hypot(q.X-p.X, q.Y-p.Y)
}

func (p *Point) ScaleBy(factor float64) {
    p.X *= factor
    p.Y *= factor
}

// ColoredPoint可直接使用Point的方法
var p = ColoredPoint{Point{1,1}, color.RGBA{255,0,0,255}}
var q = ColoredPoint{Point{5,4}, color.RGBA{0,0,255,255}}

fmt.Println(p.Distance(q.Point)) // 直接调用Point的方法
p.ScaleBy(2)                     // 支持指针方法

编译器行为:

// 编译器自动生成的包装方法
func (cp ColoredPoint) Distance(q Point) float64 {
    return cp.Point.Distance(q)
}

func (cp *ColoredPoint) ScaleBy(factor float64) {
    cp.Point.ScaleBy(factor)
}
3. 嵌入与继承的本质区别

Go语言的嵌入实现"has a"关系,而非"is a"关系:

var cp ColoredPoint
var p Point

p = cp        // 错误:类型不兼容
p = cp.Point  // 正确:显式获取嵌入实例

p.Distance(q)         // 错误:q不是Point类型
p.Distance(q.Point)   // 正确:显式传递Point字段

关键点:

  • 嵌入不改变类型系统
  • 方法调用需显式匹配参数类型
  • 保持类型独立性,避免继承带来的耦合问题
4. 指针嵌入的高级应用

通过嵌入指针实现数据共享:

type ColoredPoint struct {
    *Point      // 嵌入指针类型
    Color color.RGBA
}

// 共享同一个Point实例
p := ColoredPoint{&Point{1,1}, red}
q := ColoredPoint{&Point{5,4}, blue}
q.Point = p.Point  // 共享同一实例

p.ScaleBy(2)        // 修改会影响所有引用

fmt.Println(*p.Point) // 输出: {2 2}
fmt.Println(*q.Point) // 输出: {2 2}

指针嵌入特性:

  • 支持多实例共享底层数据
  • 方法调用自动解引用(p.ScaleBy()等价于(*p.Point).ScaleBy()
  • 适用于需要数据共享的场景

四、最佳实践建议

  1. 大型结构体一律使用指针传递
  2. 需修改原始数据时必须使用指针
  3. 优先使用结构体嵌入而非接口组合复杂类型
  4. 根据数据共享需求选择值嵌入或指针嵌入
  5. 通过显式访问保持类型清晰性,避免命名冲突
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值