Golang 哈希表详解

news2024/11/24 3:49:10

哈希表介绍

​ 一个映射,也成为关联数组,其实是一个由唯一键组成的集合,而每个键必然关联一个特定的值。这种键到值的关联关系称为映射,若在键到值的关联使用hash计算,就是哈希表,映射至少支持三个操作:

  • Add (Key,Value)

  • Remove(key)

  • value = LookUp (key)

Map的本质

map的底层结构

Golang的map使用溢出桶(链地址法)解决hash冲突

image-20230413161510215

Map结构

type hmap struct {
	count int // 代表map中元素的数量
	flags uint8 // 代表当前map的状态
	B uint8 // 2^B表示当前map中桶的数量
	noverflow uint16 // map中溢出桶的数量
	hash0     uint32 // 是哈希的种子,它能为哈希函数的结果引入随机性,这个值在创建哈希表时确定,并在调用哈希函数时作为参数传入
	buckets unsafe.Pointer // 指向map对应的桶数组的指针
	oldbuckets unsafe.Pointer // 存储未转移完毕的旧桶,扩容时非空
	nevacuate uintptr// 扩容时使用,用于标记当前旧桶中小于 nevacuate 的数据都已经转移到了新桶中
	extra *mapextra // 存储map中的溢出桶
}

桶结构

type bmap struct {
   // bucketCnt=8
   tophash [bucketCnt]uint8
}

溢出桶

type mapextra struct {
	overflow    *[]*bmap // buckets 的溢出桶
	oldoverflow *[]*bmap // oldbukets 的溢出桶
	nextOverflow *bmap // 指向下一个溢出桶
}

Map源码

创建

创建map

当使用make创建map时,只有指定的元素个数大于8时,或者使用字面量创建时元素个数>8时才会调用runtime.makemap函数进行map的创建。否则,将调用makemap_small函数创建hmap结构,但此时不会分配桶,而是在赋值(runtime.mapassign)操作时分配桶。

m := make(map[int8]int8, 9)
m1 := map[int8]int8{
		1: 1, 2: 2,
		3: 3, 4: 4,
		5: 5, 6: 6,
		7: 7, 8: 8,
		9: 9,
	}

创建流程

func makemap(t *maptype, hint int, h *hmap) *hmap {
    // 计算内存分配量时,使用最均匀的情况下的散列分布
    // hint:触发makemap函数时,传入的第二个参数,若为9,则9个元素可能会分布到不同的9个桶中
    // t.bucket.size:每个桶占用的字节,其大小等于key*8+value*8+tophash(固定8字节)+overflow指针大小(根据机器字大小4或8字节)
    // 该步骤主要计算内存溢出的可能性,若可能溢出,将hint置零
	mem, overflow := math.MulUintptr(uintptr(hint), t.bucket.size)
	if overflow || mem > maxAlloc {
		hint = 0
	}
	// 初始化hmap结构体
	if h == nil {
		h = new(hmap)
	}
    // 取得hash种子,hash种子的计算参与存放在当前m中  
	h.hash0 = fastrand()

	// 计算出需要的桶的个数
	B := uint8(0)
    // 根据hint个元素,循环计算出需要的桶数量
    // 因为后续涉及到真正的内存分配,所以这里按照最近凑的散列分布,即hint/8来控制桶的数量
	for overLoadFactor(hint, B) {
		B++
	}
	h.B = B
	// 开始为桶分配内存
	if h.B != 0 {
		var nextOverflow *bmap
        // makemap函数中的桶分配由makeBucketArray函数来实现
        // 主要任务:1.分配桶大小 2.若需要预分配溢出桶,则分配
		h.buckets, nextOverflow = makeBucketArray(t, h.B, nil)

		if nextOverflow != nil { // 若分配了溢出桶
            // 将预分配的溢出桶赋值给hmap
			h.extra = new(mapextra)
			h.extra.nextOverflow = nextOverflow
		}
	}

	return h
}

桶分配及溢出桶预分配

func makeBucketArray(t *maptype, b uint8, dirtyalloc unsafe.Pointer) (buckets unsafe.Pointer, nextOverflow *bmap) {
   // 需要分配的桶数量
   base := bucketShift(b)
   nbuckets := base
   // 如果桶的数量>b^4可能需要溢出桶
   if b >= 4 {
      // 在桶的数量上加上预估溢出桶数量
      nbuckets += bucketShift(b - 4)
      // 计算出桶需要的内存空间
      sz := t.bucket.size * nbuckets
      // 因为go将内存划分为不同的大小进行管理,所以这里内存管理器会返回up字节的内存大小,up>=sz
      up := roundupsize(sz)
      if up != sz {
         // 计算出分配的内存可以容纳多少个桶
         nbuckets = up / t.bucket.size
      }
   }

   // 当hash表所有键值被删除或该hash表被回收时dirtyalloc!=nil
   if dirtyalloc == nil {
      // 分配bucket数组
      buckets = newarray(t.bucket, int(nbuckets))
   } else {// 执行hash内存回收操作
      buckets = dirtyalloc
      size := t.bucket.size * nbuckets
      // 判断bucket是否存在指针对象
      if t.bucket.ptrdata != 0 {
         // 清楚包含指针类型元素占用的空间
         memclrHasPointers(buckets, size)
      } else {
         // 清楚非指针类型元素占用的空间
         memclrNoHeapPointers(buckets, size)
      }
   }
   // 判断是否需要分配溢出桶
   if base != nbuckets { 
      // 计算出溢出桶数组的指针地址
      nextOverflow = (*bmap)(add(buckets, base*uintptr(t.bucketsize)))
      // 计算出最后一个溢出桶的内存地址
      last := (*bmap)(add(buckets, (nbuckets-1)*uintptr(t.bucketsize)))
      // 将最后一个溢出桶的溢出指针设置为溢出桶数组的首地址,相当于将溢出桶数组首尾相连,从而避免内存泄漏问题
      last.setoverflow(t, (*bmap)(buckets))
   }
   return buckets, nextOverflow
}

赋值/更新

func mapassign(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
   if h == nil {
      panic(plainError("assignment to entry in nil map"))
   }
   // 因为map是并发不安全的,因此在并发读写的情况下会出现警告
   if h.flags&hashWriting != 0 {
      throw("concurrent map writes")
   }
   // 通过key和hash种子生成hash值
   hash := t.hasher(key, uintptr(h.hash0))
   // 将当前map标记为正在写
   h.flags ^= hashWriting
   // 当map中的所有键值对都被删除,内存可能被回收,因此这里重新分配一个bucket
   if h.buckets == nil {
      h.buckets = newobject(t.bucket) // newarray(t.bucket, 1)
   }

again:
   // 通过hash和桶个数进行与运算,得出桶的位置
   bucket := hash & bucketMask(h.B)
   // 判断当前map是否处于扩容状态,执行一个桶的搬迁
   if h.growing() {
      // 执行一次扩容操作
      growWork(t, h, bucket)
   }
   // 根据map中桶的首地址计算出目标桶的地址
   b := (*bmap)(add(h.buckets, bucket*uintptr(t.bucketsize)))
   // 返回hash的高8位
   top := tophash(hash)

   var inserti *uint8
   var insertk unsafe.Pointer
   var elem unsafe.Pointer
bucketloop:
   for {
      for i := uintptr(0); i < bucketCnt; i++ {
         // 如果第i个tophash 与 key计算出的tophash不同
         if b.tophash[i] != top {
            // 判断tophash[i]是否为空,且inserti是否为空,当此条件为true时,标识,当前tophash为第一个遇到的空tophash
            if isEmpty(b.tophash[i]) && inserti == nil {
               // 记录当前tophash的内存地址
               inserti = &b.tophash[i]
               // 记录当前tophash对应的key的内存地址
               insertk = add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
               // 记录当前tophash对应的value的内存地址
               elem = add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
            }
            // 若当前tophash 为emptyRest,表示后面的tophash都为空,而第一个空tophash已经被记录,此时则跳出循环
            if b.tophash[i] == emptyRest {
               break bucketloop
            }
            // 继续循环
            continue
         }
         // 运行至此处表示找到和当前key相同的tophash
         k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
         if t.indirectkey() {
            k = *((*unsafe.Pointer)(k))
         }
         // 如果key不相同 遍历下一个tophash
         if !t.key.equal(key, k) {
            continue
         }
         // already have a mapping for key. Update it.
         // 走到此处就可以确定当前为更新操作了
         // tophash 相同 且key也相同时更新该key的值
         if t.needkeyupdate() {
            typedmemmove(t.key, k, key)
         }
         // 的value内存所在的地址
         elem = add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
         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 
   }
   // 没有桶和溢出桶都没有找到合适的位置,创建一个新的溢出桶来存放
   if inserti == nil {
      newb := h.newoverflow(t, b)
      inserti = &newb.tophash[0]
      insertk = add(unsafe.Pointer(newb), dataOffset)
      elem = add(insertk, bucketCnt*uintptr(t.keysize))
   }
   // 若key为指针类型,分配新内存,将insertK指向新分配的内存
   if t.indirectkey() {
      kmem := newobject(t.key)
      *(*unsafe.Pointer)(insertk) = kmem
      insertk = kmem
   }
   // 若value为指针类型,分配新内存,将elem指向新分配的内存
   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 {
      throw("concurrent map writes")
   }
   // 清除标记
   h.flags &^= hashWriting
   if t.indirectelem() {
      elem = *((*unsafe.Pointer)(elem))
   }
   return elem
}

取值

map取值的函数有两个,runtime.mapaccess1runtime.mapaccess2区别在于后者返回两个参数v,bool,用以判断map中是否存在相应的key。

func mapaccess1(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
  
   if msanenabled && h != nil {
      msanread(key, t.key.size)
   }
   // map为空或者元素个数为0 返回元素类型的零值
   if h == nil || h.count == 0 {
      if t.hashMightPanic() {
         t.hasher(key, 0) // see issue 23734
      }
      return unsafe.Pointer(&zeroVal[0])
   }
   if h.flags&hashWriting != 0 {
      throw("concurrent map read and map write")
   }
   // 根据key 和hash种子计算出key的对应hash
   hash := t.hasher(key, uintptr(h.hash0))
   // 当前hash拥有的桶的个数
   m := bucketMask(h.B)
   // 获取 hash&m 计算出桶的位置
   b := (*bmap)(add(h.buckets, (hash&m)*uintptr(t.bucketsize)))
   // 判断是否处于扩容,即 h.oldbuckets 不为nil
   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
      }
   }
   // 计算出tophash 也就是hash的高八位
   top := tophash(hash)
   // 开始通过tophash遍历 桶+溢出桶
bucketloop:
   for ; b != nil; b = b.overflow(t) {
      for i := uintptr(0); i < bucketCnt; i++ {
         // 依次判断tophash
         if b.tophash[i] != top {
            // 表示后续tophash都为空,退出循环,取值失败
            if b.tophash[i] == emptyRest {
               break bucketloop
            }
            continue
         }
         // 取出map中tophash对应的key值
         k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
         if t.indirectkey() {
            k = *((*unsafe.Pointer)(k))
         }
         // 对比取值的k与tophash对应的key是否相同
         if t.key.equal(key, k) {
            // 找到key对应value值
            e := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
             // 若间接引用,需要进行解引用
             if t.indirectelem() {
               e = *((*unsafe.Pointer)(e))
            }
            return e
         }
      }
   }
   return unsafe.Pointer(&zeroVal[0])
}

删除

func mapdelete(t *maptype, h *hmap, key unsafe.Pointer) {
   // 确保没有并发操作map
   if h.flags&hashWriting != 0 {
      throw("concurrent map writes")
   }
   // 计算hash值
   hash := t.hasher(key, uintptr(h.hash0))
   // 标记当前map处于写状态
   h.flags ^= hashWriting
   // 计算出该hash可能存放在哪个桶中
   bucket := hash & bucketMask(h.B)
   // 若map处于扩容状态
   if h.growing() {
      // 执行一次扩容任务
      growWork(t, h, bucket)
   }
   // 找到桶的地址
   b := (*bmap)(add(h.buckets, bucket*uintptr(t.bucketsize)))
   bOrig := b
   // 计算出tophash
   top := tophash(hash)
search:
   // 遍历桶+溢出桶
   for ; b != nil; b = b.overflow(t) {
      for i := uintptr(0); i < bucketCnt; i++ {
         // 依次对比桶中的tophash
         if b.tophash[i] != top {
            // 若桶中tophash为emptyRest,桶中后续空间不存在key
            if b.tophash[i] == emptyRest {
               // 没找到,退出
               break search
            }
            continue
         }
         // 计算key的地址
         k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
         k2 := k
         if t.indirectkey() {
            k2 = *((*unsafe.Pointer)(k2))
         }
         // 若当前key与想要删除的key不同,继续遍历
         if !t.key.equal(key, k2) {
            continue
         }
         // 清除key
         if t.indirectkey() {// 间接引用
            *(*unsafe.Pointer)(k) = nil
         } else if t.key.ptrdata != 0 {
            memclrHasPointers(k, t.key.size)
         }
         // 计算value地址
         e := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
         // 清除vlaue
         if t.indirectelem() {// 间接引用
            *(*unsafe.Pointer)(e) = nil
         } else if t.elem.ptrdata != 0 {// 指针
            memclrHasPointers(e, t.elem.size)
         } else {// 非指针
            memclrNoHeapPointers(e, t.elem.size)
         }
         // 将该tophash设为emptyOne
         b.tophash[i] = emptyOne
		 // 当前为最后一个桶或者桶中tophash都为emptyRest,跳出循环
         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的tophash标记为emptyRest
         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:
         h.count--
         // 若map中元素被清空
         if h.count == 0 {
            // 重置hash算法
            h.hash0 = fastrand()
         }
         break search
      }
   }

   if h.flags&hashWriting == 0 {
      throw("concurrent map writes")
   }
   h.flags &^= hashWriting
}

Map 扩容

扩容模式判断

map扩容由赋值操作runtime.mapassign触发。

map扩容分两种:

  • 翻倍扩容:大多数桶都处于满负载状态

    元素数量达到map负载因子6.5:count/buckets>6.5

    func overLoadFactor(count int, B uint8) bool {
    	return count > bucketCnt && uintptr(count) > loadFactorNum*(bucketShift(B)/loadFactorDen)
    }
    
  • 等量扩容:溢出桶过多,元素过少

    h.B<=15,溢出桶>=2^B

    h.B>15,溢出桶>=2^15

    func tooManyOverflowBuckets(noverflow uint16, B uint8) bool {
       if B > 15 {
          B = 15
       }
       return noverflow >= uint16(1)<<(B&15)
    }
    

扩容前的准备

并没有直接进行扩容,而是遵循写时复制原则,只有在对数据操作时才进行扩容。

func hashGrow(t *maptype, h *hmap) {
   bigger := uint8(1)
   // 溢出桶太多,保持哈希表大小不变
   if !overLoadFactor(h.count+1, h.B) {
      bigger = 0
      h.flags |= sameSizeGrow
   }
   // 保存旧桶
   oldbuckets := h.buckets
   // 创建新的桶
   newbuckets, nextOverflow := makeBucketArray(t, h.B+bigger, nil)

   flags := h.flags &^ (iterator | oldIterator)
   if h.flags&iterator != 0 {
      flags |= oldIterator
   }
   // 设置B
   h.B += bigger
   h.flags = flags
   h.oldbuckets = oldbuckets
   h.buckets = newbuckets
   h.nevacuate = 0
   h.noverflow = 0

   if h.extra != nil && h.extra.overflow != nil {
      if h.extra.oldoverflow != nil {
         throw("oldoverflow is not nil")
      }
      // 将当前溢出桶设置为旧的溢出桶
      h.extra.oldoverflow = h.extra.overflow
      h.extra.overflow = nil
   }
   if nextOverflow != nil {
      if h.extra == nil {
         h.extra = new(mapextra)
      }
      h.extra.nextOverflow = nextOverflow
   }
}

执行扩容

具体的扩容操作被分散在对map的操作中(赋值、取值、删除),这样做的好处是避免了,一次性完成桶搬迁所产生的性能抖动问题。

执行扩容任务

func growWork(t *maptype, h *hmap, bucket uintptr) {
   // 搬迁命中的桶
   evacuate(t, h, bucket&h.oldbucketmask())
   // 若搬迁任务没有完成,在游标处再搬迁一个桶
   if h.growing() {
      evacuate(t, h, h.nevacuate)
   }
}

扩容具体实现

新桶数量-1&旧桶位置=旧桶在新桶中的位置

并发安全

通过源码可以观察到Go并没有为map进行并发保护,若并发操作map会触发painc。

sync.map提供并发安全的map。

func main() {
	m := make(map[int]int)
	go func() {
		for i := 0; i < 1000; i++ {
			m[i] = i + 1
		}
	}()
	go func() {
		for i := 0; i < 1000; i++ {
			fmt.Println(m[i])
		}
	}()
	time.Sleep(3 * time.Second)
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/425467.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

每日做题总结——day01

目录 选择题 for循环 指针数组 位段 getchar 大小端存储 进制与格式控制符 位运算 数组指针 二维数组的存储 计算二进制中1的个数 斐波那契数列求递归次数 编程题 删除公共字符 排序子序列 倒置字符串 选择题 for循环 解析&#xff1a;该题主要看for…

react face to face

React面试题 创建一个react项目 1.全局安装create-react-app npm install -g create-react-app 2.创建项目 create-react-app myapp 3.局部安装&#xff0c;可以直接用npx npx create-react-app myapp 4.进入文件夹 cd myapp 5.启用项目 npm start&#xff08;开发模式下运行&…

浅谈DNS-rebinding

为啥突然要总结一下这个很老的知识点&#xff0c;我也不知道&#xff0c;可能太菜了&#xff0c;闲下来总得学点什么~ DNS Rebinding 0x01 攻击简介 DNS Rebinding也叫做DNS重绑定攻击或者DNS重定向攻击。在这种攻击中&#xff0c;恶意网页会导致访问者运行客户端脚本&#xff…

springboot-参数校验

SpringBoot 中使用 Valid 注解 Exception 全局处理器优雅处理参数验证 注解Valid的主要作用是用于数据效验&#xff0c;可以在定义的实体中的属性上&#xff0c;添加不同的注解来完成不同的校验规则。Controller类中在参数中添加Valid注解来开启效验功能Valid配合 Spring 会抛…

2023年4月10日下午总结和近日感悟

技术和钱 人生&#xff0c;活到现阶段&#xff0c;已于一月前&#xff0c;深感技术就是这么回事&#xff0c;不再像以前那样为学习某样东西而不问来由&#xff0c;闷头去学&#xff08;也许是因为即将步入下一个人生阶段&#xff09;。虽然&#xff0c;乐于也想去接受新技术&a…

centos7下基于nginx+uwsgi部署Django项目

文章目录一&#xff1a;基础环境介绍&#xff1a;二&#xff1a;部署环境安装配置&#xff1a;1.基础依赖环境安装2.安装wegt&#xff0c;vim&#xff0c;unzip等必须命令3.安装python与pip&#xff08;或者python多版本管理工具pyenv等&#xff09;4.安装nginx5.安装uwsgi三&a…

json和CMake简单入门

Json 介绍 Json是一种轻量级的数据交换格式&#xff08;也叫数据序列化方式&#xff09;。Json采用完全独立于编程语言的文本格式来存储和表示数据。简洁和清晰的层次结构使得 Json 成为理想的数据交换语言。 易于人阅读和编写&#xff0c;同时也易于机器解析和生成&#xff…

Binder基本知识

1&#xff1a;IPC 原理 从进程角度来看 IPC 机制 每个 Android 的进程&#xff0c;只能运行在自己进程所拥有的虚拟地址空间。对应一个4GB 的虚拟地址空间&#xff0c;其中 3GB 是用户空间&#xff0c;1GB 是内核空间&#xff0c;当然内核空间的大小是可以通过参数配置调整的…

网络安全之密码学

目录 密码学 定义 密码的分类 对称加密 非对称加密 对称算法与非对称算法的优缺点 最佳解决办法 --- 用非对称加密算法加密对称加密算法的密钥 非对称加密如何解决对称加密的困境 密钥传输风险 密码管理难 常见算法 对称算法 非对称算法 完整性与身份认证最佳解决…

优维低代码:定制构件的打包及部署

优维低代码技术专栏&#xff0c;是一个全新的、技术为主的专栏&#xff0c;由优维技术委员会成员执笔&#xff0c;基于优维7年低代码技术研发及运维成果&#xff0c;主要介绍低代码相关的技术原理及架构逻辑&#xff0c;目的是给广大运维人提供一个技术交流与学习的平台。 连载…

用Python写一个BMI计算代码

有粉丝问我怎么写一个BMI算法&#xff0c;安排 height float(input("请输入身高&#xff08;米&#xff09;: ")) weight float(input("请输入体重&#xff08;千克&#xff09;: ")) 计算BMI bmi weight / (height ** 2) 显示结果 print("您的…

MySQL8.0.32安装以及环境配置

文章目录一、安装MySQL二、错误集1. 如果操作失误&#xff0c;可以重新安装一、安装MySQL 下载MySQL的社区版的压缩包&#xff1a;https://dev.mysql.com/get/Downloads/MySQL-8.0/mysql-8.0.32-winx64.zip 将压缩包解压到你要安装的目录中 将对应的安装路径添加到环境变量中…

Java面试核心知识点梳理(二)——Java集合

文章目录前言1. List1.1 ArrayList&#xff08;数组&#xff09;1.2 Vector&#xff08;数组、线程安全&#xff09;1.3 LinkedList&#xff08;链表&#xff09;2. Set2.1 HashSet&#xff08;Hash表&#xff09;2.2 TreeSet&#xff08;二叉树&#xff09;2.3 LinkHashSet&am…

3分钟通过日志定位bug,这个技能测试人必须会

♥ 前 言 软件开发中通过日志记录程序的运行情况是一个开发的好习惯&#xff0c;对于错误排查和系统运维都有很大帮助。 Python 标准库自带了强大的 logging 日志模块&#xff0c;在各种 python 模块中得到广泛应用。 一、简单使用 1. 入门小案例 import logging logging.ba…

国内Google翻译失效的解决方法(MAC/WIN)

Google宣布停止在中国大陆的翻译服务&#xff0c;原因是&#xff1a;使用率低&#xff1f;&#xff1f;&#xff0c;这导致Chrome浏览器网页翻译失效。对于一些使用Chrome&#xff0c;经常鼠标下一秒就在大洋彼岸扒拉资料&#xff0c;且英语不太好的同学来说变得非常难受。为此…

【软考中级·网络工程师】校验码差错控制

差错控制&#x1f349; 无论通信系统如何可靠&#xff0c;都不能做到完美无缺。因此&#xff0c;必须考虑怎样发现和纠正信号传输重的差错。通信过程中出现的差错大致可以分为两类&#xff1a; 一类是由热噪声引起的随机错误&#xff1b;热噪声&#xff1a;一种由电子的热运动…

基于ArcGIS、ENVI、InVEST、FRAGSTATS等多技术融合提升

专题一 空间数据获取与制图 1.1 软件安装与应用讲解 1.2 空间数据介绍 1.3海量空间数据下载 1.4 ArcGIS软件快速入门 1.5 Geodatabase地理数据库 ​ 专题二 ArcGIS专题地图制作 2.1专题地图制作规范 2.2 空间数据的准备与处理 2.3 空间数据可视化&#xff1a;地图符号…

无头盔PICO-unity开发日记3(UI按钮点击)

目录 1.UI界面加入组件 2.加入xr输入模块 3.设置光线投射遮罩 结果&#xff1a; 前提&#xff1a;做好一个ui界面 1.UI界面加入组件 画布加跟踪设备图形光线投射仪&#xff08;tracked device graphic raycaster&#xff09; 允许画布被追踪设备操纵 2.加入xr输入模块 sys…

C++ --模拟实现搜索二叉树

文章目录#搜索二叉树1. 搜索二叉树特点2. 操作分析2.0 结点结构2.1 插入2.2 升序查看2.3 查找2.4 删除2.5 前序拷贝构造3. 完整代码4. 时间复杂度分析5. 简单应用5.1 字典搜索5.2 统计次数#搜索二叉树 1. 搜索二叉树特点 若它的左子树不为空&#xff0c;则左子树上所有节点的…

Dockerfile及新型容器镜像构建技术

文章目录一、容器镜像分类1、操作系统类2、应用类二、容器镜像获取方法1、在dockerhub直接下载2、把操作系统中文件系统打包为容器镜像3、把正在运行的容器打包为容器镜像&#xff0c;即docker commit4、通过dockerfile实现容器镜像的自定义以及生成三、dockerfile1、dockerfil…