当前位置: 首页 > 科技观察

从零开始学习Go语言中的切片

时间:2023-03-14 18:41:12 科技观察

这篇文章的灵感来自于我和一位同事讨论使用切片作为堆栈的聊天。后面的话题讲到了切片在Go语言中是如何工作的。我认为这些信息对其他人也有用,所以我记下了它。数组任何关于Go中切片的讨论都从另一种数据结构开始,即数组。Go的数组有两个特点:数组的长度是固定的;[5]int是5个整数的数组,与[3]int不同。数组是值类型。看这个例子:packagemainimport"fmt"funcmain(){vara[5]intb:=ab[2]=7fmt.Println(a,b)//打印[00000][00700]}语句b:=a定义了一个新的[5]int类型的变量b,然后将a的内容复制给b。更改b对a中的内容没有影响,因为a和b是独立的值。1切片Go语言中的切片和数组有两个主要区别:切片没有固定长度。切片的长度不是其类型定义的一部分,而是由切片本身在内部维护。我们可以使用内置的len函数知道它的长度。2将一个切片赋值给另一个切片时,不会复制切片内容。这是因为切片不直接保存其内部数据,而是保存指向底层array3的指针。数据保存在底层数组中。基于第二个特性,两个切片可以共享一个公共底层数组。看看下面的例子:切片包mainimport"fmt"funcmain(){vara=[]int{1,2,3,4,5}b:=a[2:]b[0]=0fmt.Println(a,b)//打印[12045][045]}在这个例子中,a和b共享同一个底层数组——尽管b在数组中的起始偏移量不同,长度二者亦不同。通过b修改底层数组的值也会导致a中的值改变。将切片传递到函数包mainimport"fmt"funcnegate(s[]int){fori:=ranges{s[i]=-s[i]}}funcmain(){vara=[]int{1,2,3,4,5}negate(a)fmt.Println(a)//打印[-1-2-3-4-5]}在本例中,a作为实际参数传递给形参s进入negate函数,该函数遍历s中的元素并改变其符号。nagate虽然没有返回值,也没有访问main函数中的a。但是当它被传递到negate函数中时,a中的值发生了变化。大多数程序员都可以直观地理解Go中底层切片数组的工作原理,因为它的工作方式与其他语言中的类似数组类似。例如,这是用Python重写的本节的第一个示例:Python2.7.10(默认,2017年2月7日,00:08:15)[GCC4.2.1兼容AppleLLVM8.0.0(clang-800.0.34)]在darwin上键入“help”、“copyright”、“credits”或“license”以获取更多信息。>>>a=[1,2,3,4,5]>>>b=a>>>b[2]=0>>>a[1,2,0,4,5]和重写版本使用Ruby:irb(main):001:0>a=[1,2,3,4,5]=>[1,2,3,4,5]irb(main):002:0>b=a=>[1,2,3,4,5]irb(main):003:0>b[2]=0=>0irb(main):004:0>a=>[1,2,0,4,5]在大多数将数组视为对象或引用类型的语言中也是如此。4sliceheaders同时具有值??和指针属性的切片的神奇之处在于理解切片实际上是一个struct类型的结构。通常在reflect反射包中对应部分之后的这个结构称为sliceheader。切片头的定义大致如下:inas参数函数将被复制过来。程序员可以理解square的形参v和main中v的声明是相互独立的。请看下面的例子:packagemainimport"fmt"funcsquare(vint){v=v*v}funcmain(){v:=3square(v)fmt.Println(v)//打印3,而不是9因此,square对其形参v的操作不会影响main中的v。以下示例中的s也是main中声明的切片s的独立副本,而不是指向main的s的指针。packagemainimport"fmt"funcdouble(s[]int){s=append(s,s...)}funcmain(){s:=[]int{1,2,3}double(s)fmt.Println(s,len(s))//prints[123]3}Go的不寻常之处在于切片作为值而不是指针传递。当你在Go中定义一个结构时,90%的时间你传递一个指向struct5的指针。切片的传递方式真的很不寻常,我能想到的唯一一个例子是time.Time。按值而不是指针传递切片的特殊行为使许多试图理解切片如何工作的Go程序员感到困惑。您只需要记住,当您分配、获取、传递或返回切片时,您正在复制切片头结构的三个字段:指向底层数组的指针、长度和容量。小结下面以引出本主题的切片为例,总结一下本文的内容:packagemainimport"fmt"funcf(s[]string,levelint){iflevel>5{return}s=append(s,fmt.Sprint(level))f(s,level+1)fmt.Println("level:",level,"slice:",s)}funcmain(){f(nil,0)}inmain函数在一开始我们将nil切片作为级别0传递给函数f。在函数f中,我们将当前级别添加到切片的末尾,然后递增级别的值并递归。一旦level大于5,函数返回,打印出当前level和它们被复制到的s的内容。级别:5切片:[012345]级别:4切片:[01234]级别:3切片:[0123]级别:2切片:[012]级别:1切片:[01]level:0slice:[0]你可以注意到,在每个级别上,s的值不受对f的其他调用的影响,尽管作为计算更高级别时追加的副产品,调用堆栈四个f函数创建四个底层数组6,但不影响当前各自的切片。进一步阅读如果你想了解更多关于切片在Go中如何工作的信息,我建议阅读Go博客上的这些文章:Go切片:用法和内部结构(blog.golang.org)数组、切片(和字符串):TheMechanicsof'append'(blog.golang.org)相关文章:如果地图不是引用变量,那它是什么?什么是零值,它为什么有用?空结构应该在T或*T上声明方法这不是数组的独特功能。在Go语言中,所有的分配都是复制的。您还可以对数组使用len函数,但结果是已知的。它有时也称为支持数组,更松散地称为支持切片。在Go语言中,我们倾向于说值类型和指针类型,因为C++中的术语引用类型具有误导性。但是这里我觉得调用数组作为引用类型是没有问题的。如果您的结构上定义了方法或用于满足接口,那么您传入结构指针的速率可能会飙升至接近100%。证明留作练习。