首页 > 其他分享 >深入解析 Go 中 Map

深入解析 Go 中 Map

时间:2024-08-31 19:50:57浏览次数:12  
标签:Map hash 哈希 map unsafe key Go 解析 Pointer

0 前言

Go 语言中的 map 是一种内建的数据结构,用于存储键值对。它类似于其他编程语言中的哈希表或字典,提供了快速的插入、删除和查找操作。本文将深入浅出介绍map基本概念、使用方式、核心原理、性能以及最佳实践,帮助读者更好的理解和使用map

如果您觉得有帮助,请关注我,另公众号【小张的编程世界】,如有任何错误或建议,欢迎指出。感谢您的阅读!

1 map基本概念

定义: map 是一种无序的键值对集合,键和值的类型可以是任何支持相等运算的类型。key 的数据类型必须为可比较的类型,chanmapfunc不可比较。

语法:

var m map[string]int // 声明一个键为字符串,值为整数的 map

零值: map 的零值是 nil,一个 nilmap 不能存储键值对,必须进行初始化。

并发冲突: map 不是并发安全的数据结构,倘若存在并发读写行为,会抛出 fatal error,并且这是一种比较严重的错误,是无法使用recover操作捕获的,可能直接导致整个服务重启或者不可用。

2 map 使用

2.1 初始化

使用make创建:

// 创建一个空的 map,容量可省略
m := make(map[string]int,10) 

直接赋值初始化:

// 创建并初始化一个 map
m := map[string]int{"a": 1, "b": 2, "c": 3} 

2.2 插入或更新元素

m["key"] = value // 插入或更新键为 "key" 的值

2.3 获取元素

value := m["key"] // 获取键为 "key" 的值

如果键不存在,返回值类型的零值。要检查键是否存在,可以使用下面的语法:

value, ok := m["key"] // 如果键存在,ok 为 true,否则为 false

2.4 删除元素

delete(m, "key") // 删除键为 "key" 的键值对

2.5 遍历

map 的遍历是无序的

for key, value := range m {
    fmt.Println(key, value) // 输出键和值
}

若不关心value值也可采取底下这种方式忽略

for key := range m {
		fmt.Println(key)
	}

3 核心原理

Go 语言中的 map 是通过哈希表 (hash table) 实现的。它提供了快速的键值对存储、查找、插入和删除功能。为了深入理解 map 的底层实现,我们需要探讨几个关键的概念:哈希函数哈希桶键冲突处理扩容机制

哈希表的核心任务是通过哈希函数将键映射到哈希函桶中,然后在桶中存储键值对。

3.1 哈希函数

定义: 哈希函数是一种将输入数据(通常是任意长度)映射到定长输出数据(哈希值)的函数。

特性:

  1. 确定性: 即对于相同的输入,总是产生相同的输出。这一特性保证了哈希表等数据结构中的键值对能够准确地存储和检索。
  2. 高效性: 对于任意输入,哈希函数能够在短时间内生成哈希值。
  3. 均匀分布: 理想的哈希函数应将输入数据均匀地分布到所有可能的哈希值范围内,尽量避免哈希冲突(不同的输入映射到相同的哈希值)。
  4. 不可逆性: 给定一个哈希值,很难找到其对应的输入。
  5. 冲突性: 尽管均匀分布,但是由于输入域(key)无穷大,输出域(hash 值)有限,因此大数据量下必然存在不同 key 映射到相同 hash 值。

3.2 哈希桶

每个桶 (bucket) 是一个存储多个键值对的容器。一个桶通常可以存储 8 个键值对。当一个桶装满时,新的键值对将会被放入到一个新的桶中,形成链表。

在这里插入图片描述

3.3 键冲突处理

在 Go 语言的 map 实现中,哈希冲突是通过一种叫做链地址法(也称为拉链法)的技术来解决的。

链地址法的核心思想是将冲突的键值对存储在同一个桶中,并通过溢出桶 (overflow buckets) 来继续存储更多的键值对。

桶的基本结构:每个桶可以存储 8 个键值对。桶中不仅存储键和值,还存储一个 top hash,即键哈希值的高 8 位,用于快速比较和查找。

溢出桶:如果一个桶被填满(即包含了 8 个键值对),那么后续的键值对将被存储在该桶的溢出桶中。溢出桶通过指针链接形成一个链表,这样可以继续存储新的键值对。

3.4 查找操作

当在 map 中查找一个键时,Go 会根据键的哈希值定位到一个特定的桶,然后依次检查桶中的键,比较它们的 top hash 和实际键值。

匹配 top hash:首先比较 top hash,因为这是一个快速的初步检查。如果 top hash 不匹配,那么键肯定不在该桶中。

遍历溢出桶:如果在初始桶中找不到目标键,查找过程会继续在溢出桶中搜索,直到找到键或确定键不存在。

3.5 插入操作

  1. 计算哈希值:对键进行哈希运算,计算出哈希值,并确定目标桶的位置。
  2. 插入到桶中:检查目标桶是否有空闲槽位。如果有空位,则直接插入;否则,进入下一步。
  3. 使用溢出桶:如果目标桶已满(即所有 8 个槽位都被占用),Go 会创建或使用现有的溢出桶,将新键值对存储在溢出桶中。

3.6 扩容机制

map 中的键值对数量增多,导致哈希冲突频繁发生时,Go 的 map 会触发扩容机制:

  • 渐进式扩容:扩容时,map 会重新分配一个更大的哈希桶数组(通常是当前大小的两倍)。原有的键值对会逐步搬移到新的桶中。每次插入、删除或查找操作时,都会触发一些键值对的迁移,直到整个迁移过程完成。
  • 再哈希:在扩容过程中,map 会重新计算键的哈希值,并将它们分配到新的桶中。这一过程有助于减少冲突,优化查找效率。

4 源码走读

go版本为1.22

4.1 数据结构

hmap

type hmap struct {
	count     int  // 键值对总数,可以使用 len() 内置函数来获取
	flags     uint8 // 标识位,用于记录和管理map的状态,比如是否被并发读写
	B         uint8 // 计算哈希桶数值的,哈希桶数量为2的B次方
	noverflow uint16  // 溢出桶数量
	hash0     uint32 // 哈希种子
	buckets   unsafe.Pointer // 是一个指向 Bucket 数组的指针 实际存储数据的地方
	oldbuckets unsafe.Pointer // 扩容时,保存旧的桶数组的指针
	nevacuate  uintptr     // 扩容时的进度标识。小于这个值的桶已经完成了迁移   
	extra *mapextra  // 指向 mapextra 结构体的指针
}

mapextra:

type mapextra struct {
	overflow    *[]*bmap // 包含了 hmap.buckets 的溢出桶
	oldoverflow *[]*bmap //包含了 hmap.oldbuckets 的溢出桶
	nextOverflow *bmap // 下一个可用的溢出桶
}

bmap:

const bucketCnt = 8
// bmap 就是 map 中的桶,可以存储 8 组 key-value 对的数据,以及一个指向下一个溢出桶的指针
type bmap struct {
	tophash [bucketCnt]uint8
}

// 上面的 bmap 只是表面(runtime/map.go)的结构,编译期间会动态地创建一个新的结构
// 每组 key-value 对数据包含 key 高 8 位 hash 值 tophash,key 和 value
type bmap struct {
   tophash  [bucketCnt]uint8   
   keys     [bucketCnt]keytype   
   values   [bucketCnt]valuetype    
   pad      uintptr
   overflow uintptr
}

大概结构图如下:

在这里插入图片描述

4.2 图解流程

4.2.1 初始化

源码:

func makemap(t *maptype, hint int, h *hmap) *hmap {
	mem, overflow := math.MulUintptr(uintptr(hint), t.Bucket.Size_)
  // 超出内存可配置范围 置为0
	if overflow || mem > maxAlloc {
		hint = 0
	}

	// initialize Hmap
  // 为nil 初始化
	if h == nil {
		h = new(hmap)
	}
  // 随机一个hash种子
	h.hash0 = uint32(rand())

	// Find the size parameter B which will hold the requested # of elements.
	// For hint < 0 overLoadFactor returns false since hint < bucketCnt.
  // 计算桶数组容量
	B := uint8(0)
	for overLoadFactor(hint, B) {
		B++
	}
	h.B = B

	// allocate initial hash table
	// if B == 0, the buckets field is allocated lazily later (in mapassign)
	// If hint is large zeroing this memory could take a while.
	if h.B != 0 {
		var nextOverflow *bmap
    // 初始化桶数组
		h.buckets, nextOverflow = makeBucketArray(t, h.B, nil)
		// 容量比较大,会提前申请一批溢出桶
    if nextOverflow != nil {
			h.extra = new(mapextra)
			h.extra.nextOverflow = nextOverflow
		}
	}

	return h
}

初始化代码流程图如下:

在这里插入图片描述

4.2.2 插入数据

大概分为以下几步:

  1. 根据 key计算出hash
  2. 根据hash值找到对应的桶
  3. 判断当前是否正在扩容中,如果是则进行迁移工作,推进渐进式扩容
  4. 遍历该桶,先遍历本桶再遍历溢出桶,遍历过程中会对第一个空位进行标记,同时会判断插入的 key 是否存在桶中
  5. 如果key存在,则更新对应的 value 并退出遍历
  6. 否则桶遍历完之后,将元素插入标记的第一个空位
  7. 如果当前可扩容,则进行扩容,并重新返回第(2)步

源码:

func mapassign(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
  // 未初始化 直接panic
	if h == nil {
		panic(plainError("assignment to entry in nil map"))
	}
	if raceenabled {
		callerpc := getcallerpc()
		pc := abi.FuncPCABIInternal(mapassign)
		racewritepc(unsafe.Pointer(h), callerpc, pc)
		raceReadObjectPC(t.Key, key, callerpc, pc)
	}
	if msanenabled {
		msanread(key, t.Key.Size_)
	}
	if asanenabled {
		asanread(key, t.Key.Size_)
	}
  // 并发写 直接 fatal
	if h.flags&hashWriting != 0 {
		fatal("concurrent map writes")
	}
  // 通过hash种子计算key对应的hash值
	hash := t.Hasher(key, uintptr(h.hash0))

	// Set hashWriting after calling t.hasher, since t.hasher may panic,
	// in which case we have not actually done a write.
	// 设置写标记
  h.flags ^= hashWriting

  // 桶数组为空则进行初始化
	if h.buckets == nil {
		h.buckets = newobject(t.Bucket) // newarray(t.Bucket, 1)
	}

again:
  // 通过hash找到对应的桶索引
	bucket := hash & bucketMask(h.B)
  // 是否在正在扩容
	if h.growing() {
    // 进行迁移工作
		growWork(t, h, bucket)
	}
  // 获取bucket的地址
	b := (*bmap)(add(h.buckets, bucket*uintptr(t.BucketSize)))
	// 获取hash的高8位
  top := tophash(hash)

  // 3个中间变量 标记 bucket 中出现的第一个空位
	var inserti *uint8  // tophash位置
	var insertk unsafe.Pointer // key位置
	var elem unsafe.Pointer // value位置
bucketloop:
	for {
    // 外层循环遍历桶链表 先本桶在溢出桶
		for i := uintptr(0); i < bucketCnt; i++ {
      // 内层循环遍历桶内key-values
			if b.tophash[i] != top {
        //如果高8位不同,
        //如果某个空位的 tophash 是空且 inserti 为 nil,则尝试将 inserti、insertk elem 指向第一个空位,方便后续插入
				if isEmpty(b.tophash[i]) && inserti == nil {
					inserti = &b.tophash[i]
					insertk = add(unsafe.Pointer(b), dataOffset+i*uintptr(t.KeySize))
					elem = add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.KeySize)+i*uintptr(t.ValueSize))
				}
        // 如果整个桶没有数据(包括溢出桶)直接break
				if b.tophash[i] == emptyRest {
					break bucketloop
				}
				continue
			}
      // 高8位相等
			k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.KeySize))
			if t.IndirectKey() {
				k = *((*unsafe.Pointer)(k))
			}
      // 对比key和所插入的key是否相同
			if !t.Key.Equal(key, k) {
				continue
			}
      
			// already have a mapping for key. Update it.
      // 存在则更新
			if t.NeedKeyUpdate() {
				typedmemmove(t.Key, k, key)
			}
			elem = add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.KeySize)+i*uintptr(t.ValueSize))
			goto done
		}
    // 继续遍历溢出桶
		ovf := b.overflow(t)
		if ovf == nil {
			break
		}
		b = ovf
	}

	// Did not find mapping for key. Allocate new cell & add entry.

	// If we hit the max load factor or we have too many overflow buckets,
	// and we're not already in the middle of growing, start growing.
  // 当前不处于扩容中,超过最大负载因子或者太多溢出桶的话,开始扩容
	if !h.growing() && (overLoadFactor(h.count+1, h.B) || tooManyOverflowBuckets(h.noverflow, h.B)) {
		// 扩容
    hashGrow(t, h)
    // 重新开始,扩容会导致key的位置发生变化
		goto again // Growing the table invalidates everything, so try again
	}
  //如果没空位的话,创建一个溢出桶,然后插入
	if inserti == nil {
		// The current bucket and all the overflow buckets connected to it are full, allocate a new one.
		newb := h.newoverflow(t, b)
		inserti = &newb.tophash[0]
		insertk = add(unsafe.Pointer(newb), dataOffset)
		elem = add(insertk, bucketCnt*uintptr(t.KeySize))
	}

	// store new key/elem at insert position
	if t.IndirectKey() {
		kmem := newobject(t.Key)
		*(*unsafe.Pointer)(insertk) = kmem
		insertk = kmem
	}
	if t.IndirectElem() {
		vmem := newobject(t.Elem)
		*(*unsafe.Pointer)(elem) = vmem
	}
	typedmemmove(t.Key, insertk, key)
	*inserti = top
	h.count++

done:
	if h.flags&hashWriting == 0 {
		fatal("concurrent map writes")
	}
	h.flags &^= hashWriting
	if t.IndirectElem() {
		elem = *((*unsafe.Pointer)(elem))
	}
	return elem
}

插入数据代码流程图如下:

在这里插入图片描述

4.2.3 获取数据

大概分为以下几步:

  1. 根据 key计算出hash
  2. 根据hash值找到对应的桶
  3. 判断当前是否正在扩容中
  4. 如果是2倍扩容,将遍历起点往前移2的B次方
  5. 沿着桶链表依次遍历各个桶内的 key-value
  6. 命中相同的 key,则返回 value
  7. 倘若 key 不存在,则返回零值

源码:

func mapaccess1(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
	if raceenabled && h != nil {
		callerpc := getcallerpc()
		pc := abi.FuncPCABIInternal(mapaccess1)
		racereadpc(unsafe.Pointer(h), callerpc, pc)
		raceReadObjectPC(t.Key, key, callerpc, pc)
	}
	if msanenabled && h != nil {
		msanread(key, t.Key.Size_)
	}
	if asanenabled && h != nil {
		asanread(key, t.Key.Size_)
	}
  // 未初始化或者数量为0 直接返回零值
	if h == nil || h.count == 0 {
		if err := mapKeyError(t, key); err != nil {
			panic(err) // see issue 23734
		}
		return unsafe.Pointer(&zeroVal[0])
	}
  // 其他协程在并发写的话直接fatal
	if h.flags&hashWriting != 0 {
		fatal("concurrent map read and map write")
	}
  // 计算key对应的hash值
	hash := t.Hasher(key, uintptr(h.hash0))
	m := bucketMask(h.B)
	b := (*bmap)(add(h.buckets, (hash&m)*uintptr(t.BucketSize)))
	// 判断oldbuckets是否为空,不为空则处于扩容中
  if c := h.oldbuckets; c != nil {
    // 如果是2倍扩容
		if !h.sameSizeGrow() {
			// There used to be half as many buckets; mask down one more power of two.
      // 有一半的元素可能在靠前的旧桶位置,所以这里对 m 除以 2。为了确保能够遍历到所有可能存在的位置
			m >>= 1
		}
		oldb := (*bmap)(add(c, (hash&m)*uintptr(t.BucketSize)))
    // 若旧桶还没迁移完
		if !evacuated(oldb) {
			b = oldb
		}
	}
  // 获得hash 高8位
	top := tophash(hash)
bucketloop:
  // 从b开始遍历桶 包括溢出桶
	for ; b != nil; b = b.overflow(t) {
		for i := uintptr(0); i < bucketCnt; i++ {
			 // 比较高8位
      if b.tophash[i] != top {
				if b.tophash[i] == emptyRest {
					break bucketloop
				}
				continue
			}
			k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.KeySize))
			if t.IndirectKey() {
				k = *((*unsafe.Pointer)(k))
			}
      // 找了key
			if t.Key.Equal(key, k) {
				e := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.KeySize)+i*uintptr(t.ValueSize))
				if t.IndirectElem() {
					e = *((*unsafe.Pointer)(e))
				}
				return e
			}
		}
	}
  // 没找到,返回类型对应的零值
	return unsafe.Pointer(&zeroVal[0])
}

获取数据代码流程图如下:

在这里插入图片描述

4.2.4 删除数据

大概分为以下几步:

  1. 根据 key计算出hash
  2. 根据hash值找到对应的桶
  3. 判断当前是否正在扩容中,如果是则进行迁移工作,推进渐进式扩容
  4. 遍历该桶,先遍历本桶再遍历溢出桶
  5. key存在,删除对应的key-value对,并将当前位置的 tophash置为空

源码:

func mapdelete(t *maptype, h *hmap, key unsafe.Pointer) {
	if raceenabled && h != nil {
		callerpc := getcallerpc()
		pc := abi.FuncPCABIInternal(mapdelete)
		racewritepc(unsafe.Pointer(h), callerpc, pc)
		raceReadObjectPC(t.Key, key, callerpc, pc)
	}
	if msanenabled && h != nil {
		msanread(key, t.Key.Size_)
	}
	if asanenabled && h != nil {
		asanread(key, t.Key.Size_)
	}
  // 未初始化或为空直接返回
	if h == nil || h.count == 0 {
		if err := mapKeyError(t, key); err != nil {
			panic(err) // see issue 23734
		}
		return
	}
  // 其他协程在并发写直接fatal
	if h.flags&hashWriting != 0 {
		fatal("concurrent map writes")
	}

  // 根据key 获得hash值
	hash := t.Hasher(key, uintptr(h.hash0))

	// Set hashWriting after calling t.hasher, since t.hasher may panic,
	// in which case we have not actually done a write (delete).
	// 标记为正在写
  h.flags ^= hashWriting

  // 找到桶位置
	bucket := hash & bucketMask(h.B)
  // 是否在扩容
	if h.growing() {
    // 进行迁移工作
		growWork(t, h, bucket)
	}
	b := (*bmap)(add(h.buckets, bucket*uintptr(t.BucketSize)))
	bOrig := b
	top := tophash(hash)
search:
	for ; b != nil; b = b.overflow(t) {
    // 遍历桶链表
		for i := uintptr(0); i < bucketCnt; i++ {
      // 遍历桶内key-value对
			if b.tophash[i] != top {
        // 桶内没数据直接break
				if b.tophash[i] == emptyRest {
					break search
				}
				continue
			}
			k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.KeySize))
			k2 := k
			if t.IndirectKey() {
				k2 = *((*unsafe.Pointer)(k2))
			}
      // key不相等继续遍历
			if !t.Key.Equal(key, k2) {
				continue
			}
			// Only clear key if there are pointers in it.
			if t.IndirectKey() {
				*(*unsafe.Pointer)(k) = nil
			} else if t.Key.PtrBytes != 0 {
				memclrHasPointers(k, t.Key.Size_)
			}
			e := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.KeySize)+i*uintptr(t.ValueSize))
			if t.IndirectElem() {
				*(*unsafe.Pointer)(e) = nil
			} else if t.Elem.PtrBytes != 0 {
				memclrHasPointers(e, t.Elem.Size_)
			} else {
				memclrNoHeapPointers(e, t.Elem.Size_)
			}
      // 高8位置为空
			b.tophash[i] = emptyOne
			// If the bucket now ends in a bunch of emptyOne states,
			// change those to emptyRest states.
			// It would be nice to make this a separate function, but
			// for loops are not currently inlineable.
			if i == bucketCnt-1 {
				if b.overflow(t) != nil && b.overflow(t).tophash[0] != emptyRest {
					goto notLast
				}
			} else {
				if b.tophash[i+1] != emptyRest {
					goto notLast
				}
			}
      // 向前遍历将tophash 为 emptyOne的更新为emptySet
			for {
				b.tophash[i] = emptyRest
				if i == 0 {
					if b == bOrig {
						break // beginning of initial bucket, we're done.
					}
					// Find previous bucket, continue at its last entry.
					c := b
					for b = bOrig; b.overflow(t) != c; b = b.overflow(t) {
					}
					i = bucketCnt - 1
				} else {
					i--
				}
				if b.tophash[i] != emptyOne {
					break
				}
			}
		notLast:
      // 删除成功 count-1
			h.count--
			// Reset the hash seed to make it more difficult for attackers to
			// repeatedly trigger hash collisions. See issue 25237.
			if h.count == 0 {
        // 如果数量为0 更新hash种子
				h.hash0 = uint32(rand())
			}
			break search
		}
	}
  // 校验是否有并发写
	if h.flags&hashWriting == 0 {
		fatal("concurrent map writes")
	}
  // 清除标志
	h.flags &^= hashWriting
}

删除数据代码流程大概如下:

在这里插入图片描述

4.2.5 遍历数据

我们都知道map遍历是无序的,今天就从源码角度来分析下

源码:

func mapiterinit(t *maptype, h *hmap, it *hiter) {
	if raceenabled && h != nil {
		callerpc := getcallerpc()
		racereadpc(unsafe.Pointer(h), callerpc, abi.FuncPCABIInternal(mapiterinit))
	}

	it.t = t
  // 未初始化或数量为0 直接返回
	if h == nil || h.count == 0 {
		return
	}

	if unsafe.Sizeof(hiter{})/goarch.PtrSize != 12 {
		throw("hash_iter size incorrect") // see cmd/compile/internal/reflectdata/reflect.go
	}
	it.h = h

	// grab snapshot of bucket state
	it.B = h.B
	it.buckets = h.buckets
	if t.Bucket.PtrBytes == 0 {
		// Allocate the current slice and remember pointers to both current and old.
		// This preserves all relevant overflow buckets alive even if
		// the table grows and/or overflow buckets are added to the table
		// while we are iterating.
		h.createOverflow()
		it.overflow = h.extra.overflow
		it.oldoverflow = h.extra.oldoverflow
	}

	// decide where to start
  // 决定从哪里开始(随机的)
	r := uintptr(rand())
  // 选择从哪个bucket开始遍历
	it.startBucket = r & bucketMask(h.B)
  // 选择从bucket的哪个元素开始遍历
	it.offset = uint8(r >> h.B & (bucketCnt - 1))

	// iterator state
	it.bucket = it.startBucket

	// Remember we have an iterator.
	// Can run concurrently with another mapiterinit().
	if old := h.flags; old&(iterator|oldIterator) != iterator|oldIterator {
		atomic.Or8(&h.flags, iterator|oldIterator)
	}

	mapiternext(it) // 真正遍历的函数
}

标签:Map,hash,哈希,map,unsafe,key,Go,解析,Pointer
From: https://blog.csdn.net/luozong2689/article/details/141684428

相关文章

  • 深入解析 Go 中 Slice
    0前言slice是一种灵活且强大的数据结构,它在功能上类似于其他编程语言中的数组,但提供了更多的灵活性。与数组不同,slice允许动态调整长度,使其在大多数场景中更加适用。本文将深入解析slice的基本概念及底层实现原理,并通过分析一些面试中常见的易错题,加深对slice的理......
  • 2024-08-31:用go语言,给定一个数组apple,包含n个元素,每个元素表示一个包裹中的苹果数量;
    2024-08-31:用go语言,给定一个数组apple,包含n个元素,每个元素表示一个包裹中的苹果数量;另一个数组capacity包含m个元素,表示m个不同箱子的容量。有n个包裹,每个包裹内装有指定数量的苹果,以及m个箱子,每个箱子的容量不同。任务是将这n个包裹中的所有苹果重新分配到箱子中,最小化所需的......
  • 2024-08-31:用go语言,给定一个数组apple,包含n个元素,每个元素表示一个包裹中的苹果数量;
    2024-08-31:用go语言,给定一个数组apple,包含n个元素,每个元素表示一个包裹中的苹果数量;另一个数组capacity包含m个元素,表示m个不同箱子的容量。有n个包裹,每个包裹内装有指定数量的苹果,以及m个箱子,每个箱子的容量不同。任务是将这n个包裹中的所有苹果重新分配到箱子中,最小化所需的箱子......
  • zlib1.dll丢失全解析:压缩与解压修复的专家级策略
    zlib1.dll是一个与压缩和解压缩相关的动态链接库(DLL)文件,通常与zlib库的功能实现有关。这个DLL文件可能包含了处理数据压缩、资源管理和与其他系统组件交互等功能所需的函数和资源,对于确保使用zlib库的应用程序或服务的正常运行非常重要。当zlib1.dll文件丢失时,可能会导致以......
  • Spring高手之路22——AOP切面类的封装与解析
    1.AOP是如何收集切面类并封装的?在 Spring 中,AOP(Aspect-OrientedProgramming,面向切面编程)通过以下几个步骤收集切面类并进行封装:1.定义切面类:切面类通过 @Aspect 注解来标记,表示这是一个切面。在切面类中定义通知(advice),例如 @Before、@After、@Around 等,用于指定在目标方法......
  • Go实战全家桶之一:goconfig依赖注入扩展之自动注入配置项、工业级巨匠
    开源地址:goconfig:gitclonehttps://gitee.com/ichub/go.git基础结构packageichubconfigimport("gitee.com/ichub/goconfig/common/base/basedto""gitee.com/ichub/goconfig/common/base/baseutils/reflectutils""github.com/gogf/......
  • Goolge earth studio 进阶4——路径修改与平滑
    如果我们希望在大约中途时获得更多的城市鸟瞰视角。可以将相机拖动到这里并创建一个新的关键帧。camera_target_clip_7EarthStudio会自动平滑我们的路径,所以当我们通过这个关键帧时,不是一个生硬的角度,而是一个平滑的曲线。camera_target_clip_8路径上有贝塞尔控制......
  • AI写论文文献综述全指南:从理论到实践的全面解析
    在当今文献资料数量呈爆炸式的时代,如何快速的撰写一份高质量的论文文献综述成为了不得不面对的难题。随着人工智能技术的发展为文献综述的撰写提供了新的思路和方法,利用AI写论文文献综述可以大大的提高论文写作效率和质量。一、引言面对海量的文献资料,需要我们具备较高的学术......
  • Go plan9 汇编: 打通应用到底层的任督二脉
    原创文章,欢迎转载,转载请注明出处,谢谢。0.前言作为一个严肃的Gopher,了解汇编是必须的。本汇编系列文章会围绕基本的Go程序介绍汇编的基础知识。1.Go程序到汇编首先看一个简单到令人发指的示例:packagemainfuncmain(){ a:=1 print(a)}运行程序,输出:#gorun......
  • 深入解析多商户商城系统源码:如何开发直播商城小程序?
    本篇文章,小编将深入解析多商户商城系统源码的关键技术,并详细探讨如何基于这些源码开发一个功能完善的直播商城小程序。一、多商户商城系统源码的核心构架多商户商城系统源码的核心在于其能够支持多个商户独立运营,但同时又在一个统一的平台上实现数据的共享与整合。这个系统的核心构......