目录

Go struct 可比较吗?能当 map key 吗?

相信很多 Gopher 都踩过类似的坑——Go 的 struct 到底能不能比较?又能不能作为 map 的 key?

这篇文章就把结论和底层逻辑和实战技巧给大家讲透。

一、Go struct 是否可比?

Go 里不是所有 struct 都能比较,关键看结构体的所有字段是否都属于“可比较类型”

这是 Go 的类型系统规则,先把可比较和不可比较类型分清楚。

(一)可比较的 struct

可比较的 struct:所有字段均为可比较类型。

像 int、string、bool、指针这些基础类型都是“可比较类型”;

如果 struct 的所有字段都属于这类,那这个 struct 就能用==!=比较,比较时会逐字段校验是否相等。

比如做用户信息校验时,判断两个用户结构体是否完全一致,就可以直接比较:

package main

import "fmt"

// User 所有字段均为可比较类型(int、string、bool)
type User struct {
    ID       int
    Username string
    IsVip    bool
}

func main() {
    // 两个字段完全相同的结构体
    u1 := User{ID: 1, Username: "gopher", IsVip: true}
    u2 := User{ID: 1, Username: "gopher", IsVip: true}

    // 直接用==比较
    if u1 == u2 {
        fmt.Println("u1和u2完全相等")
    } else {
        fmt.Println("u1和u2不相等")
    }

    // 字段不完全相同的情况
    u3 := User{ID: 2, Username: "gopher", IsVip: true}
    if u1 == u3 {
        fmt.Println("u1和u3完全相等")
    } else {
        fmt.Println("u1和u3不相等(ID不同)")
    }
}

运行结果会输出 “u1 和 u2 完全相等”以及“u1 和 u3 不相等( ID 不同)”,符合预期。

这种结构体不仅能直接比较,还能用来做 switch 的 case 条件,本质都是依赖其可比性

(二)不可比较的 struct

不可比较的 struct:包含不可比较类型字段。

如果 struct 里有 slice、map、function 这些“不可比较类型”的字段,那这个 struct 就不能用==比较,强行用会直接编译报错。

比如定义一个包含文章内容和标签的结构体,标签用 slice 存储,再尝试比较就会出错:

package main

import "fmt"

// Article 包含不可比较类型字段Tags(slice)
type Article struct {
    ID    int
    Title string
    Tags  []string // slice是不可比较类型
}

func main() {
    a1 := Article{
        ID:    1,
        Title: "Go struct详解",
        Tags:  []string{"Go", "结构体"},
    }
    a2 := Article{
        ID:    1,
        Title: "Go struct详解",
        Tags:  []string{"Go", "结构体"},
    }

    // 尝试比较两个包含slice的结构体,编译直接报错
    // 错误信息:invalid operation: a1 == a2 (struct containing []string cannot be compared)
    if a1 == a2 {
        fmt.Println("a1和a2相等")
    }
}

遇到这种情况想判断两个结构体是否“语义相等”(比如 slice 元素完全相同),就得自己写比较函数,逐字段校验,尤其是不可比较字段要特殊处理:

package main

import "fmt"

type Article struct {
    ID    int
    Title string
    Tags  []string
}

// 自定义比较函数,判断两个Article是否语义相等
func ArticleEqual(a, b Article) bool {
    // 先比较可比较字段
    if a.ID != b.ID || a.Title != b.Title {
        return false
    }

    // 再比较不可比较字段Tags(slice)
    if len(a.Tags) != len(b.Tags) {
        return false
    }
    for i := range a.Tags {
        if a.Tags[i] != b.Tags[i] {
            return false
        }
    }
    return true
}

func main() {
    a1 := Article{
        ID:    1,
        Title: "Go struct详解",
        Tags:  []string{"Go", "结构体"},
    }
    a2 := Article{
        ID:    1,
        Title: "Go struct详解",
        Tags:  []string{"Go", "结构体"},
    }
    a3 := Article{
        ID:    1,
        Title: "Go struct详解",
        Tags:  []string{"Go", "基础"},
    }

    if ArticleEqual(a1, a2) {
        fmt.Println("a1和a2语义相等")
    }
    if ArticleEqual(a1, a3) {
        fmt.Println("a1和a3语义相等")
    } else {
        fmt.Println("a1和a3语义不相等(Tags不同)")
    }
}

二、Go struct 作为 map key ?

map 的 key 有个硬性要求:必须是可比较类型

所以 struct 能不能当 map key,答案和“能不能比较”完全挂钩——只有可比较的 struct 才能作为 map 的 key

这一点在做缓存、数据去重时特别常用。

(一)可比较 struct 作为 map key

比如做用户积分缓存,用 User 结构体当 key 存储对应的积分,因为 User 的所有字段都是可比较类型,所以完全可行:

package main

import "fmt"

type User struct {
    ID       int
    Username string
}

func main() {
    // 定义map,key为User结构体,value为积分(int)
    scoreMap := make(map[User]int)

    // 往map里存数据
    u1 := User{ID: 1, Username: "gopher"}
    scoreMap[u1] = 95

    // 根据struct key取数据
    if score, ok := scoreMap[u1]; ok {
        fmt.Printf("%s的积分是:%d\n", u1.Username, score)
    }

    // 用新的相同结构体取数据(同样能取到)
    u2 := User{ID: 1, Username: "gopher"}
    if score, ok := scoreMap[u2]; ok {
        fmt.Printf("%s的积分是:%d\n", u2.Username, score)
    }

    // 不同结构体key取数据(取不到)
    u3 := User{ID: 2, Username: "gopher"}
    if score, ok := scoreMap[u3]; ok {
        fmt.Printf("%s的积分是:%d\n", u3.Username, score)
    } else {
        fmt.Printf("未找到%s的积分\n", u3.Username)
    }
}

运行后会正确输出 u1 和 u2 的积分,且 u3 因为 ID 不同取不到数据。

这里要注意:只要两个 struct 的所有字段都相等,它们就是 map 里的同一个 key,不管是不是同一个实例。

(二)不可比较 struct 作为 map key

如果强行用包含 slice、map 等不可比较字段的 struct 当 map key,编译时就会报错,提示 key 类型不可比较:

package main

import "fmt"

type Article struct {
    ID    int
    Tags  []string // slice是不可比较类型
}

func main() {
    // 尝试定义key为Article的map,编译直接报错
    // 错误信息:invalid map key type Article (struct containing []string cannot be compared)
    articleMap := make(map[Article]string)
    articleMap[Article{ID: 1, Tags: []string{"Go"}}] = "已发布"
}

常见问题

Q1. struct 作为 map key 后,修改字段值会怎样?

问题:把 struct 存到 map 里后,如果修改了 struct 的字段值,再用原来的 struct 实例去取数据,还能取到吗?

答案:取不到!因为修改字段后,struct 的“哈希值”变了,map 会认为是新的 key。

package main

import "fmt"

type User struct {
    ID       int
    Username string
}

func main() {
    u1 := User{ID: 1, Username: "gopher"}
    scoreMap := map[User]int{u1: 95}

    // 修改struct的字段值
    u1.Username = "go_dev"

    // 再用u1取数据(取不到)
    if score, ok := scoreMap[u1]; ok {
        fmt.Printf("%s的积分是:%d\n", u1.Username, score)
    } else {
        fmt.Printf("未找到%s的积分\n", u1.Username)
    }

    // 用原来的字段值构造struct才能取到
    u2 := User{ID: 1, Username: "gopher"}
    if score, ok := scoreMap[u2]; ok {
        fmt.Printf("%s的积分是:%d\n", u2.Username, score)
    }
}

解决办法:如果 struct 要作为 map key,尽量把它定义为“不可变”的——要么字段都是基本类型且不轻易修改,要么直接用指针当 key(但要注意指针的可比性是比较地址)。

Q2. 匿名 struct 的比较问题

问题:两个匿名 struct 的字段完全一样,能比较吗?

答案:不能!因为匿名 struct 是不同的类型,即使字段完全相同,Go 也会认为是不同类型,类型不同无法比较。

package main

import "fmt"

func main() {
    // 两个匿名struct,字段完全相同
    a := struct {
        Name string
        Age  int
    }{Name: "张三", Age: 20}

    b := struct {
        Name string
        Age  int
    }{Name: "张三", Age: 20}

    // 尝试比较,编译报错:invalid operation: a == b (mismatched types struct { Name string; Age int } and struct { Name string; Age int })
    if a == b {
        fmt.Println("a和b相等")
    }
}

解决办法:如果需要比较,就定义一个具名 struct,不要用匿名的;如果只是临时用,且要判断语义相等,就逐字段比较。

Q3. 包含指针字段的 struct 比较

问题:struct 里有指针字段,比较时是比较指针指向的值还是地址?

答案:比较指针的地址,不是指向的值!即使两个指针指向的内容完全相同,只要地址不同,struct 比较就会返回不相等。

package main

import "fmt"

type Person struct {
    Name string
    Age  *int
}

func main() {
    age1 := 20
    age2 := 20

    p1 := Person{Name: "张三", Age: &age1}
    p2 := Person{Name: "张三", Age: &age2} // Age指向不同地址

    // 虽然Age指向的值相同,但地址不同,所以p1 != p2
    if p1 == p2 {
        fmt.Println("p1和p2相等")
    } else {
        fmt.Println("p1和p2不相等(Age指针地址不同)")
    }

    // Age指向同一个地址时,才相等
    p3 := Person{Name: "张三", Age: &age1}
    if p1 == p3 {
        fmt.Println("p1和p3相等")
    }
}

解决办法:如果要比较指针指向的内容,需要自定义比较函数,在函数里解引用指针后再比较。

总结

最后总结核心规则:

  1. 可比不可比,全看字段集:所有字段可比较,struct 就可比较;有一个不可比,整体就不可比。
  2. map key 要可比,struct 满足才能上:只有可比较的 struct 才能当map key,含 slice、map 的直接 pass。
  3. key 值修改要谨慎,哈希变化找不到:struct 当 key 后别乱改字段,改了就不是原来的 key了。
  4. 指针比较比地址,语义相等自定义:含指针字段比地址,要比内容写函数。

其实 struct 的可比性和 map key 使用,本质都是 Go 类型系统的基础规则,只要记住“可比较类型”的范围,再结合实际场景,就能彻底掌握。

如果还有其他踩坑经历,欢迎在评论区交流!

版权声明

未经授权,禁止转载本文章。
如需转载请保留原文链接并注明出处。即视为默认获得授权。
未保留原文链接未注明出处或删除链接将视为侵权,必追究法律责任!

本文原文链接: https://fiveyoboy.com/articles/go-compare-struct/

备用原文链接: https://blog.fiveyoboy.com/articles/go-compare-struct/