三. go 常见数据结构实现原理之 silce

2023-11-05

一. 基础

  1. 为什么要使用切片: slice出现的原因主要是为了解决数组长度固定的问题
  2. slice是一种动态数组的实现,它由三个属性组成:array指针数组,len表示slice中元素的个数,cap表示底层数组中元素的个数,len不能大于cap,假设指定cap为10,当添加第11个元素时会报错
  3. 切片是数组的一个引用,是引用类型,传递时是引用拷贝
  4. 切片的长度是可变的,但是在初始化时如果直接赋值不能超过初始化的len长度
  5. 其它问题小总结:
  1. 每个切片都指向一个底层数组
  2. 每个切片都保存了当前切片的长度、底层数组可用容量
  3. 使用len()计算切片长度时间复杂度为O(1),不需要遍历切片
  4. 使用cap()计算切片容量时间复杂度为O(1),不需要遍历切片
  5. 通过函数传递切片时,不会拷贝整个切片,因为切片本身只是个结构体而矣
  6. 使用append()向切片追加元素时有可能触发扩容,扩容后将会生成新的切片
  1. 切片的两种创建方式
  1. 第一种: 首先创建数组,让切片去引用数组,创建处理
  2. 第二种: 通过make创建切片(参数说明: type 数据类型,len: 大小, cap:容量可选值,但要大于len
	//1.声明一个数组,指定长度为10
 	var array [10]int
 	//array[5:6]引用这个数组,通过数组的方式创建切片,
 	//数组下标5开始,6结束(不包含6)
    var slice = array[5:6]

	slice := make([]int, 5, 10)
	
	//原理类似make,也可以看为创建数组不指定长度?
	//注意: [ ] 里面不要写数组的容量,因为如果写了个数以后就是数组了,而不是切片了
	var ss []string = []string{"tom","aaa"}
  1. 两种方式的区别:
  1. 直接引用数组方式: 数组是事先存在的,注意数组和切片操作可能作用于同一块内存
  2. make方式: 底层也会创建一个数组,由切片底层维护
  1. cap(切片): 获取当前切片中存放的最多数据的个数(动态可伸缩的,有点像HashMap的扩容,底层的数组长度)

几个小问题

1. 题目一

  1. 判断输出什么,解释:
  1. main函数中定义了一个10个长度的整型数组array,然后定义了一个切片slice,切取数组的第6个元素,
  2. 打印slice的长度和容量,
  3. 判断切片的第一个元素和数组的第6个元素地址是否相等
func main() {
    var array [10]int
    var slice = array[5:6]
    fmt.Println("lenth of slice: ", len(slice))
    fmt.Println("capacity of slice: ", cap(slice))
    fmt.Println(&slice[0] == &array[5])
}
  1. 结果: slice跟据数组array创建,与数组共享存储空间,slice起始位置是array[5],长度为1,容量为5,slice[0]和array[5]地址相同

2. 题目二

  1. 判断输出什么,解释:
  1. AddElement(): 接收一个切片和一个元素,把元素append进切片中,并返回
  2. main()函数中定义一个切片,并向切片中append 3个元素
  3. 接着调用AddElement()继续向切片append进第4个元素同时定义一个新的切片newSlice
  4. 最后判断新切片newSlice与旧切片slice是否共用一块存储空间
func AddElement(slice []int, e int) []int {
    return append(slice, e)
}
func main() {
    var slice []int
    slice = append(slice, 1, 2, 3)
    newSlice := AddElement(slice, 4)
    fmt.Println(&slice[0] == &newSlice[0])
}
  1. 结果: append函数执行时会判断切片容量是否能够存放新增元素,如果不能则会重新申请存储空间,新存储空间将是原来的2倍或1.25倍(取决于扩展原空间大小)本例中实际执行了两次append操作,第一次空间增长到4,所以第二次append不会再扩容,所以新旧两个切片将共用一块存储空间。程序会输出”true

3. 题目三

  1. 判断输出什么,解释:
  1. 该段程序源自select的实现代码,程序中定义一个长度为10的切片order,
  2. pollorder和lockorder分别是对order切片做了order[low:high:max]操作生成的切片,
  3. 最后程序分别打印pollorder和lockorder的容量和长度
func main() {
    orderLen := 5
    order := make([]uint16, 2 * orderLen)
    pollorder := order[:orderLen:orderLen]
    lockorder := order[orderLen:][:orderLen:orderLen]
    fmt.Println("len(pollorder) = ", len(pollorder))
    fmt.Println("cap(pollorder) = ", cap(pollorder))
    fmt.Println("len(lockorder) = ", len(lockorder))
    fmt.Println("cap(lockorder) = ", cap(lockorder))
}
  1. 结果: order[low:high:max]操作,意思是对order进行切片,新切片范围是[low, high),新切片容量是max。order长度为2倍的orderLen,pollorder切片指的是order的前半部分切片,lockorder指的是order的后半部分切片,即原order分成了两段。所以,pollorder和lockerorder的长度和容量都是orderLen,即5

4. 数组和切片陷阱

  1. 参考博客

二. Slice实现原理

切片的创建与底层结构

  1. Slice底层依托数组实现,查看slice结构体,array指针指向底层数组,len表示切片长度,cap表示底层数组容量,当调用make()函数初始化时
func makeslice(et *_type, len, cap int) unsafe.Pointer {
	mem, overflow := math.MulUintptr(et.size, uintptr(cap))
	//1.对切片有效数据的个数,与容量等进行验证 make([]切片类型,有效个数,容量(大于等于有效个数))
	//当验证不同过时,panic终止程序继续运行
	if overflow || mem > maxAlloc || len < 0 || len > cap {
		mem, overflow := math.MulUintptr(et.size, uintptr(len))
		if overflow || mem > maxAlloc || len < 0 {
			panicmakeslicelen()
		}
		panicmakeslicecap()
	}
	//2.前面的验证通过后,调用mallocgc函数,该函数中会获取一个int类型值,也就是结构体中array unsafe.Pointer的长度
	//最终会创建一个 slice 结构体
	return mallocgc(mem, et, true)
}
// src/runtime/slice.go:slice
type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}
  1. 例如: slice := make([]int, 5, 10), 表示:

该Slice长度为5,即可以使用下标slice[0] ~ slice[4]来操作里面的元素,capacity为10,表示后续向slice添加新的元素时可以不必重新分配内存,直接使用预留内存即可
在这里插入图片描述

  1. 使用数组创建切片: 内存中首先开辟一个array空间,然后开辟silce切片空间,引用了array的变量,所以地址值与引用的intArr数组中首个引用元素的值相同

切片从数组array[5]开始,到数组array[7]结束(不含array[7])即切片长度为2,数组后面的内容都作为切片的预留内存,即capacity为5
在这里插入图片描述

append() 与切片的扩容

  1. 当使用append向Slice追加元素时,如果Slice空间不足,会触发Slice扩容, 例如向一个capacity为5,且length也为5的Slice再次追加1个元素时,就会发生扩容
  2. 扩容时会执行底层的growslice()函数, 在该函数中会经过多个判断
  1. 如果当前添加元素所需容量 (cap) 大于原先容量的两倍 (doublecap),会已当前所需容量作为扩容容量
  2. 当前所需容量(cap)不大于原容量的两倍(doublecap),会判断原切片的长度(old.len)是否小于1024
  3. 如果原切片长度(lod.len)小于1024, 扩容为原容量的两倍
  4. 如果原切片长度(lod.len)大于1024,会获取原切片长度,以原切片长度的1.25倍进行扩容,直到大于所需容量(cap)为止,然后判断最终申请容量(newcap)是否溢出,如果溢出,最终申请容量等于所需容量(cap)
  5. 例如: 所需容量 cap = 1024+2 = 1026,doublecap = 2048, 大于1024, 获取原切片长度 old.len = 1024,计算扩容容量newcap = 1024 + 1024/4 = 1280
func growslice(et *_type, old slice, cap int) slice {
	if raceenabled {
		callerpc := getcallerpc()
		racereadrangepc(old.array, uintptr(old.len*int(et.size)), callerpc, abi.FuncPCABIInternal(growslice))
	}
	if msanenabled {
		msanread(old.array, uintptr(old.len*int(et.size)))
	}
	if asanenabled {
		asanread(old.array, uintptr(old.len*int(et.size)))
	}

	if cap < old.cap {
		panic(errorString("growslice: cap out of range"))
	}

	if et.size == 0 {
		// append should not create a slice with nil pointer but non-zero len.
		// We assume that append doesn't need to preserve old.array in this case.
		return slice{unsafe.Pointer(&zerobase), old.len, cap}
	}

	newcap := old.cap
	doublecap := newcap + newcap
	if cap > doublecap {
		newcap = cap
	} else {
		const threshold = 256
		if old.cap < threshold {
			newcap = doublecap
		} else {
			// Check 0 < newcap to detect overflow
			// and prevent an infinite loop.
			for 0 < newcap && newcap < cap {
				// Transition from growing 2x for small slices
				// to growing 1.25x for large slices. This formula
				// gives a smooth-ish transition between the two.
				newcap += (newcap + 3*threshold) / 4
			}
			// Set newcap to the requested cap when
			// the newcap calculation overflowed.
			if newcap <= 0 {
				newcap = cap
			}
		}
	}

	var overflow bool
	var lenmem, newlenmem, capmem uintptr
	// Specialize for common values of et.size.
	// For 1 we don't need any division/multiplication.
	// For goarch.PtrSize, compiler will optimize division/multiplication into a shift by a constant.
	// For powers of 2, use a variable shift.
	switch {
	case et.size == 1:
		lenmem = uintptr(old.len)
		newlenmem = uintptr(cap)
		capmem = roundupsize(uintptr(newcap))
		overflow = uintptr(newcap) > maxAlloc
		newcap = int(capmem)
	case et.size == goarch.PtrSize:
		lenmem = uintptr(old.len) * goarch.PtrSize
		newlenmem = uintptr(cap) * goarch.PtrSize
		capmem = roundupsize(uintptr(newcap) * goarch.PtrSize)
		overflow = uintptr(newcap) > maxAlloc/goarch.PtrSize
		newcap = int(capmem / goarch.PtrSize)
	case isPowerOfTwo(et.size):
		var shift uintptr
		if goarch.PtrSize == 8 {
			// Mask shift for better code generation.
			shift = uintptr(sys.Ctz64(uint64(et.size))) & 63
		} else {
			shift = uintptr(sys.Ctz32(uint32(et.size))) & 31
		}
		lenmem = uintptr(old.len) << shift
		newlenmem = uintptr(cap) << shift
		capmem = roundupsize(uintptr(newcap) << shift)
		overflow = uintptr(newcap) > (maxAlloc >> shift)
		newcap = int(capmem >> shift)
	default:
		lenmem = uintptr(old.len) * et.size
		newlenmem = uintptr(cap) * et.size
		capmem, overflow = math.MulUintptr(et.size, uintptr(newcap))
		capmem = roundupsize(capmem)
		newcap = int(capmem / et.size)
	}

	// The check of overflow in addition to capmem > maxAlloc is needed
	// to prevent an overflow which can be used to trigger a segfault
	// on 32bit architectures with this example program:
	//
	// type T [1<<27 + 1]int64
	//
	// var d T
	// var s []T
	//
	// func main() {
	//   s = append(s, d, d, d, d)
	//   print(len(s), "\n")
	// }
	if overflow || capmem > maxAlloc {
		panic(errorString("growslice: cap out of range"))
	}

	var p unsafe.Pointer
	if et.ptrdata == 0 {
		p = mallocgc(capmem, nil, false)
		// The append() that calls growslice is going to overwrite from old.len to cap (which will be the new length).
		// Only clear the part that will not be overwritten.
		memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem)
	} else {
		// Note: can't use rawmem (which avoids zeroing of memory), because then GC can scan uninitialized memory.
		p = mallocgc(capmem, et, true)
		if lenmem > 0 && writeBarrier.enabled {
			// Only shade the pointers in old.array since we know the destination slice p
			// only contains nil pointers because it has been cleared during alloc.
			bulkBarrierPreWriteSrcOnly(uintptr(p), uintptr(old.array), lenmem-et.size+et.ptrdata)
		}
	}
	memmove(p, old.array, lenmem)

	return slice{p, old.len, newcap}
}
  1. 注意如果继续扩容当append,1280->1696,似乎不是1.25倍,而是1.325倍,查看growslice()中执行的roundupsize()用来内存对齐函数,golang中是根据对象大小来配不同的mspan内存的,为了避免造成过多的内存碎片,slice在扩容中需要对扩容后的cap容量进行内存对齐的操作,如果所需容量cap在变成1600后又进入了内存对齐的过程,最终cap变为了1696, 也就是1.325倍
func roundupsize(size uintptr) uintptr {
	if size < _MaxSmallSize {
		if size <= smallSizeMax-8 {
			return uintptr(class_to_size[size_to_class8[divRoundUp(size, smallSizeDiv)]])
		} else {
			return uintptr(class_to_size[size_to_class128[divRoundUp(size-smallSizeMax, largeSizeDiv)]])
		}
	}
	if size+_PageSize < size {
		return size
	}
	return alignUp(size, _PageSize)
}

切片的值传递引用传递

  1. 首先切片是值传递
  2. 在上面我们了解到slice底层是一个结构体,len、cap、array分别表示长度、容量、底层数组的地址,
  1. 当slice作为函数的参数传递的时候,跟普通结构体的传递是没有区别的;
  2. 如果直接传slice,实参slice是不会被函数中的操作改变的,
  3. 如果传递的是slice的指针,是会改变原来的slice的;
  4. 注意: 无论是传递slice还是slice的指针,如果改变了slice的底层数组,都是会影响slice的

切片再切片(特殊切片)

  1. 先看一下截取切片,在截取时,通过截取创建出的多个slice实际作用在同一个底层数组上,这里我们可以变向的看成浅拷贝
  slice := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
  s1 := slice[2:5]
  s2 := s1[2:7]
  1. 截取出的新切片追加元素时,还要考虑一个扩容问题, 在扩容时,会创建一个新的底层数组的问题

切片的 Copy

  1. 先看一下截取切片,在截取时,通过截取创建出的多个slice实际作用在同一个底层数组上,这里我们可以变向的看成浅拷贝
  slice := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
  s1 := slice[2:5]
  s2 := s1[2:7]
  1. golang提供了copy函数
  slice1 := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
  var slice2 []int
  slice3 := make([]int, 5)

  //执行发现slice1的内容并未copy到slice2
  copy_1 := copy(slice2, slice1)

 //执行发现slice1的内容成功copy到了slice3中(与slice2不同的是cap长度)
 copy_2 := copy(slice3, slice1)

 //修改slice3中数据并不会影响copy_2
 slice3[0] = 100
  1. 总结:
  1. 通过截取方式创建的多个新slice,底层实际都作用在同一个数组上,可以看为浅拷贝,修改其中一个,其它的也会变
  2. golang提供了copy函数, 在通过该函数对slice进行copy时要注意目标切片大小,不能为0, 也就是需要在拷贝前申请内存空间
  3. 通过copy函数操作切片时,可以看为深拷贝,修改其中一个,不会影响到复制出的新切片
  1. 查看copy()源码,在对slice进行coyp()动作时,底层会执行runtime/slice.go文件中slicecopy函数
func slicecopy(to, fm slice, width uintptr) int {
  // 如果源切片或者目标切片有一个长度为0,那么就不需要拷贝,直接 return 
  if fm.len == 0 || to.len == 0 {
    return 0
  }

  // n 记录下源切片或者目标切片较短的那一个的长度
  n := fm.len
  if to.len < n {
    n = to.len
  }

  // 如果入参 width = 0,也不需要拷贝了,返回较短的切片的长度
  if width == 0 {
    return n
  }

  //如果开启竞争检测
  if raceenabled {
    callerpc := getcallerpc()
    pc := funcPC(slicecopy)
    racewriterangepc(to.array, uintptr(n*int(width)), callerpc, pc)
    racereadrangepc(fm.array, uintptr(n*int(width)), callerpc, pc)
  }
  if msanenabled {
    msanwrite(to.array, uintptr(n*int(width)))
    msanread(fm.array, uintptr(n*int(width)))
  }

  size := uintptr(n) * width
  if size == 1 { // common case worth about 2x to do here
    // TODO: is this still worth it with new memmove impl?
    //如果只有一个元素,那么直接进行地址转换
    *(*byte)(to.array) = *(*byte)(fm.array) // known to be a byte pointer
  } else {
    //如果不止一个元素,那么就从 fm.array 地址开始,拷贝到 to.array 地址之后,拷贝个数为size
    memmove(to.array, fm.array, size)
  }
  return n
}

总结

  1. 自己对切片的理解,在我们使用slice时需要调用make()函数进行初始化,底层会执行makeslice(),调用mallocgc(),最终会创建一个slice结构体变量
  1. 该结构体中有三个属性:array unsafe.Pointer 指向底层存储数据的数组指针, len表示切片长度,cap表示底层数组容量(也可以理解为当前切片触发扩容的值与当前切片保存数据的最大个数)
  2. 假设 cap 未超过1024,当cap==len时,切片会自动扩容,扩容为当前cap的2倍,当cap超过1024时,则扩容加上1/4倍
  1. 扩容规则总结,当使用append向Slice追加元素时,如果Slice空间不足,会触发Slice扩容,执行底层的growslice()函数:
  1. 当执行append()对切片进行追加时,底层会通过growslice()进行扩容, 在该方法中 首先判断当前Slice中是否还有空闲容量,
  2. 如果当前数组有空余位置不需要扩容,直接将元素追加到当前数组上,并且设置Slice.len++
  3. 如果没有空余位置,会重新分配一块新的内存地址,在分配时,会判断,当前切片元素个数是否小于1024,
  4. 小于则新Slice容量将扩大为原来的2倍
  5. 大于等于1024时,则新Slice容量将扩大为原来的1.25倍
  6. 扩容后,将新元素追加进新Slice,Slice.len++,返回新的Slice
  1. slice和数组有什么区别
  1. 长度和容量:数组的长度是固定的,一旦定义就不能改变;而切片的长度和容量都可以动态改变,可以根据需要动态扩容
  2. 内存布局:数组是一个连续的内存块,所有元素的类型都相同;而切片是一个引用类型,它包含一个指向底层数组的指针、长度和容量
  3. 传递方式:数组在函数调用时会被复制一份,因此对数组的修改不会影响原始数组;而切片在函数调用时只会传递指针和长度,不会复制整个切片,因此对切片的修改会影响原始切片。
  4. 初始化方式:数组可以使用[n]T{…}的方式进行初始化,其中n表示数组的长度,T表示数组元素的类型;而切片可以使用make([]T, len, cap)的方式进行初始化,其中len表示切片的长度,cap表示切片的容量。
  1. slice是有序的吗

是有序的,查看slice底层,实际使用一个数组用来存储数据,通过array unsafe.Pointer指针属性指向该数组

  1. 传参数组和传参slice有什么区别?传参slice会有什么问题吗?
  1. 在函数中以数组作为入参时,函数会接收到该数组的副本,在函数内部对该数组元素的修改不会影响原始数组。由于是复制的副本进行传递如果一个非常大的数组作为参数,对性能可能会有影响
  2. 传递一个 slice 作为参数时,会传递这个slice 的引用,如果在函数内部修改了slice中的元素,会影响到原始值,但是在处理大量数据时,使用 slice 作为函数参数可以减少复制和开
  1. 参考博客
  1. GO专家编程
  2. 参考博客
  3. 参考博客
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

三. go 常见数据结构实现原理之 silce 的相关文章

  • Ubuntu下的CUDA编程(二)

    Ubuntu下cuda编程的基本过程 一 运行程序 按照上一篇文章所述 安装好cuda软件以后 就可以使用 nvcc V 命令查看所用到的编译器版本 本人用版本信息来自 Cuda compilation tools release 3 2
  • python学习——如何求质数/素数

    质数判断 方法一 一个大于1的自然数 除了1和它本身外 不能被其他自然数 质数 整除 2 3 5 7等 换句话说就是该数除了1和它本身以外不再有其他的因数 也就是说 从2到n 1遍历 如果存在一个数是这个整数n的因数 那么它就不是质数 但是

随机推荐

  • docker保存镜像到本地,并加载本地镜像文件

    docker保存镜像到本地 并加载本地镜像文件 1 查看已有的镜像文件 docker images 显示效果如下所示 2 将镜像打包成本地文件 指令 docker save 镜像id gt 文件名 tar docker save 17282
  • COCO数据集的下载、介绍及如何使用(数据载入及数据增广,含代码)

    如何使用COCO数据集 COCO数据集可以说是语义分割等计算机视觉任务中应用较为广泛的一个数据集 具体可以应用到物体识别 语义分割及目标检测等方面 我是在做语义分割方面任务时用到了COCO数据集 但本文主要讲解的是数据载入方面 因此可以通用
  • springboot 微信小程序 对接微信支付功能(完整版)

    微信小程序对接微信支付功能 业务流程时序图 JAVA版 1 项目架构 2 pom xml配置文件 3 小程序账号参数配置类 4 JAVA 通用代码 4 1 工具类 4 1 1 IdGen id生成类 4 1 2 Render 响应结果类 4
  • Springboot

    0 学习目标 了解SpringBoot的作用 掌握java配置的方式 了解SpringBoot自动配置原理 掌握SpringBoot的基本使用 了解Thymeleaf的基本使用 1 了解SpringBoot 在这一部分 我们主要了解以下3个
  • CUDA编程第四章: 全局内存

    前言 本章内容 学习CUDA内存模型 CUDA内存管理 全局内存编程 探索全局内存访问模式 研究全局内存数据布局 统一内存编程 最大限度地提高全局内存吞吐量 在上一章中 你已经了解了线程是如何在GPU中执行的 以及如何通过操作线程束来优化核
  • 【Seq2Seq】压缩填充序列、掩蔽、推理和 BLEU

    大家好 我是Sonhhxg 柒 希望你看完之后 能对你有所帮助 不足请指正 共同学习交流 个人主页 Sonhhxg 柒的博客 CSDN博客 欢迎各位 点赞 收藏 留言 系列专栏 机器学习 ML 自然语言处理 NLP 深度学习 DL fore
  • Openstack云平台脚本部署之Aodh告警服务配置(十三)

    目录 一 简介 二 部署脚本 三 参考文档 四 源码 五 系列文章 一 简介 Openstack告警服务Aodh负责当收集的数据度量或事件超过所设定的阈值时 会出发报警 从Liberty 版本后从Ceilometer 中拆分出来 独立为单独
  • java把图片url地址转为图片文件并打包压缩下载

    序言 最近做项目时遇到一个需求就是把上传到oss上的图片批量压缩下载 众所周知 上传到oss的图片返回保存的是url地址 而url是无法直接下载成图片的 所有中间需要转一下 下面是我写的一个工具类 纯java操作 不依赖第三方jar有需要的
  • C语言猜数字小游戏

    在大家小时候 肯定玩过猜数字的游戏 那么用代码形式输出的猜数字游戏 大家玩过吗 今天就跟随博主一起来实现这一个小游戏吧 1 我们要先思考的问题是 怎么样才能让计算机随机产生数字呢 这里推荐大家使用一个C语言函数的网站 Cplusplus 里
  • 史上最简单的 SpringCloud 教程

    版权声明 本文为博主原创文章 遵循 CC 4 0 BY SA 版权协议 转载请附上原文出处链接和本声明 本文链接 https blog csdn net forezp article details 81040925 一 spring cl
  • vue 组件样式不生效问题,和如果更改组件样式

    我们现在的编程离不开组件的使用 例如 element ui avue 等 问题 组件的样式太过单一不满足开发的需求 还有的是组件有自己的样式更改但是找不到 解决 深层构造器 css 自带 gt gt gt gt gt gt name col
  • 如何在C#中从同步方法调用异步方法?

    我有一个public async void Foo 方法 我想从同步方法中调用它 到目前为止 我从MSDN文档中看到的所有内容都是通过异步方法调用异步方法 但是我的整个程序不是使用异步方法构建的 这有可能吗 这是从异步方法调用这些方法的一个
  • casperJs的安装

    自己买了vps就是爽 想装什么就装什么 就比如说casperjs 1 首先需要安装它的运行环境phantomjs 将这个git项目clone到自己的vps上 https github com ariya phantomjs 通过查看官方文档
  • Zookeeper实践(四)zookeeper的WEB客户端zkui使用

    前面几篇实践说明了zookeeper如何配置和部署 如何开发 因为大多是后台操作 对于维护和产品项目管理人员来说太抽象 下面介绍一下zookeeper的web客户端使用 一 环境准备 1 既然是客户端 必然得先有一个zookeeper服务
  • Class 09 - Data Frame和查看数据

    Class 09 Data Frame和查看数据 DataFrame tibbles head str colnames mutate 创建 Dataframe DataFrame 在我们开始做数据清洗或者检查数据是否存在偏差之前 我们需要
  • error: method does not override or implement a method from a supertype java:方法不会覆盖或实现超类型的方法

    错误 编译报错 error method does not override or implement a method from a supertype 即java 方法不会覆盖或实现超类型的方法 详细错误 解决方案 对超类进行继承即可
  • 交换两个变量的值的4种方法,你了解了吗?

    目录 一 引入第三变量 二 不引入第三变量 1 a a b b a b a a b 2 利用异或 3 巧妙运用优先级 总结 在我们的开发中 或者在我们平时的练习中 常常会遇到交换两个变量的值 那么如何交换两个变量的值呢 可能很多初学者都只知
  • 区块链四级知识考试

    区块链知识四级考试 考试时间30分钟 总分100分 请认真作答 出题人及监考老师 高志豪 请转载者注明 谢谢支持 一 单选题 每题5分 共40分 1 加密数字货币如果设置过短的确认时间会更容易导致什么出现 A 高效率 B 低效率 C 孤块
  • C/C++:C/C++在大数据时代的应用,以及C/C++程序员未来的发展路线

    目录 1 C C 在大数据时代的应用 1 1 C C 数据处理 1 2 C C 数据库 1 3 C C 图像处理和计算机视觉 1 3 1 导读 2 C C 程序员未来的发展路线 2 1 图导 1 C C 在大数据时代的应用 C C 在大数据
  • 三. go 常见数据结构实现原理之 silce

    目录 一 基础 几个小问题 1 题目一 2 题目二 3 题目三 4 数组和切片陷阱 二 Slice实现原理 切片的创建与底层结构 append 与切片的扩容 切片的值传递引用传递 切片再切片 特殊切片 切片的 Copy 总结 一 基础 为什