当前位置: 首页 > news >正文

【十二】Golang 映射

💢欢迎来到张胤尘的开源技术站
💥开源如江河,汇聚众志成。代码似星辰,照亮行征程。开源精神长,传承永不忘。携手共前行,未来更辉煌💥

文章目录

  • 映射
    • 映射的定义
    • 映射初始化
      • `make` 函数
      • 使用字面量
    • 源码分析
      • 数据结构
        • `hmap`
        • `bmap`
      • 数据存储
      • 键值访问
        • 竞态检测
        • `Sanitizer` 检测
        • 空检查
        • 并发写检查
        • 哈希值计算
        • 桶定位
        • 扩容情况处理
        • 桶内查找
      • 键值插入、扩容机制
        • 参数检查
        • 竞态检测
        • `Sanitizer` 检测
        • 并发检测
        • 哈希值计算
        • 初始化桶数组
        • 定位桶和处理扩容
        • 遍历桶并检查键
        • 检查是否需要扩容
          • `overLoadFactor`
          • `tooManyOverflowBuckets`
        • 插入或更新键值对
          • 插入位置为空
          • 存储新的键值对
        • 返回值的地址
      • 键值删除
        • 竞态检测
        • `Sanitizer` 检测
        • 参数检查
        • 并发检测
        • 设置写入标记
        • 计算哈希值和目标桶
        • 处理扩容
        • 定位目标桶
        • 查找目标键
        • 删除目标键值对
        • 清理空闲槽
        • 重置哈希种子
        • 清除写入标记
    • 常用操作
      • 访问映射值
      • 检查键是否存在
      • 修改映射的值
      • 添加键值对
      • 删除键值对
      • 遍历映射
      • 获取映射长度
      • 映射嵌套
      • 映射的拷贝
      • 清空映射
    • 常见问题
      • 检查键是否存在
      • 初始化映射时指定容量
      • 避免并发修改
      • 不使用映射存储大量的数据

映射

golagn 中,映射是一种键值对的集合,其中每个键(key)都唯一地映射到一个值(value)。它类似于 c/c++ 中的 std::unordered_map,但是具体实现上还是有很大的区别,下面会进行详细的剖析。

映射的定义

映射的类型定义为:

map[KeyType]ValueType
  • KeyType:键的类型,必须是可比较的(可以进行 ==!= 比较)。其中常见的可比较类型包括:
    • 基本类型:int, float64, string, bool 等。
    • 复合类型:指针、通道、接口、包含可比较字段的结构体。
  • ValueType:值的类型可以是任何类型,包括基本类型、自定义类型、切片、结构体、映射等。

映射初始化

golang 中提供了两种初始化映射的方式:make 函数、使用字面量。

make 函数

makegolang 中用于初始化切片、映射和通道的标准函数。对于映射来说,make 的语法如下:

m := make(map[KeyType]ValueType)

例如,创建一个空的映射,其中键为 string 类型,值为 int 类型,如下所示:

m := make(map[string]int)

另外在 golang 中可以为映射指定初始容量(如果不指定,默认容量为零)。

例如,创建一个初始容量为 10 的映射,如下所示:

m := make(map[string]int, 10)

这种方式创建映射中的键值对数量为 0,后续可以通过赋值操作添加键值对。

使用字面量

映射的字面量语法允许在声明时直接初始化键值对。这种方式适用于在编译时已知数据的情况。

语法如下所示:

m := map[KeyType]ValueType{Key1: Value1,Key2: Value2,// ...
}

例如:

m := map[string]int{"a": 1,"b": 2,"c": 3,
}

源码分析

下面主要针对 map 中的数据结构、数据存储、键值访问、键值插入和扩容机制、键值删除五大部分源码进行剖析。

数据结构

map 的内部由两个核心数据结构组成:hmapbmap

hmap

hmapgolangmap 的底层核心数据结构之一,它封装了哈希表的所有关键信息。源码如下所示:

源码位置:src/runtime/map.go

// A header for a Go map.
type hmap struct {// Note: the format of the hmap is also encoded in cmd/compile/internal/reflectdata/reflect.go.// Make sure this stays in sync with the compiler's definition.count     int // # live cells == size of map.  Must be first (used by len() builtin)flags     uint8B         uint8  // log_2 of # of buckets (can hold up to loadFactor * 2^B items)noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for detailshash0     uint32 // hash seedbuckets    unsafe.Pointer // array of 2^B Buckets. may be nil if count==0.oldbuckets unsafe.Pointer // previous bucket array of half the size, non-nil only when growingnevacuate  uintptr        // progress counter for evacuation (buckets less than this have been evacuated)extra *mapextra // optional fields
}
  • count:表示当前 map 中存储的键值对数量,len() 直接访问它。
  • flags:用于存储一些标志位,例如是否正在扩容、是否需要清理等。
  • B:表示桶的数量为 2^B。例如,如果 B = 3,则桶的数量为 2^3 = 8B 的值决定了 buckets 数组的大小。
  • noverflow:表示溢出桶的数量。当一个桶满了(最多存储 8 对键值对),会创建一个溢出桶,并通过链表连接。
  • hash0:哈希种子,用于计算键的哈希值。通过哈希种子可以避免哈希冲突,增加哈希值的随机性。
  • buckets:指向底层桶数组的指针,桶数组的大小为 2^B,每个桶是一个 bmap 结构体。
  • oldbuckets:在扩容时,旧的桶数组会存储在这里。新的桶数组大小是旧数组的两倍。扩容完成后,oldbuckets 会被释放。
  • nevacuate:用于记录扩容进度的计数器。表示已经迁移完成的桶数量。
  • extra:存储一些可选字段。
bmap

bmap 是存储键值对的“桶”,每个桶可以存储最多 8 对键值对。源码如下所示:

源码位置:src/runtime/map.go

// A bucket for a Go map.
type bmap struct {// tophash generally contains the top byte of the hash value// for each key in this bucket. If tophash[0] < minTopHash,// tophash[0] is a bucket evacuation state instead.tophash [abi.MapBucketCount]uint8// Followed by bucketCnt keys and then bucketCnt elems.// NOTE: packing all the keys together and then all the elems together makes the// code a bit more complicated than alternating key/elem/key/elem/... but it allows// us to eliminate padding which would be needed for, e.g., map[int64]int8.// Followed by an overflow pointer.
}
  • tophashabi.MapBucketCount 是一个常量,值为 8。存储每个键的哈希值的最高字节(top byte)。特殊情况:如果 tophash[0] < minTopHash,则表示该桶处于迁移状态,而不是存储键的哈希值。
// Maximum number of key/elem pairs a bucket can hold.
MapBucketCountBits = 3 // log2 of number of elements in a bucket.
MapBucketCount     = 1 << MapBucketCountBits
minTopHash     = 5 // minimum tophash for a normal filled cell.
  • tophash 之后,bmap 会依次存储键和值。键和值的存储方式是连续的:先存储所有键(bucketCnt 个键),然后存储所有值(bucketCnt 个值)。这种设计可以减少内存对齐时的填充,从而节省内存。例如,在 map[int64]int8 的情况下,如果键和值交替存储,可能会因为对齐问题浪费内存。
  • 最后在每个 bmap 的末尾包含一个指向下一个溢出桶的指针(overflow)。当一个桶满了(最多存储 8 对键值对)时,会通过链表的方式创建一个溢出桶,用于存储额外的键值对。

数据存储

例如:在64位平台上有一个 map[int]string,键的大小为 8 字节(int64),值的大小为 16 字节(指向字符串数据的指针(8 字节)和字符串的长度(8 字节))。一个 bmap 的内存布局如下所示:

在这里插入图片描述

键值访问

map 数据存储模型可知,因为键和值的存储是动态的,访问键和值时就需要通过指针偏移来实现。源码内容如下所示:

源码位置:src/runtime/map.go

// mapaccess1 returns a pointer to h[key].  Never returns nil, instead
// it will return a reference to the zero object for the elem type if
// the key is not in the map.
// NOTE: The returned pointer may keep the whole map live, so don't
// hold onto it for very long.
func mapaccess1(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {// ...
}
  • t *maptypemap 的类型信息,例如键的类型、值的类型、哈希函数等。
  • h *hmapmap 的底层结构,包含哈希表的元数据(如桶数组、键值对数量等)。
  • key unsafe.Pointer:查找目标键的指针。
  • 返回值:返回指向值的指针。如果键不存在,则返回值类型的零值的指针。

对于 mapaccess1 源码的流程进行了如下步骤的拆解:

  • 竞态检测
  • Sanitizer 检测
  • 空检查
  • 并发写检查
  • 哈希值计算
  • 桶定位
  • 扩容情况处理
  • 桶查找

下面针对每一步骤进行详细说明。

竞态检测

如果启用了竞态检测,记录当前操作的上下文,以便在发生竞态时能够追踪。

if raceenabled && h != nil {callerpc := getcallerpc()pc := abi.FuncPCABIInternal(mapaccess1)racereadpc(unsafe.Pointer(h), callerpc, pc)raceReadObjectPC(t.Key, key, callerpc, pc)
}
Sanitizer 检测

msanread 会标记对 key 的读取操作,确保该内存区域已经被正确初始化。防止程序读取未初始化的内存,从而避免潜在的未定义行为。

asanread 会检查对 key 的读取操作是否安全,例如是否越界或是否访问了已释放的内存。

if msanenabled && h != nil {msanread(key, t.Key.Size_)
}
if asanenabled && h != nil {asanread(key, t.Key.Size_)
}
空检查

如果 map 为空,直接返回值类型的零值。

if h == nil || h.count == 0 {if err := mapKeyError(t, key); err != nil {panic(err) // see issue 23734}return unsafe.Pointer(&zeroVal[0])
}
并发写检查

如果 map 正在被写入(hashWriting 标志被设置),抛出并发读写错误,这一点也表明 map 并不支持并发安全。

if h.flags&hashWriting != 0 {fatal("concurrent map read and map write")
}
哈希值计算

使用键的哈希函数计算哈希值,其中 h.hash0 是哈希种子,用于避免哈希冲突。

hash := t.Hasher(key, uintptr(h.hash0))
桶定位

代码中使用哈希值的低 B 位(bucketMask(h.B))定位到初始桶。其中 BucketSize 是每个桶的大小(包括键和值的存储)。

m := bucketMask(h.B)
b := (*bmap)(add(h.buckets, (hash&m)*uintptr(t.BucketSize)))
扩容情况处理

如果当前真正在扩容,那么检查旧的桶数组 oldbuckets。如果旧桶尚未迁移完成(!evacuated(oldb)),使用旧桶进行查找。

if c := h.oldbuckets; c != nil {if !h.sameSizeGrow() {// There used to be half as many buckets; mask down one more power of two.// 如果扩容前的桶数量是当前的一半,进一步掩码哈希值m >>= 1}oldb := (*bmap)(add(c, (hash&m)*uintptr(t.BucketSize)))if !evacuated(oldb) {b = oldb}
}
桶内查找
// 获取键的哈希值的最高字节
top := tophash(hash)
bucketloop:// 遍历当前桶及其溢出桶for ; b != nil; b = b.overflow(t) {// 遍历桶内的所有键值对for i := uintptr(0); i < abi.MapBucketCount; i++ {// 检查当前个键的哈希值最高字节是否匹配,如果不相等,说明当前槽位的键与目标键不匹配if b.tophash[i] != top {// 如果遇到空槽(emptyRest)跳出桶循环,因为后续槽位都是空的if b.tophash[i] == emptyRest {break bucketloop}// 不匹配则跳过当前槽位continue}// 计算第 i 个键的地址k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.KeySize))// 如果键是指针类型,解引用键的指针if t.IndirectKey() {k = *((*unsafe.Pointer)(k))}// 比较传入的键和当前键是否相等if t.Key.Equal(key, k) {// 计算第 i 个值的地址e := add(unsafe.Pointer(b), dataOffset+abi.MapBucketCount*uintptr(t.KeySize)+i*uintptr(t.ValueSize))// 如果值是指针类型,解引用值的指针if t.IndirectElem() {e = *((*unsafe.Pointer)(e))}// 返回值的指针return e}}}// 如果未找到,则返回值类型的零值指针return unsafe.Pointer(&zeroVal[0])

根据上一小结中的 bmap 数据存储模型可知,桶内查找中最为重要的就是键的偏移量计算和值的偏移量计算,如下所示:

k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.KeySize))

其中,dataOffsettophash 数组之后的偏移量,i * uintptr(t.KeySize) 是计算得第 i 个键的偏移量。

e := add(unsafe.Pointer(b), dataOffset+abi.MapBucketCount*uintptr(t.KeySize)+i*uintptr(t.ValueSize))

其中,dataOffset + abi.MapBucketCount*uintptr(t.KeySize) 是值的起始偏移量(所有键之后);i * uintptr(t.ValueSize) 是第 i 个值的偏移量。

键值插入、扩容机制

map 实现中,当插入元素到达一定的时机后会触发扩容机制,下面从元素的插入开始深入研究映射的扩容机制。

golang 中,mapassign 是一个底层的运行时函数,用于实现 map 的赋值操作。当使用 m[key] = value 语法向 map 中插入或更新键值对时,最终会调用 mapassign 函数。

源码位置:src/runtime/map.go

// Like mapaccess, but allocates a slot for the key if it is not present in the map.
//
// mapassign should be an internal detail,
// but widely used packages access it using linkname.
// Notable members of the hall of shame include:
//   - github.com/bytedance/sonic
//   - github.com/cloudwego/frugal
//   - github.com/RomiChan/protobuf
//   - github.com/segmentio/encoding
//   - github.com/ugorji/go/codec
//
// Do not remove or change the type signature.
// See go.dev/issue/67401.
//
//go:linkname mapassign
func mapassign(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {// ...
}

根据上面的函数原型参数解释:

  • t *maptypemap 的类型信息。
  • h *hmapmap 的底层数据结构。
  • key unsafe.Pointer 是要插入或更新的键的指针。

mapassign 函数内部将代码执行流程划分了如下几个阶段:

  • 参数检查
  • 竞态检测
  • Sanitizer 检测
  • 并发检测
  • 哈希值计算
  • 初始化桶数组
  • 定位桶和处理扩容
  • 遍历桶并检查键
  • 检查是否需要扩容
  • 插入或更新键值对
  • 返回值的地址

下面针对每一步骤进行详细说明。

参数检查

如果 hnil,则直接抛出错误,因为不能对 nilmap 进行赋值。

if h == nil {panic(plainError("assignment to entry in nil map"))
}
竞态检测

如果启用了竞态检测,记录当前操作的上下文,以便在发生竞态时能够追踪。

if raceenabled && h != nil {callerpc := getcallerpc()pc := abi.FuncPCABIInternal(mapaccess1)racereadpc(unsafe.Pointer(h), callerpc, pc)raceReadObjectPC(t.Key, key, callerpc, pc)
}
Sanitizer 检测

asanread 会检查对 key 的读操作是否安全,例如是否越界或是否访问了已释放的内存。

if msanenabled && h != nil {msanread(key, t.Key.Size_)
}
if asanenabled && h != nil {asanread(key, t.Key.Size_)
}
并发检测

如果 h.flags 中的 hashWriting 标志被设置,说明当前有其他协程正在写入 map,直接触发 fatal 错误。这也说明 map 不支持并发写入。

if h.flags&hashWriting != 0 {fatal("concurrent map writes")
}
哈希值计算

使用键的哈希函数计算哈希值,并结合哈希种子以避免哈希冲突。

hash := t.Hasher(key, uintptr(h.hash0))
初始化桶数组

如果 map 的桶数组尚未初始化,则分配一个初始大小的桶数组(默认)。

if h.buckets == nil {h.buckets = newobject(t.Bucket) // newarray(t.Bucket, 1)
}
定位桶和处理扩容
again:bucket := hash & bucketMask(h.B)	// 计算当前键的哈希值对应的桶索引if h.growing() {	// 如果 hashWriting 被设置,表示 map 正在扩容,则调用 growWork 函数处理扩容逻辑growWork(t, h, bucket)}

growWork 函数是扩容的核心逻辑,负责迁移旧桶中的数据到新桶中,源码如下所示:

func growWork(t *maptype, h *hmap, bucket uintptr) {// make sure we evacuate the oldbucket corresponding// to the bucket we're about to use// bucket&h.oldbucketmask() 表示当前桶索引对应的旧桶索引evacuate(t, h, bucket&h.oldbucketmask())	// 将旧桶中的数据迁移到新桶中// evacuate one more oldbucket to make progress on growingif h.growing() {	// 检查是否设置了 hashWriting 标志,表示 map 是否正在扩容// h.nevacuate 记录当前需要迁移的旧桶索引evacuate(t, h, h.nevacuate)}
}

需要注意的是每次迁移完成一个旧桶后,h.nevacuate 才会更新为下一个需要迁移的旧桶索引,这样设计的好处是避免一次性迁移所有数据导致的性能抖动。

下面介绍 evacuate 函数,它的核心逻辑包括:

  • 遍历旧桶及其溢出桶。
  • 将旧桶中的键值对重新哈希并插入到新桶中。
  • 更新 h.nevacuate 需要迁移的旧桶索引,并记录已迁移的旧桶数量。

函数定义如下所示:

func evacuate(t *maptype, h *hmap, oldbucket uintptr) {// ...
}
  • tmap 的类型信息。
  • hmap 的底层数据结构。
  • oldbucket:当前需要迁移的旧桶索引。

函数内容如下所示:

func evacuate(t *maptype, h *hmap, oldbucket uintptr) {// 通过旧桶数组的指针和旧桶索引,计算出当前旧桶的地址// h.oldbuckets: 旧桶数组的指针// oldbucket*uintptr(t.BucketSize)): 计算当前旧桶的偏移量// 然后通过 add 函数计算处旧桶的地址b := (*bmap)(add(h.oldbuckets, oldbucket*uintptr(t.BucketSize)))// 算新桶的掩码值newbit := h.noldbuckets()if !evacuated(b) {	// 检查当前旧桶是否已经被迁移,如果未迁移,则执行以下逻辑var xy [2]evacDstx := &xy[0]// 新桶的地址x.b = (*bmap)(add(h.buckets, oldbucket*uintptr(t.BucketSize)))// 新桶中键的起始地址x.k = add(unsafe.Pointer(x.b), dataOffset)// 新桶中值的起始地址x.e = add(x.k, abi.MapBucketCount*uintptr(t.KeySize))// 如果扩容,则初始化第二个迁移目标if !h.sameSizeGrow() {y := &xy[1]// 新桶的地址y.b = (*bmap)(add(h.buckets, (oldbucket+newbit)*uintptr(t.BucketSize)))// 新桶中键的起始地址y.k = add(unsafe.Pointer(y.b), dataOffset)// 新桶中值的起始地址y.e = add(y.k, abi.MapBucketCount*uintptr(t.KeySize))}// 遍历当前旧桶及其所有溢出桶for ; b != nil; b = b.overflow(t) {k := add(unsafe.Pointer(b), dataOffset)	// 当前桶中键的首地址e := add(k, abi.MapBucketCount*uintptr(t.KeySize))	// 当前桶中值的首地址// 遍历当前桶中的所有键值对for i := 0; i < abi.MapBucketCount; i, k, e = i+1, add(k, uintptr(t.KeySize)), add(e, uintptr(t.ValueSize)) {top := b.tophash[i]	// 当前键值对的哈希高位值// 如果当前键值对为空,标记该槽已迁移且为空if isEmpty(top) {b.tophash[i] = evacuatedEmptycontinue}// 如果哈希高位值小于 minTopHash,表示哈希表状态异常if top < minTopHash {throw("bad map state")}// 判断键是否为指针类型,如果是指针类型,则解引用指针获取实际地址k2 := kif t.IndirectKey() {k2 = *((*unsafe.Pointer)(k2))}// 判断是迁移到 x 还是迁移到 yvar useY uint8// 是否是等量扩容(桶数量不变)if !h.sameSizeGrow() {// Compute hash to make our evacuation decision (whether we need// to send this key/elem to bucket x or bucket y).// 重新计算键的哈希值hash := t.Hasher(k2, uintptr(h.hash0))// 判断是否是 NaN 键if h.flags&iterator != 0 && !t.ReflexiveKey() && !t.Key.Equal(k2, k2) {// If key != key (NaNs), then the hash could be (and probably// will be) entirely different from the old hash. Moreover,// it isn't reproducible. Reproducibility is required in the// presence of iterators, as our evacuation decision must// match whatever decision the iterator made.// Fortunately, we have the freedom to send these keys either// way. Also, tophash is meaningless for these kinds of keys.// We let the low bit of tophash drive the evacuation decision.// We recompute a new random tophash for the next level so// these keys will get evenly distributed across all buckets// after multiple grows.// 使用旧桶的 tophash 的最低位决定是否迁移到桶 yuseY = top & 1top = tophash(hash)	// 重新计算 tophash} else {// 对于普通键,根据新哈希值的 newbit 是否是1来决定是否迁移到桶y中if hash&newbit != 0 {useY = 1}}}if evacuatedX+1 != evacuatedY || evacuatedX^1 != evacuatedY {throw("bad evacuatedN")}// 标记当前键值对已迁移b.tophash[i] = evacuatedX + useY // evacuatedX + 1 == evacuatedY// 确定迁移目的桶dst := &xy[useY]                 // evacuation destination// 判断目标桶是否已经满了,如果已经满了,则创建一个新的溢出桶,然后继续迁移if dst.i == abi.MapBucketCount {dst.b = h.newoverflow(t, dst.b)dst.i = 0dst.k = add(unsafe.Pointer(dst.b), dataOffset)dst.e = add(dst.k, abi.MapBucketCount*uintptr(t.KeySize))}// 下面为真正迁移的代码,每行都是简单的指针操作,不再赘述dst.b.tophash[dst.i&(abi.MapBucketCount-1)] = top // mask dst.i as an optimization, to avoid a bounds checkif t.IndirectKey() {*(*unsafe.Pointer)(dst.k) = k2 // copy pointer} else {typedmemmove(t.Key, dst.k, k) // copy elem}if t.IndirectElem() {*(*unsafe.Pointer)(dst.e) = *(*unsafe.Pointer)(e)} else {typedmemmove(t.Elem, dst.e, e)}dst.i++// These updates might push these pointers past the end of the// key or elem arrays.  That's ok, as we have the overflow pointer// at the end of the bucket to protect against pointing past the// end of the bucket.dst.k = add(dst.k, uintptr(t.KeySize))dst.e = add(dst.e, uintptr(t.ValueSize))}}// Unlink the overflow buckets & clear key/elem to help GC.// 如果旧桶不再被迭代器使用,清理旧桶中的指针以帮助 GCif h.flags&oldIterator == 0 && t.Bucket.Pointers() {b := add(h.oldbuckets, oldbucket*uintptr(t.BucketSize))// Preserve b.tophash because the evacuation// state is maintained there.ptr := add(b, dataOffset)n := uintptr(t.BucketSize) - dataOffsetmemclrHasPointers(ptr, n)}}// 如果当前桶是正在迁移的桶,更新迁移进度if oldbucket == h.nevacuate {advanceEvacuationMark(h, t, newbit)}
}
遍历桶并检查键

下面这段代码遍历目标桶及其溢出桶,寻找合适的插入位置或更新现有键值对。如下所示:

// 通过目标桶的索引 bucket 定位到对应的桶
b := (*bmap)(add(h.buckets, bucket*uintptr(t.BucketSize)))
// 根据键的哈希值计算其哈希高位值
top := tophash(hash)var inserti *uint8
var insertk unsafe.Pointer
var elem unsafe.Pointer
bucketloop:for {// 遍历当前桶的所有槽位for i := uintptr(0); i < abi.MapBucketCount; i++ {// 当前槽位的哈希高位值是否与目标值匹配,如果不匹配,说明当前槽位不包含目标键if b.tophash[i] != top {// 如果当前槽位为空且未找到空闲槽位,则记录该槽位,用于后续插入if isEmpty(b.tophash[i]) && inserti == nil {// inserti:空闲槽地址inserti = &b.tophash[i]// insertk:空闲槽键的地址insertk = add(unsafe.Pointer(b), dataOffset+i*uintptr(t.KeySize))// elem:空闲槽值的地址elem = add(unsafe.Pointer(b), dataOffset+abi.MapBucketCount*uintptr(t.KeySize)+i*uintptr(t.ValueSize))}// 如果当前槽位的哈希值为 emptyRest,说明后续槽位均为空,结束遍历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))}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+abi.MapBucketCount*uintptr(t.KeySize)+i*uintptr(t.ValueSize))goto done	// 结束操作}// 如果当前桶未找到匹配的键或空闲槽位,则检查溢出桶ovf := b.overflow(t)	// 获取当前桶的溢出桶if ovf == nil {break	// 如果没有溢出桶,则结束遍历}b = ovf	// 如果有溢出桶,更新桶的地址}
检查是否需要扩容
if !h.growing() && (overLoadFactor(h.count+1, h.B) || tooManyOverflowBuckets(h.noverflow, h.B)) {hashGrow(t, h)goto again // Growing the table invalidates everything, so try again
}

根据上述代码中的逻辑,核心在于 overLoadFactortooManyOverflowBuckets 的判断,下面对这两个函数进行刨析。

overLoadFactor

判断当前 map 的负载因子是否超过了设定的阈值。其中 count 表示当前 map 中的键值对数量;B 表示当前 map 的桶数量的对数(2^B 是当前桶的数量)。

func overLoadFactor(count int, B uint8) bool {return count > abi.MapBucketCount && uintptr(count) > loadFactorNum*(bucketShift(B)/loadFactorDen)
}

具体计算逻辑如下所示:

  • count > abi.MapBucketCount:确保键值对数量大于单个桶的容量(8 个键值对)。

  • uintptr(count) > loadFactorNum*(bucketShift(B)/loadFactorDen):计算当前负载因子是否超过阈值。

l o a d F a c t o r D e n = 2 loadFactorDen = 2 loadFactorDen=2

l o a d F a c t o r N u m = l o a d F a c t o r D e n ∗ a b i . M a p B u c k e t C o u n t ∗ 13 / 16 = 2 ∗ 8 ∗ 13 / 16 = 13 loadFactorNum \\= loadFactorDen * abi.MapBucketCount * 13 / 16 \\= 2 * 8 * 13 / 16 \\= 13 loadFactorNum=loadFactorDenabi.MapBucketCount13/16=2813/16=13

func bucketShift(b uint8) uintptr {// Masking the shift amount allows overflow checks to be elided.return uintptr(1) << (b & (goarch.PtrSize*8 - 1))
}
  • goarch.PtrSize:表示指针的大小(以字节为单位)。在 64 位系统中,goarch.PtrSize = 8;在 32 位系统中,goarch.PtrSize = 4

假设 B = 3 则有:

b u c k e t S h i f t ( B ) = > b u c k e t S h i f t ( 3 ) = u i n t p t r ( 1 ) < < ( 3 & ( 8 ∗ 8 − 1 ) ) = u i n t p t r ( 1 ) < < 3 & 63 = u i n t p t r ( 1 ) < < 3 = 8 bucketShift(B) => bucketShift(3) \\= uintptr(1) << (3 \& (8*8 - 1)) \\= uintptr(1) << 3 \& 63 \\= uintptr(1) << 3 \\= 8 bucketShift(B)=>bucketShift(3)=uintptr(1)<<(3&(881))=uintptr(1)<<3&63=uintptr(1)<<3=8

综上所述,结算结果为:

u i n t p t r ( c o u n t ) > l o a d F a c t o r N u m ∗ ( b u c k e t S h i f t ( B ) / l o a d F a c t o r D e n ) = u i n t p t r ( c o u n t ) > 13 ∗ ( 8 / 2 ) = 13 ∗ 4 = 52 uintptr(count) > loadFactorNum*(bucketShift(B)/loadFactorDen) \\= uintptr(count) > 13 * (8 / 2) \\= 13 * 4 \\= 52 uintptr(count)>loadFactorNum(bucketShift(B)/loadFactorDen)=uintptr(count)>13(8/2)=134=52

因此,当键值对数量 count 超过 52 时,overLoadFactor 返回 true,触发扩容。

通过负载因子(loadFactorThreshold)可以快速的计算出扩容的时机,公式如下所示:

l o a d F a c t o r T h r e s h o l d = l o a d F a c t o r N u m / l o a d F a c t o r D e n = 13 / 2 = 6.5 loadFactorThreshold \\= loadFactorNum / loadFactorDen \\= 13 / 2 \\= 6.5 loadFactorThreshold=loadFactorNum/loadFactorDen=13/2=6.5

这意味着当键值对数量超过 当前桶的容量6.5 倍 时,map 会触发扩容。

tooManyOverflowBuckets

判断当前 map 的溢出桶数量是否过多。如果溢出桶数量过多,说明 map 的哈希表分布不够均匀,需要扩容以优化性能。

// tooManyOverflowBuckets reports whether noverflow buckets is too many for a map with 1<<B buckets.
// Note that most of these overflow buckets must be in sparse use;
// if use was dense, then we'd have already triggered regular map growth.
func tooManyOverflowBuckets(noverflow uint16, B uint8) bool {// If the threshold is too low, we do extraneous work.// If the threshold is too high, maps that grow and shrink can hold on to lots of unused memory.// "too many" means (approximately) as many overflow buckets as regular buckets.// 当溢出桶的数量接近或超过当前桶的数量时,认为溢出桶过多。// See incrnoverflow for more details.if B > 15 {// 如果 B 大于 15,将其限制为 15。B = 15}// The compiler doesn't see here that B < 16; mask B to generate shorter shift code.return noverflow >= uint16(1)<<(B&15)
}
  • noverflow:当前 map 的溢出桶数量。
  • B:当前 map 的桶数量的对数(2^B 是当前桶的数量)。

为什么限制 B 的最大值为 15?

首先对于 map 来说 2^15 = 32768,这已经是一个非常大的哈希表。另外对于内存的使用效率来说,如果 B 超过 15 可能会导致内存使用过多,或者哈希表过于稀疏,从而降低性能。

插入或更新键值对
插入位置为空
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]	// 指向新溢出桶的第一个 tophash 位置。tophash 是一个数组,用于存储键的哈希值的高位信息insertk = add(unsafe.Pointer(newb), dataOffset)	// 计算新溢出桶中键的存储位置。add 是一个低级操作,用于通过偏移量计算指针位置。dataOffset 是键值对数据在桶中的偏移量elem = add(insertk, abi.MapBucketCount*uintptr(t.KeySize))	// 计算新溢出桶中值的存储位置。abi.MapBucketCount 是每个桶中键值对的数量,t.KeySize 是键的大小。通过偏移量计算出值的存储位置
}
存储新的键值对
// store new key/elem at insert position
if t.IndirectKey() {	// 判断键是否是指针类型。如果是指针类型,则需要间接存储kmem := newobject(t.Key)	// 分配一个新的对象,用于存储键*(*unsafe.Pointer)(insertk) = kmem	// 将分配的对象地址存储到 insertk 指向的位置insertk = kmem	// 更新 insertk 使其指向实际的键存储位置
}if t.IndirectElem() {	// 判断值是否是指针类型。如果是指针类型,则需要间接存储vmem := newobject(t.Elem)	// 分配一个新的对象,用于存储值*(*unsafe.Pointer)(elem) = vmem	// 将分配的对象地址存储到 elem 指向的位置
}typedmemmove(t.Key, insertk, key) // 将传入的键 key 复制到 insertk 指向的位置
*inserti = top	// 将键的哈希值的高位信息存储到 inserti 指向的位置。top 是哈希值的高位部分
h.count++	// 增加哈希表的计数器 count,表示哈希表中键值对的数量增加了一个
返回值的地址
done:if h.flags&hashWriting == 0 {	// 检查 h.flags 是否设置了 hashWriting 标志// hashWriting 在 map 写入操作开始时设置,写入操作结束时清除。它用于检测并发写入冲突// 如果检测到并发写入,调用 fatal 函数抛出致命错误fatal("concurrent map writes")}h.flags &^= hashWriting	// 清除 hashWriting 标志if t.IndirectElem() {	// 判断 map 的值是否是指针类型elem = *((*unsafe.Pointer)(elem)) // 如果 map 的值是指针类型,则需要通过指针解引用获取实际的值地址}return elem	// 返回值的地址

键值删除

golang 中,mapdelete 是一个底层的运行时函数,用于实现 map 的键值删除逻辑。

源码位置:src/runtime/map.go

下面给出函数原型,如下所示:

func mapdelete(t *maptype, h *hmap, key unsafe.Pointer) {// ...
}
  • tmap 的类型信息。
  • hmap 的底层数据结构。
  • key:要删除的键的指针。

mapdelete 函数中的代码逻辑分为如下几个阶段:

  • 竞态检测
  • Sanitizer 检测
  • 参数检查
  • 并发检测
  • 设置写入标记
  • 计算哈希值和目标桶
  • 处理扩容
  • 定位目标桶
  • 查找键值对
  • 删除目标键值对
  • 清理空闲槽
  • 重置哈希种子
  • 清除写入标记

下面针对每一步骤进行详细说明。

竞态检测

如果启用了竞态检测,记录对 map 的写操作和键的读操作,以便在发生竞态时能够追踪。

if raceenabled && h != nil {callerpc := getcallerpc()pc := abi.FuncPCABIInternal(mapdelete)// 记录写操作的程序计数器信息racewritepc(unsafe.Pointer(h), callerpc, pc)// 记录读操作的程序计数器信息raceReadObjectPC(t.Key, key, callerpc, pc)
}
Sanitizer 检测

记录键的读操作,检测未初始化或越界的内存访问。

if msanenabled && h != nil {msanread(key, t.Key.Size_)
}
if asanenabled && h != nil {asanread(key, t.Key.Size_)
}
参数检查

如果 mapnil 或者键值对数量为 0,直接返回。如果键无效 mapKeyError 则报出异常。

if h == nil || h.count == 0 {if err := mapKeyError(t, key); err != nil {panic(err) // see issue 23734}return
}
并发检测

检查是否已经有其他协程正在写入 map,不支持并发操作。

if h.flags&hashWriting != 0 {fatal("concurrent map writes")
}
设置写入标记

设置 hashWriting 标志,表示当前正在写入 map

h.flags ^= hashWriting
计算哈希值和目标桶

计算键的哈希值和目标桶的索引,用于定位目标桶。

hash := t.Hasher(key, uintptr(h.hash0))
bucket := hash & bucketMask(h.B)
处理扩容

如果 map 正在扩容,调用 growWork 处理扩容逻辑。

有关扩容的源码分析,请查看 “键值插入、扩容机制” 小结。

if h.growing() {growWork(t, h, bucket)
}
定位目标桶

通过目标桶的索引 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 < abi.MapBucketCount; i++ {// 如果当前槽位的哈希值不匹配,跳过if b.tophash[i] != top {// 如果遇到 emptyRest,表示后续槽位均为空,结束搜索if b.tophash[i] == emptyRest {break search}continue}// 获取当前槽的键地址k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.KeySize))k2 := kif t.IndirectKey() {	// 判断键是否为指针类型k2 = *((*unsafe.Pointer)(k2))}if !t.Key.Equal(key, k2) {	// 比较键是否相等,如果不相等则跳过本次循环continue}// 删除目标键值对}}
删除目标键值对

删除键值对的核心逻辑,负责清空键和值的内容,并更新桶的状态。

// Only clear key if there are pointers in it.
if t.IndirectKey() {	// 判断键是否为指针类型*(*unsafe.Pointer)(k) = nil	// 如果是指针类型,直接将指针置为 nil
} else if t.Key.Pointers() {memclrHasPointers(k, t.Key.Size_)	// 如果键包含指针,则清空键的内容,帮助 GC
}// 计算当前槽位的值的地址
e := add(unsafe.Pointer(b), dataOffset+abi.MapBucketCount*uintptr(t.KeySize)+i*uintptr(t.ValueSize))
// 根据值的类型,清空值的内容
if t.IndirectElem() {	// 判断值是否为指针类型*(*unsafe.Pointer)(e) = nil	// 如果是指针类型,直接将指针置为 nil
} else if t.Elem.Pointers() {	// 判断值是否包含指针memclrHasPointers(e, t.Elem.Size_)	// 如果值包含指针,则清空值的内容,帮助 GC
} else {memclrNoHeapPointers(e, t.Elem.Size_)	// 如果值不包含指针,则清空值的内容,不需要指针操作
}// 更新槽位状态
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 == abi.MapBucketCount-1 {// 如果当前桶有溢出桶并且溢出桶的第一个槽不是 emptyRest,表示后续槽仍包含数据if b.overflow(t) != nil && b.overflow(t).tophash[0] != emptyRest {goto notLast}
} else {// 下一个槽不是 emptyRest,表示后续槽仍包含数据if b.tophash[i+1] != emptyRest {goto notLast}
}// 清理空闲槽
for {// 将当前槽的状态更新为 emptyRestb.tophash[i] = emptyRestif 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 = abi.MapBucketCount - 1	// 继续清理前一个桶的最后一个槽} else {// 继续清理当前桶的前一个槽i--}// 如果当前槽不是 emptyOne,清理完成 if b.tophash[i] != emptyOne {break}
}
notLast:// 减少键值对的数量h.count--
重置哈希种子

如果 map 中没有键值对,重置哈希种子以防止哈希碰撞攻击

if h.count == 0 {h.hash0 = uint32(rand())
}
清除写入标记

清除 hashWriting 标志,表示写入操作完成

// 如果标志未设置,说明可能存在并发写入问题
if h.flags&hashWriting == 0 {fatal("concurrent map writes")
}// 清除
h.flags &^= hashWriting

常用操作

访问映射值

通过键访问对应的值,格式如下所示:

value := m["key"]

其中,如果键不存在,则返回值类型的零值。例如:

package mainimport "fmt"func main() {m := map[string]int{"apple":  1,"banana": 2,"cherry": 3,}fmt.Println(m["apple"])  // 1fmt.Println(m["orange"]) // 0
}

检查键是否存在

通过多值赋值检查键是否存在,格式如下所示:

value, ok := m["key"]
if ok {fmt.Println("Key exists, value:", value)
} else {fmt.Println("Key does not exist")
}
  • ok 是一个布尔值,表示键是否存在。
  • 如果键存在,oktruevalue 是对应的值。
  • 如果键不存在,okfalsevalue 是值类型的零值。

例如:

package mainimport "fmt"func main() {m := map[string]int{"apple":  1,"banana": 2,"cherry": 3,}value, ok := m["apple"]if ok {fmt.Println("Key exists, value:", value) // Key exists, value: 1} else {fmt.Println("Key does not exist")}
}

修改映射的值

通过键直接赋值,格式如下所示:

m["key"] = newValue

例如:

package mainimport "fmt"func main() {m := map[string]int{"apple":  1,"banana": 2,"cherry": 3,}m["apple"] = 10         // 修改键为 "apple" 的值为 10fmt.Println(m["apple"]) // 10
}

添加键值对

直接赋值即可,格式如下所示:

m["newKey"] = newValue

例如:

package mainimport "fmt"func main() {m := map[string]int{"apple":  1,"banana": 2,"cherry": 3,}m["orange"] = 4          // 添加键为 "orange",值为 4 的键值对fmt.Println(m["orange"]) // 4
}

删除键值对

使用 delete 函数进行删除,格式如下所示:

delete(m, "key")

例如:

package mainimport "fmt"func main() {m := map[string]int{"apple":  1,"banana": 2,"cherry": 3,}fmt.Println(m["banana"]) // 2delete(m, "banana")      // 删除键为 "banana" 的键值对fmt.Println(m["banana"]) // 0
}

遍历映射

使用 for range 循环遍历映射中的键值对,例如:

package mainimport "fmt"func main() {m := map[string]int{"apple":  1,"banana": 2,"cherry": 3,}// Key: apple, Value: 1// Key: banana, Value: 2// Key: cherry, Value: 3for key, value := range m {fmt.Printf("Key: %v, Value: %v\n", key, value)}
}

需要注意的是,映射的遍历顺序是随机的,因为映射内部是无序的。

获取映射长度

使用 len 函数获取映射中键值对的数量,例如:

package mainimport "fmt"func main() {m := map[string]int{"apple":  1,"banana": 2,"cherry": 3,}length := len(m)fmt.Println(length) // 3
}

映射嵌套

映射的值可以是任何类型,包括另一个映射,从而实现嵌套映射,例如:

package mainimport "fmt"func main() {nestedMap := map[string]map[string]int{"fruits": {"apple":  1,"banana": 2,},"vegetables": {"carrot":   3,"broccoli": 4,},}fmt.Println(nestedMap["fruits"]["apple"]) // 1
}

映射的拷贝

映射是引用类型,直接赋值会共享同一个底层数据结构。如果需要拷贝映射,需要手动创建一个新的映射并复制键值对。例如:

package mainimport "fmt"func main() {m1 := map[string]int{"apple":  1,"banana": 2,}m2 := make(map[string]int)for key, value := range m1 {m2[key] = value	// 深拷贝}// Key: apple, Value: 1// Key: banana, Value: 2for key, value := range m2 {fmt.Printf("Key: %s, Value: %d\n", key, value)}m2["apple"] = 3fmt.Println(m1["apple"]) // 此时访问 m1 中的 apple,并没有修改
}

清空映射

虽然 golang 没有直接清空映射的函数,但可以通过遍历映射并删除所有键值对来实现。例如:

package mainimport "fmt"func main() {m := map[string]int{"apple":  1,"banana": 2,}fmt.Println(len(m)) // 2for key := range m {delete(m, key)}fmt.Println(len(m)) // 0
}

常见问题

检查键是否存在

在访问映射中的值之前,应始终检查键是否存在,以避免误用零值。例如:

value, exists := myMap["key"]
if exists {fmt.Println("Value:", value)
} else {fmt.Println("Key does not exist")
}

初始化映射时指定容量

如果已知映射的大小,建议在初始化时指定容量,以减少后续的内存重新分配。例如:

myMap := make(map[string]int, 100) // 预计映射大小为 100

避免并发修改

golang 的映射不是线程安全的,因此在并发场景下直接修改映射会导致竞态条件。

  • 使用 sync.Mutex 加锁保护映射
var mu sync.Mutex
mu.Lock()
myMap["key"] = value
mu.Unlock()
  • 使用并发安全的映射 sync.Map,这个是 golang 标准库中提供的数据类型。
var myMap sync.Map
myMap.Store("key", 1) // 存储键值对
val, ok := myMap.Load("key") // 根据键读取值

关于更多并发相关的知识点,请关注后续文章 《Golang 并发编程》。

不使用映射存储大量的数据

映射是基于哈希表实现的,其内部结构需要额外的内存来存储哈希值、指针和元数据。也就是说映射的内存占用通常比数组或切片更高。对于大量数据,这种额外的内存开销可能会导致显著的性能问题。

下面给出一些可以替代的方案:

  • 使用外部数据库存储大规模数据。
  • 使用切片存储,切片的内存占用相对较低,另外对于有序的数据切片的性能通常会优于映射的性能。
  • 采用分块存储,将多个数据分散到不同的映射中,每个映射负责一部分数据,从而减少扩容的开销。
  • 定期清理不需要的键值对,优化内存使用。
  • 另外如果对于数据量较小且不需要频繁查询的数据,可以考虑其他的数据结构。

🌺🌺🌺撒花!

如果本文对你有帮助,就点关注或者留个👍
如果您有任何技术问题或者需要更多其他的内容,请随时向我提问。
在这里插入图片描述

相关文章:

【十二】Golang 映射

&#x1f4a2;欢迎来到张胤尘的开源技术站 &#x1f4a5;开源如江河&#xff0c;汇聚众志成。代码似星辰&#xff0c;照亮行征程。开源精神长&#xff0c;传承永不忘。携手共前行&#xff0c;未来更辉煌&#x1f4a5; 文章目录 映射映射的定义映射初始化make 函数使用字面量 源…...

简单理解Oracle中的latch

可以用一个小卖部抢购的例子来理解 Oracle 数据库中的 Latch&#xff1a; 1、 什么是 Latch&#xff1f; 打个比方&#xff0c;假设数据库的某个内存区域&#xff08;比如缓存的数据块&#xff09;是小卖部货架上的最后一包辣条&#xff0c;Latch 就像是货架前的一个狭窄通道&a…...

hbase集群部署

1.hbase集群的搭建&#xff08;以及内部逻辑&#xff09; 虽然Hmaster有多个&#xff0c;但是属于热备&#xff0c;起作用的就active上的这个。 部署流程&#xff1a; 因为我配置的hadoop是一个非HA的&#xff0c;所以修改为以下 如果是HA的hadoop一定要做以下这一步。 在启动…...

塔能物联运维保障智慧地下停车场安全与高效

一、智慧地下停车场安全在城市升级改造中的关键地位 随着城市的不断发展和升级改造&#xff0c;智慧地下停车场的重要性日益凸显。在现代城市中&#xff0c;土地资源愈发珍贵&#xff0c;地下停车场成为解决停车难题的关键设施。然而&#xff0c;停车场的安全问题是其正常运行和…...

面试八股文--数据库基础知识总结(2) MySQL

本文介绍关于MySQL的相关面试知识 一、关系型数据库 1、定义 关系型数据库&#xff08;Relational Database&#xff09;是一种基于关系模型的数据库管理系统&#xff08;DBMS&#xff09;&#xff0c;它将数据存储在表格&#xff08;表&#xff09;中&#xff0c;并通过表格…...

深入理解指针2

深入理解指针2 数组名的理解 数组名就是首元素的地址 int arr[]{1,3,2}; printf("%p\n",arr); printf("%p\n",&arr[0]);但是有两种情况除外&#xff0c; 1.sizeof(数组名)&#xff0c;sizeof操作符统计的是整个数组的大小&#xff0c;并不是第一个元素…...

QT各种版本下载安装

参考链接&#xff1a; 【Qt】超详细&#xff01;Qt4.8.6和VS2010的配置及使用 由于QT官网一般现在进不去&#xff0c;所以下载一些QT版本只能通过镜像或者以前下载存储的安装包来进行&#xff0c;现在推荐两种方法 从参考链接中搬过来&#xff1a; 方案一&#xff1a;国内镜…...

java进阶学习脑图

今天开始分享我的第一篇博客&#xff0c;先放上我自己花费一个月完成的java进阶学习脑图吧&#xff01; 谁都想像R大一样对JVM可以知无不言&#xff0c;言无不尽&#xff1b; 谁都想像Doug Lea一样可以参与JUC这种核心模块的开发&#xff1b; 但是&#xff0c;不能只停留在想…...

Spring 原始注解详解与实战指南

&#x1f4dd; 1. 前言 在 Spring 框架的发展过程中&#xff0c;注解的引入大大简化了配置&#xff0c;提升了开发效率 本文将详细介绍 Spring 最初引入的核心注解&#xff0c;包括 Component、Controller、Service、Repository、Autowired、Qualifier 和 Value 等&#xff0c;…...

uniapp封装请求

在uniapp中封装HTTP请求&#xff0c;通常我们会使用uni.request方法。uni.request是uni-app提供的一个网络请求API&#xff0c;可以用来发送各种类型的HTTP请求&#xff08;GET、POST、PUT、DELETE等&#xff09;。下面是如何在uniapp中封装一个通用的HTTP请求方法&#xff0c;…...

YOLOv10 解析与地平线 征程 6 模型量化

一&#xff0c;YOLOv10 解析 1.简介 近些年来&#xff0c;研究人员对 YOLO 的架构设计、优化目标、数据增强策略等进行了探索&#xff0c;取得了显著进展。然而&#xff0c;后处理对非极大值抑制&#xff08;NMS&#xff09;的依赖阻碍了 YOLO 的端到端部署&#xff0c;并对推…...

基本网络安全的实现

基本网络安全的实现 一 &#xff1a;AAA AAA 是Authentication&#xff0c;Authorization and Accounting&#xff08;认证、授权和计费&#xff09;的简 称&#xff0c;它提供了一个用来对认证、授权和计费这三种安全功能进行配置的一致性框架&#xff0c; 它是对网络安全…...

ROS2 强化学习:案例与代码实战

一、引言 在机器人技术不断发展的今天&#xff0c;强化学习&#xff08;RL&#xff09;作为一种强大的机器学习范式&#xff0c;为机器人的智能决策和自主控制提供了新的途径。ROS2&#xff08;Robot Operating System 2&#xff09;作为新一代机器人操作系统&#xff0c;具有…...

Java数据结构第十四期:走进二叉树的奇妙世界(三)

专栏&#xff1a;数据结构(Java版) 个人主页&#xff1a;手握风云 目录 一、二叉树OJ练习题 1.1. 相同的树 1.2. 另一棵树的子树 1.3. 翻转二叉树 1.4. 平衡二叉树 1.5. 对称二叉树 一、二叉树OJ练习题 1.1. 相同的树 判断两棵树是否相同&#xff0c;我们是否只能遍历一…...

GO 进行编译时插桩,实现零码注入

Go 编译时插桩 Go 语言的编译时插桩是一种在编译阶段自动注入监控代码的技术&#xff0c;目的是在不修改业务代码的情况下&#xff0c;实现对应用程序的监控和追踪。 基本原理 Go 编译时插桩的核心思想是通过在编译过程中对源代码进行分析和修改&#xff0c;将监控代码注入到…...

《炎龙骑士团 1 邪神之封印》游戏信息

发行公司&#xff1a;1994 年由汉堂国际资讯公司发行。 游戏类型&#xff1a;回合制角色扮演游戏 故事背景 远古之战&#xff1a;在远古时代&#xff0c;圣族与魔族爆发大战&#xff0c;魔族领导者大邪神力量强大&#xff0c;圣族处于下风。圣族派出十二战士突袭&#xff0c;虽…...

本地大模型编程实战(23)用智能体(Agent)实现基于SQL数据构建问答系统(2)

本文将用 智能体(Agent) 实现对 SQLite 数据库的查询&#xff1a;用户用自然语言提出问题&#xff0c;智能体也用自然语言根据数据库的查询结果回答问题。 本次将分别在英文、中文环境下&#xff0c;使用 qwen2.5 、 MFDoom/deepseek-r1-tool-calling:7b 以及 llama3.1 做实验。…...

Flash-03

1-问题&#xff1a;Flash软件画两个图形&#xff0c;若有部分重合则变为一个整体 解决方法1&#xff1a;两个图形分属于不同的图层 解决方法2&#xff1a;将每个图形都转化为【元件】 问题2&#xff1a;元件是什么&#xff1f; 在 Adobe Flash&#xff08;现在称为 Adobe Anim…...

防火墙双机热备---VRRP,VGMP,HRP(超详细)

双机热备技术-----VRRP&#xff0c;VGMP&#xff0c;HRP三个组成 注&#xff1a;与路由器VRRP有所不同&#xff0c;路由器是通过控制开销值控制数据包流通方向 防火墙双机热备&#xff1a; 1.主备备份模式 双机热备最大的特点就是防火墙提供了一条专门的备份通道&#xff08;心…...

PC端-发票真伪查验系统-Node.js全国发票查询接口

在现代企业的财务管理中&#xff0c;发票真伪的验证至关重要。随着电子发票的普及&#xff0c;假发票问题日益严峻&#xff0c;如何高效、准确的对发票进行真伪查验&#xff0c;已经成为各类企业在日常运营中必须解决的关键问题。翔云发票查验接口做企业财务管理、税务合规的好…...

3.1部署filebeat:5044

beats是ELK体系中新增的一个工具&#xff0c;, 属于一个轻量的日志采集器。 1.安装&#xff08;每台&#xff09; # tar xf filebeat-6.4.1-linux-x86_64.tar.gz # mv filebeat-6.4.1-linux-x86_64 /usr/local/filebeat #yum -y install httpd #systemctl start httpd 2.测试…...

在 Windows 上配置 Ollama 服务并开放局域网访问

为了在局域网内共享 Ollama 服务&#xff0c;我们需要完成以下两步&#xff1a; 1、设置 Ollama 的环境变量 OLLAMA_HOST&#xff0c;使其监听局域网的 IP 地址。 &#xff08;1&#xff09; 配置 Ollama 服务的监听地址 Ollama 服务使用环境变量 OLLAMA_HOST 来指定监听的地…...

C#快速调用DeepSeek接口,winform接入DeepSeek查询资料 C#零门槛接入DeepSeek C#接入DeepSeek源代码下载

下载地址<------完整源码 在数字化转型加速的背景下&#xff0c;企业应用系统对智能服务的需求日益增长。DeepSeek作为先进的人工智能服务平台&#xff0c;其自然语言处理、图像识别等核心能力可显著提升业务系统的智能化水平。传统开发模式下&#xff0c;C#开发者需要耗费大…...

解决后端跨域问题

目录 一、什么是跨域问题&#xff1f; 1、跨域问题的定义 2、举例 3、为什么会有跨域问题的存在&#xff1f; 二、解决跨域问题 1、新建配置类 2、编写代码 三、结语 一、什么是跨域问题&#xff1f; 1、跨域问题的定义 跨域问题&#xff08;Cross-Origin Resource Sh…...

【教程】使用docker+Dify搭建一个本地知识库

现在AI火的一塌糊涂&#xff0c;再不搭建一个自己的AI知识库就有点落伍了&#xff0c;这里我是自己的windows11电脑。用了dockerdifydeepseek。 一、安装docker 网址&#xff1a;https://www.docker.com/ 什么是docker&#xff1f; Docker 是一种开放源代码的容器化平台&…...

微信小程序数据绑定与事件处理:打造动态交互体验

在上一篇中&#xff0c;我们学习了如何搭建微信小程序的开发环境并创建了一个简单的“Hello World”页面。然而&#xff0c;一个真正的小程序不仅仅是静态内容的展示&#xff0c;它需要与用户进行动态交互。本文将深入探讨微信小程序中的数据绑定和事件处理机制&#xff0c;通过…...

Spring MVC 的执行流程解析:从用户请求到响应返回

Spring MVC 是一种基于 Model-View-Controller 设计模式的 Web 框架&#xff0c;用于处理用户请求、执行相应的业务逻辑并返回响应。它广泛应用于 Java Web 开发&#xff0c;提供了灵活的架构和丰富的功能。 本文将详细介绍 Spring MVC 的执行流程&#xff0c;帮助你理解它是如…...

c++day5

作业&#xff1a; 编写一个如下场景&#xff1a; 有一个英雄Hero类&#xff0c;私有成员&#xff0c;攻击&#xff0c;防御&#xff0c;速度&#xff0c;生命值&#xff0c;以及所有的set get 方法 编写一个 武器 Weapon 类&#xff0c;拥有私有成员攻击力&#xff0c;以及set …...

Deepseek 实战全攻略,领航科技应用的深度探索之旅

想玩转 Deepseek&#xff1f;这攻略别错过&#xff01;先带你了解它的基本原理&#xff0c;教你搭建运行环境。接着给出自然语言处理、智能客服等应用场景的实操方法与代码。还分享模型微调、优化技巧&#xff0c;结合案例加深理解&#xff0c;让你全面掌握&#xff0c;探索科技…...

公共数据授权运营模式研究(总体框架、主要模式及发展趋势)

本报告以公共数据运营模式为核心&#xff0c;以释放公共数据价值为目标&#xff0c;深入分析公共数据概念及特征&#xff0c;厘清公共数据运营的内涵及本质&#xff0c;提出纵深分域数据要素市场运营体系的总体思路&#xff0c;构建了一座&#xff08;一个数据底座&#xff09;…...

本地开发用ASP.NET Core Web API项目创建及测试

1. 服务端代码&#xff08;C#&#xff09; 1.1 创建ASP.NET Core Web API项目 打开Visual Studio 2022。 选择“创建新项目”。 选择“ASP.NET Core Web API”模板&#xff0c;点击“下一步”。 输入项目名称&#xff08;如OracleApi&#xff09;&#xff0c;选择项目位置&…...

【虚拟仪器技术】labview操作指南和虚拟仪器技术习题答案(一)

今天是2025年2月24日&#xff0c;画的是fate/Grand Order里面的阿尔托莉雅.卡斯特&#xff0c;武内老师的画。 目录 第1章 第2章 第3章 第4章 第5章 关注作者了解更多 我的其他CSDN专栏 毕业设计 求职面试 大学英语 过程控制系统 工程测试技术 虚拟仪器技术 可编程…...

SpringCloud系列教程:微服务的未来(二十五)-基于注解的声明队列交换机、消息转换器、业务改造

前言 在现代分布式系统中&#xff0c;消息队列是实现服务解耦和异步处理的关键组件。Spring框架提供了强大的支持&#xff0c;使得与消息队列&#xff08;如RabbitMQ、Kafka等&#xff09;的集成变得更加便捷和灵活。本文将深入探讨如何利用Spring的注解驱动方式来配置和管理队…...

LLM之论文阅读——Context Size对RAG的影响

前言 RAG 系统已经在多个行业中得到广泛应用&#xff0c;尤其是在企业内部文档查询等场景中。尽管 RAG 系统的应用日益广泛&#xff0c;关于其最佳配置的研究却相对缺乏&#xff0c;特别是在上下文大小、基础 LLM 选择以及检索方法等方面。 论文原文: On the Influence of Co…...

C#实现本地AI聊天功能(Deepseek R1及其他模型)。

前言 1、C#实现本地AI聊天功能 WPFOllamaSharpe实现本地聊天功能,可以选择使用Deepseek 及其他模型。 2、此程序默认你已经安装好了Ollama。 在运行前需要线安装好Ollama,如何安装请自行搜索 Ollama下载地址&#xff1a; https://ollama.org.cn Ollama模型下载地址&#xf…...

git 查询包含某个文件夹的步骤

步骤 1&#xff1a;拉取最新的远程分支信息 确保本地缓存的远程分支信息是最新的&#xff1a; bash 复制 git fetch --all 步骤 2&#xff1a;遍历所有远程分支并检查目标文件夹 使用 git ls-tree 检查每个分支是否包含目标文件夹。以下脚本会列出所有包含 your_folder_pa…...

微软开源神器OmniParser-v2.0本地部署教程

安装python环境 我这里是以前安装好的版本&#xff1a;python 3.11.5&#xff0c;这里不再介绍&#xff0c;有需要的可以在网上找教程。 安装Anaconda 我这里是以前安装好的版本&#xff1a;conda 23.7.4&#xff0c;这里也不再介绍&#xff0c;有需要的可以在网上找教程。 …...

解决 Git 合并冲突:当本地修改与远程提交冲突时

目录 错误原因分析 解决方法 1. 暂存本地修改并合并&#xff08;保留更改&#xff09; 2. 丢弃本地修改&#xff08;强制覆盖&#xff09; 3. 暂存修改后合并&#xff08;推荐&#xff1a;使用 git stash&#xff09; 4. 选择性合并&#xff08;手动处理冲突文件&#xf…...

VScode中Markdown PDF无法正确输出包含数学公式的pdf解决方案

在使用VScode的Markdown PDF插件时&#xff0c;可能会遇到无法正确输出包含公式的PDF文件的问题。下面为你提供一种有效的解决方案。 具体操作步骤 步骤一&#xff1a;定位模板文件 在安装Markdown PDF插件后&#xff0c;你需要找到对应的模板文件。该文件的路径通常如下&am…...

uniapp 网络请求封装(uni.request 与 uView-Plus)

一、背景 在开发项目中&#xff0c;需要经常与后端服务器进行交互&#xff1b;为了提高开发效率和代码维护性&#xff0c;以及降低重复性代码&#xff0c;便对网络请求进行封装统一管理。 二、创建环境文件 2.1、根目录新建utils文件夹&#xff0c;utils文件夹内新建env.js文…...

Jtti.cc:站群服务器SEO优化建议,如何分配多IP?

站群优化的核心目标之一是尽可能通过多个网站互相引导流量&#xff0c;从而提升主站的权重。这时候&#xff0c;多IP的分配至关重要&#xff0c;因为搜索引擎会检测到同一IP下的网站之间的关联性。如果一个IP地址下有过多的相似站点&#xff0c;搜索引擎可能会认为这些站点存在…...

银行系统功能架构设计元模型

1. 元模型核心目标 ​规范性:定义功能模块的标准化描述方式,便于跨团队协作。​可复用性:抽象通用组件,减少重复开发。​可扩展性:支持未来业务创新和技术升级(如开放银行API集成)。​2. 元模型层级结构 采用分层架构模式,分为以下核心层级: ​**(1) 业务功能层** ​…...

uniapp写的h5跳转小程序

使用场景&#xff1a; 我们对接第三方支付的时候&#xff0c;对方只提供了原生小程序id和appid&#xff0c;由我们的app和h5平台跳转至小程序。 遇到的问题&#xff1a; app跳转本地正常&#xff0c;线上报错如下 解决办法&#xff1a; 需要去微信开放平台申请应用appid 易…...

DeepSeek点燃AI大模型战火:编程语言争霸,谁将问鼎“终极武器”王座?

DeepSeek点燃AI大模型战火&#xff1a;编程语言争霸&#xff0c;谁将问鼎“终极武器”王座&#xff1f; 一、DeepSeek&#xff1a;AI大模型竞赛的“导火索” 2023年&#xff0c;中国AI公司深度求索&#xff08;DeepSeek&#xff09;发布DeepSeek-R1大模型&#xff0c;凭借其超…...

游戏引擎学习第123天

仓库:https://gitee.com/mrxiao_com/2d_game_3 黑板&#xff1a;线程同步/通信 目标是从零开始编写一个完整的游戏。我们不使用引擎&#xff0c;也不依赖任何库&#xff0c;完全自己编写游戏所需的所有代码。我们做这个节目不仅是为了教育目的&#xff0c;同时也是因为编程本…...

钉钉快捷免登录 通过浏览器打开第三方系统,

一、钉钉内跳转至浏览器的实现 使用钉钉JSAPI的跳转接口 在钉钉内通过dd.biz.navigation.openLink方法强制在系统浏览器中打开链接。此方法需在钉钉开发者后台配置应用权限&#xff0c;确保应用具备调用该API的资格37。 示例代码&#xff1a; dd.ready(() > {dd.biz.navigat…...

塔能科技构建智慧隧道生态系统——城市升级改造的协同创新典范

一、智慧隧道生态系统的概念与意义 &#xff08;一&#xff09;概念解析 智慧隧道生态系统是一个涵盖多方面协同关系的复杂概念。在隧道建设方面&#xff0c;它不仅仅是简单的挖掘和结构搭建&#xff0c;而是将智能化技术融入其中&#xff0c;例如采用先进的传感器技术&#x…...

在Anaconda的虚拟环境中安装R,并在vscode中使用

在 Anaconda 的虚拟环境中使用 R&#xff0c;并且希望在 VS Code 中同时使用 Python 和 R&#xff0c;确实需要同时安装 Python 和 R。这是因为 VS Code 的 Jupyter 插件和内核管理依赖于 Python&#xff0c;而 R 则作为 Jupyter 的另一个内核运行。 以下是具体的操作步骤和逻…...

创建型模式 - 建造者模式 (Builder Pattern)

创建型模式 - 建造者模式 (Builder Pattern) 建造者模式是一种创建型设计模式&#xff0c;它将一个复杂对象的构建与表示分离&#xff0c;使得同样的构建过程可以创建不同的表示。 需求描述 在游戏开发中&#xff0c;创建一个复杂的游戏角色&#xff0c;角色具有多种属性&…...

路由追踪核心技术深度解析:Traceroute与Tracert命令实战指南(跨平台/抓包/网络安全防护)

目录 路由器是什么&#xff1f; 路由器的基本功能&#xff1a; 路由追踪技术&#xff08;Traceroute&#xff09; 路由追踪的工作原理 实现技术 路由追踪的输出示例 路由追踪的用途 traceroute 命令&#xff08;Linux 和 macOS&#xff09; 基本语法 常用选项 示例 …...