《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
}
总结
- 可比较的结构体:当结构体的所有字段都是可比较类型时,结构体可比较。
- 不可比较的结构体:若结构体包含不可比较类型的字段,则结构体不可直接比较。
- 深度比较方案:对于不可直接比较的结构体,可使用
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()
) - 适用于需要数据共享的场景
四、最佳实践建议
- 大型结构体一律使用指针传递
- 需修改原始数据时必须使用指针
- 优先使用结构体嵌入而非接口组合复杂类型
- 根据数据共享需求选择值嵌入或指针嵌入
- 通过显式访问保持类型清晰性,避免命名冲突