zoukankan      html  css  js  c++  java
  • Go中的Slice有何不同,传递类型,详解切片类型

    golang精选博文翻译仓库

    本文翻译自:https://dave.cheney.net/2018/07/12/slices-from-the-ground-up

    数组Arrays

    每次讨论到Go的切片问题,都会从这个变量是不是切片开始,换句话说,就是Go的序列类型,在Go中,数组有两种关联属性。

    1. 数组拥有固定的大小;[5]int即表明是一个有5个int的数组,又与[3]int相区分开

    2. 他们是值类型。思考下面这个例子

      package main
      
      import "fmt"
      
      func main() {
              var a [5]int
              b := a
              b[2] = 7
              fmt.Println(a, b) // prints [0 0 0 0 0] [0 0 7 0 0]
      }
      

      声明语句b:=a声明一个新变量b,一个[5]int的数据类型,并把a的内容拷贝到b中,更改b中的值并不会对a中内容造成影响,因为ab是独立的。

      *作者注:这并不是数组的特殊属性,在Go中,每次分配其实都是副本值传递 *

    切片Slices

    Go的切片类型与数组类型有两个不同的地方:

    1. 切片其实没有固定长度,一个切片的长度没有声明为其类型的一部分,而是被保留在切片结构本身中并且可以通过内置函数len来重置他。

      作者注:你也可以用len去重置数组变量的长度,但并没有用

      译者注:不明白用len()去重置切片长度是什么意思,欢迎补充,原文如下:

      Slices do not have a fixed length. A slice’s length is not declared as part of its type, rather it is held within the slice itself and is recoverable with the built-in function len.

    2. 用一个切片赋值给另一个切片并不会创建前一个切片的内容副本,因为切片类型没有直接拥有它的内容(序列),而是拥有一个指针,而这个指针指向切片下方的数组,数组内才是切片的内容。

      作者注:这有时也被成为后台数组(backing arrays)

    由于第二个特性,两个数组可以同时分享一个后台数组,思考以下例子:

    例1:对切片再切片

    package main
    
    import "fmt"
    
    func main() {
            var a = []int{1,2,3,4,5}
            b := a[2:]
            b[0] = 0
            fmt.Println(a, b) // prints [1 2 0 4 5] [0 4 5]
    }
    

    译者注:a也是个切片,而不是数组,只要[]内没有数字,就是切片,在本例中,a是数组{1,2,3,4,5}的一个切片

    在这个例子中,ab共同分享同一个后台数组,尽管b开始的偏移量和和长度都不同于a,所以底层数组的更改会用同时影响到ab

    例2:传切片变量给函数

    package main
    
    import "fmt"
    
    func negate(s []int) {
            for i := range s {
                    s[i] = -s[i]
            }
    }
    
    func main() {
            var a = []int{1, 2, 3, 4, 5}
            negate(a)
            fmt.Println(a) // prints [-1 -2 -3 -4 -5]
    }
    

    在例2中,a被传值给negate函数作为形式参数s,函数遍历s中的元素,将他们转为相反数,尽管negate函数没有返回任何值或者用任何方式去在main中访问a,但是a中的内容还是被negate所修改了。

    大多程序员程序员对Go中的切片与数组有一个直观的了解,应为这样的概念在其他语言中也有,例如

    Python 重写例1

    Python 2.7.10 (default, Feb  7 2017, 00:08:15) 
    [GCC 4.2.1 Compatible Apple LLVM 8.0.0 (clang-800.0.34)] on darwin
    Type "help", "copyright", "credits" or "license" for more information.
    >>> 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
    => 0
    irb(main):004:0> a
    => [1, 2, 0, 4, 5]
    

    Slice Header

    想要理解slice是如何做到本身是一个类,并且又是一个指针的话,就得理解slice的底层结构

    官方文档-1

    slicet Header看起来就像这样:

    package runtime
    
    type slice struct {
            ptr   unsafe.Pointer
            len   int
            cap   int
    }
    

    它不像mapchan类型,它们是引用类型,而切片是值类型,在赋值或者作为参数传递给函数的时候会复制。

    为了说明这些,程序员可以直观的将square()的形参理解为mainv的副本

    package main
    
    import "fmt"
    
    func square(v int) {
            v = v * v
    }
    
    func main() {
            v := 3
            square(v)
            fmt.Println(v) // prints 3, not 9
    }
    

    译者注:这是值传递,如果把形参改为v *int ,其他也做相应修改,则可输出9

    square的操作并不会对原本的v有任何影响,形参可以理解为所传值的单独拷贝。

    package main
    
    import "fmt"
    
    func double(s []int) {
            s = append(s, s...)
    }
    
    func main() {
            s := []int{1, 2, 3}
            double(s)
            fmt.Println(s, len(s)) // prints [1 2 3] 3
    }
    

    Go的slice变量稍微有点不一样的特性就是他是作为值传递的,不仅仅是一个指针,90%的时间当我们在Go中声明一个结构体的时候,你会传递一个结构体指针。Slice的值传递很不常见,我能想到的另一个值传递的结构体为time.time

    作者注:当结构体实现了某个接口的时候,那么传递指针的概率这接近100%

    正是这种异常的,将slice作为值传递,而不是指针传递,这引起了Go程序员的混乱思考,但是只要记住:当我们赋值,截取,传递或者返回一个切片的时候,你只是在创建一个Slice Header结构体,这个结构体有着三个字段:指向后台数组的指针,当前长度len,容量cap

    总结

    写一个用切片作为栈的例子

    package main
    
    import "fmt"
    
    func f(s []string, level int) {
            if level > 5 {
                   return
            }
            s = append(s, fmt.Sprint(level))
            f(s, level+1)
            fmt.Println("level:", level, "slice:", s)
    }
    
    func main() {
            f(nil, 0)
    }
    
    

    main开始,我们传递一个空值给f作为level 0,在f内部,我们添加当前的levels,一旦level大于5,f就会return ,打印s的副本。

    level: 5 slice: [0 1 2 3 4 5]
    level: 4 slice: [0 1 2 3 4]
    level: 3 slice: [0 1 2 3]
    level: 2 slice: [0 1 2]
    level: 1 slice: [0 1]
    level: 0 slice: [0]
    

    扩展阅读

    Go切片用处和内在原理

    数组,切片,和String,append的原理

  • 相关阅读:
    C#进程操作(使用cmd启动和停止.exe)
    NCHW 与NHWC 的区别
    对于openeuler x_86_64,openeuler aarch_64的安装以及yum换源
    读西瓜书笔记
    修改SpringBoot启动Logo
    SpringBoot集成Redis
    SpringBoot集成Dubbo
    那些Java架构师必知必会的技术
    CompletableFuture 入门学习
    使用 VirtualBox+Vagrant 快速搭建 Linux 虚拟机环境
  • 原文地址:https://www.cnblogs.com/Jun10ng/p/12695328.html
Copyright © 2011-2022 走看看