用 map 来实现 set 数据结构

在线运行open in new window启动 AI 助手open in new window

type Set struct {
	// 空结构体不占空间,编译后是固定的地址
	m map[interface{}]struct{}
}

func NewSet() *Set {
	// map 必须初始化
	return &Set{m: map[interface{}]struct{}{}}
}

func (s *Set) Add(vals ...interface{}) {
	// 添加值,这些值可能是重复的
	for _, val := range vals {
		s.m[val] = struct{}{}
	}
}

func (s *Set) Values() []interface{} {
	var res []interface{}

	// 去重的结果
	for k := range s.m {
		res = append(res, k)
	}

	return res
}

func main() {
	s := NewSet()
	s.Add(1, 2, 3, 1, 2, 3)
	s.Add("one", "two", "three", "one", "two", "three")
	fmt.Println(s.Values()) // [1 2 3 one two three]
}

程序代码使用 Set 来表示去重的数据集合,其中包含一个类型为 map[interface{}]struct{} 的字段 m。这个 map 的键是接口类型,可以接受任何类型的值作为键,值则是空结构体 struct{}。由于空结构体不占空间,因此在编译后,它们是固定的地址。这个 map 就是用来保存集合中的元素的。

程序中定义了 NewSet() 函数来创建一个新的空集合。这个函数会初始化 Set 结构体中的 map 字段 m,确保它不是 nil。

Set 结构体还定义了两个方法:Add() 和 Values()。

Add() 方法接受一个或多个参数,把它们添加到集合中。添加时,程序会遍历这些参数,逐个将它们作为键添加到 Set 结构体中的 map 字段 m 中,并将对应的值设置为一个空结构体。这样,如果重复添加相同的元素,map 中只会保存一次,以实现集合中元素的唯一性。

Values() 方法返回集合中所有元素的列表。在实现过程中,程序先创建一个空的 interface 类型的切片 res,然后遍历 Set 结构体中的 map 字段 m 中所有的键,将它们逐个添加到 res 中。由于 map 中的键已经保证了唯一性,res 中也不会出现重复的元素。

在程序的主函数 main() 中,首先使用 NewSet() 函数创建了一个新的空集合 s,然后通过 Add() 方法向 s 中添加了一些元素。最后,调用 Values() 方法获取集合中所有元素,并将结果打印出来。打印的结果为 [1 2 3 one two three],说明集合中的元素都成功地添加到了 Set 结构体的 map 字段 m 中,并被正确去重。

实现对 map 的 keys 自然序访问

在线运行open in new window启动 AI 助手open in new window

func main() {
	m := make(map[int]string)
	m[1] = "one"
	m[2] = "two"
	m[3] = "three"

	keys := make([]int, 0, len(m))
	for k := range m {
		keys = append(keys, k)
	}

	sort.Ints(keys)
	for _, k := range keys {
		fmt.Printf("%d = %s\n", k, m[k])
	}
}

程序首先创建了一个 map 变量 m,并分别添加了三个键值对,键的类型为整数,值的类型为字符串。

然后,程序创建了一个空的整型切片 keys,用于存储 map 中所有的键。在这个示例中,使用了 make() 函数创建了一个初始容量为 0,长度为 m 中键的个数的切片。

接下来,程序使用 range 循环遍历 m 中的所有键,并将这些键添加到 keys 切片中。由于切片的长度不够,每次添加时切片的容量会自动扩展,保证能够容纳所有键。

然后,程序使用 sort.Ints() 函数将 keys 切片中的键按升序排序。

最后,程序使用 range 循环遍历排序后的 keys 切片,并通过 map 变量 m 输出每个键值对。输出时,程序使用 %d 和 %s 分别表示整型和字符串类型,将键和对应的值依次输出。

实现对 map 的 keys 插入序访问

在线运行open in new window启动 AI 助手open in new window

type InsertedOrderMap struct {
	keys []int
	data map[int]string
}

func NewInsertedOrderMap() *InsertedOrderMap {
	return &InsertedOrderMap{
		data: map[int]string{},
	}
}

func (m *InsertedOrderMap) Add(k int, v string) {
	// TODO 处理重复的 k 的顺序
	m.keys = append(m.keys, k)
	m.data[k] = v
}

func (m *InsertedOrderMap) Iterate(f func(k int, v string)) {
	for _, k := range m.keys {
		f(k, m.data[k])
	}
}

func main() {
	m := NewInsertedOrderMap()

	m.Add(1, "one")
	m.Add(3, "three")
	m.Add(2, "two")
	m.Add(0, "zero")

	m.Iterate(func(k int, v string) {
		fmt.Printf("%d=%s\n", k, v)
	})
}

程序代码定义的 Map 会按照键的插入顺序遍历 Map,并且可以处理重复的键。该 Map 是通过 InsertedOrderMap 结构体实现的。InsertedOrderMap 结构体包含两个字段:

keys 字段是一个整型切片,用于按照插入顺序保存 Map 中所有的键。 data 字段是一个 map,用于保存键值对。它的键是整型,值是字符串类型。 程序中定义了 NewInsertedOrderMap() 函数来创建一个新的 InsertedOrderMap 对象。该函数会返回一个指向 InsertedOrderMap 结构体的指针,并初始化 data 字段,确保它不是 nil。

InsertedOrderMap 结构体还定义了两个方法:Add() 和 Iterate()。

Add() 方法用于向 Map 中添加键值对。它接受两个参数:一个整数 k 和一个字符串 v。程序将这个键值对添加到 data 字段中,同时将键 k 添加到 keys 切片中。注意,由于 keys 切片是按照插入顺序保存键的,因此即使有重复的键,它们在 keys 切片中也应该按照插入顺序排列。

Iterate() 方法用于遍历 Map 中的所有键值对。它接受一个回调函数 f,并将 Map 中每个键值对依次传递给这个函数。在这个示例中,回调函数 f 接受两个参数:一个整数 k 和一个字符串 v,分别表示键和对应的值。在遍历 keys 切片时,程序调用回调函数 f,将当前键 k 和对应的值 m.data[k] 作为参数传递给它。

在程序的主函数 main() 中,首先使用 NewInsertedOrderMap() 函数创建了一个新的 InsertedOrderMap 对象 m。然后,使用 Add() 方法向 m 中添加了四个键值对,键分别为 1、3、2 和 0,值分别为 "one"、"three"、"two" 和 "zero"。

最后,程序使用 Iterate() 方法遍历 Map 中的所有键值对,并将它们输出到控制台。输出时,程序使用 %d 和 %s 分别表示整型和字符串类型,将键和对应的值依次输出。

正确的复制一个 map 对象

在线运行open in new window启动 AI 助手open in new window

func copyMap(m map[string]interface{}) map[string]interface{} {
	cp := make(map[string]interface{})
	for k, v := range m {
		vm, ok := v.(map[string]interface{})
		if ok {
			cp[k] = copyMap(vm)
		} else {
			cp[k] = v
		}
	}

	return cp
}

func main() {
	m1 := map[string]interface{}{"one": 1, "two": 2, "three": 3}
	m2 := m1
	m2["one"] = "111"

	// m1 changed also
	fmt.Println(m1["one"]) // 111
	fmt.Println(m1["one"]) // 111

	// reset m1
	m1["one"] = 1 
	m3 := make(map[string]interface{}, len(m1))
	for k, v := range m1 {
		m3[k] = v
	}

	// m1 no chanes
	fmt.Println(m1["one"]) // 1
	fmt.Println(m3["one"]) // 1

	// deep copy a map
	m4 := map[string]interface{}{"zero": 0, "m1": m1, "m2": m2}
	m5 := copyMap(m4)

	m5["zero"] = "000"
	delete(m5, "m1")
	fmt.Println(m4) // map[m1:map[one:1 three:3 two:2] m2:map[one:1 three:3 two:2] zero:0]
	fmt.Println(m5) // map[m2:map[one:1 three:3 two:2] zero:000]
}

程序中定义了一个 copyMap() 函数,用于深拷贝一个 map[string]interface{} 类型的 Map。该函数接受一个 map[string]interface{} 类型的参数 m,并返回一个新的 map[string]interface{} 类型的 Map。

在实现过程中,程序首先创建一个空的 map[string]interface{} 类型的 Map cp,然后使用 range 循环遍历输入的 Map m 中的所有键值对。对于每个键值对,程序将键 k 和对应的值 v 分别复制到 cp Map 中。

如果值 v 是一个 map[string]interface{} 类型的 Map,则说明这是一个嵌套的 Map,需要递归调用 copyMap() 函数将其深拷贝。否则,直接将值 v 复制到 cp Map 中。

在程序的主函数 main() 中,首先创建了一个 Map m1,并将其赋值给另一个 Map m2。然后,程序将 m2 中键为 "one" 的值修改为 "111",并输出 m1["one"] 的值,此时应该为 "111"。这表明 m2 和 m1 共享同一块内存地址,对 m2 的修改也会影响到 m1。

接着,程序使用 make() 函数创建了一个新的空的 Map m3,并将 m1 中的所有键值对复制到 m3 中。由于是新创建的 Map,因此 m3 和 m1 不共享同一块内存地址。程序将 m1 中键为 "one" 的值修改为 1,并输出 m1["one"] 和 m3["one"] 的值,此时它们应该都为 1。

接下来,程序创建了一个 Map m4,其中包含了一个嵌套的 Map m1 和一个与 m1 共享同一块内存地址的 Map m2。然后,程序使用 copyMap() 函数深拷贝了 m4,并将结果保存在新的 Map m5 中。接着,程序将 m5 中键为 "zero" 的值修改为 "000",并从 m5 中删除了键为 "m1" 的键值对。最后,程序输出 m4 和 m5 的值,以检查深拷贝是否成功。

nil 可以作为 map 的 key 值

在线运行open in new window启动 AI 助手open in new window

func main() {
	m := map[interface{}]int{}

	m["one"] = 1
	m["two"] = 2
	m["three"] = 3

	m[nil] = 100
	fmt.Println(m) // map[<nil>:100 one:1 three:3 two:2]
}

程序首先创建了一个 map[interface{}]int 类型的 Map 变量 m,该 Map 中的键可以是任意类型的值,而值必须是整型。

然后,程序使用 m[key] = value 的形式向 Map 中添加了三个键值对,其中键分别为 "one"、"two" 和 "three",对应的值分别为 1、2 和 3。

接着,程序使用 m[nil] = 100 的形式向 Map 中添加了一个键值对,其中键为 nil,对应的值为 100。

最后,程序通过 fmt.Println() 函数将 Map 变量 m 输出到控制台。

默认的 map 不是线程安全的

在线运行open in new window启动 AI 助手open in new window

func main() {
	m := map[int]int{}

	// fatal error: concurrent map read and map write

	go func() {
		for {
			m[1] = 1
		}
	}()

	go func() {
		for {
			_ = m[1]
		}
	}()

	select {}
}

程序首先创建了一个 map[int]int 类型的 Map 变量 m,该 Map 中的键和值都是整型。

然后,程序启动了两个协程。第一个协程使用一个无限循环不断向 Map 中添加键值对,其中键为 1,值为 1。第二个协程也使用一个无限循环不断从 Map 中读取键为 1 的值。

最后,程序通过 select{} 让主协程一直等待,防止程序退出。

这个程序在运行时会产生一个 "fatal error: concurrent map read and map write" 的错误,这是因为 Go 语言的 Map 不是并发安全的。在多个协程同时进行 Map 的读取和写入操作时,会产生冲突,从而导致运行时错误。

要解决这个问题,可以使用 Go 语言的 sync.Map 类型来实现并发安全的 Map。另外,也可以通过使用互斥锁(mutex)来保证 Map 的读写操作是串行化的,从而避免并发冲突。

线程安全的 sync.Map 对象

在线运行open in new window启动 AI 助手open in new window

func main() {
	var (
		m  sync.Map
		wg sync.WaitGroup
	)

	wg.Add(2)

	go func() {
		defer wg.Done()
		for i := 0; i < 100; i++ {
			m.Store(1, 1)
		}
	}()

	go func() {
		defer wg.Done()
		for i := 0; i < 100; i++ {
			_, _ = m.Load(1)
		}
	}()

	wg.Wait()

	m.Range(func(k, v interface{}) bool {
		fmt.Println(k, v)
		return true
	}) // 1 1
}

程序首先定义了一个 sync.Map 类型的 Map 变量 m,该 Map 是并发安全的,可以在多个协程中同时读取和写入。

然后,程序创建了一个 sync.WaitGroup 类型的变量 wg,并调用了 wg.Add(2) 方法将其计数器加 2,表示有两个协程需要等待。

接着,程序启动了两个协程。第一个协程使用一个循环向 Map 中添加键值对,其中键为 1,值为 1,每次循环添加一次,循环执行 100 次。第二个协程也使用一个循环从 Map 中读取键为 1 的值,每次循环读取一次,循环执行 100 次。

两个协程都执行完毕后,程序调用 wg.Wait() 方法等待所有协程执行完毕,然后再执行后面的代码。

最后,程序通过 m.Range() 方法遍历 Map 中的所有键值对,并输出它们的值。由于在之前的协程中,已经向 Map 中添加了 100 个键值对,并且也从 Map 中读取了这些键值对,因此在遍历 Map 时只会输出键为 1,值为 1 的一个键值对。

用 Mutex 实现安全的 map 对象

在线运行open in new window启动 AI 助手open in new window

type ThreadSafeMap struct {
	m  map[string]interface{}
	mu *sync.Mutex
}

func NewThreadSafeMap() *ThreadSafeMap {
	return &ThreadSafeMap{
		m:  map[string]interface{}{},
		mu: &sync.Mutex{},
	}
}

func (sm *ThreadSafeMap) Set(k string, v interface{}) {
	sm.mu.Lock()
	defer sm.mu.Unlock()

	sm.m[k] = v
}

func (sm *ThreadSafeMap) Get(k string) (interface{}, bool) {
	sm.mu.Lock()
	defer sm.mu.Unlock()

	v, ok := sm.m[k]
	return v, ok
}

func (sm *ThreadSafeMap) Len() int {
	sm.mu.Lock()
	defer sm.mu.Unlock()

	return len(sm.m)
}

func (sm *ThreadSafeMap) Iterate(f func(k string, v interface{})) {
	sm.mu.Lock()
	defer sm.mu.Unlock()

	for k, v := range sm.m {
		f(k, v)
	}
}

func main() {
	wg := sync.WaitGroup{}

	sm := NewThreadSafeMap()
	for i := 0; i < 10; i++ {
		wg.Add(1)

		go func(idx int) {
			defer wg.Done()
			sm.Set(fmt.Sprintf("key_%d", idx), fmt.Sprintf("value_%d", idx))
		}(i)
	}

	wg.Wait()

	fmt.Println(sm.Len())
	sm.Iterate(func(k string, v interface{}) {
		fmt.Printf("%s = %v\n", k, v)
	})
}

包含了一个普通的 Map 变量 m 和一个互斥锁 mu。互斥锁 mu 用于保证 Map 的读写操作是串行化的,从而避免多个协程同时读取和写入 Map 时可能产生的并发冲突。

然后,程序定义了一系列方法来对 Map 进行读写和遍历操作。其中,Set() 方法用于向 Map 中设置一个键值对,Get() 方法用于从 Map 中获取指定键的值,Len() 方法用于获取 Map 的键值对数量,Iterate() 方法用于遍历 Map 中的所有键值对。

接着,程序创建了一个 sync.WaitGroup 类型的变量 wg,并使用一个循环启动了 10 个协程。每个协程都调用 wg.Add(1) 将计数器加 1,并使用一个匿名函数向 Map 中设置一个键值对。匿名函数使用 fmt.Sprintf() 函数生成键和值,其中键的格式为 "key_{index}",值的格式为 "value_{index}",其中 index 表示协程的索引。

所有协程都执行完毕后,程序调用 wg.Wait() 方法等待它们执行完毕。然后,程序调用 Len() 方法获取 Map 的键值对数量,并输出到控制台。最后,程序使用 Iterate() 方法遍历 Map 中的所有键值对,并输出它们的值。

Last Updated:
Contributors: Bob Wang