> 백엔드 개발 > Golang > 대규모 혼란 현장에서 []*T란 무엇일까요? *[]T란 무엇인가요? *[]*T뭔데요?

대규모 혼란 현장에서 []*T란 무엇일까요? *[]T란 무엇인가요? *[]*T뭔데요?

醉折花枝作酒筹
풀어 주다: 2021-08-02 09:21:21
앞으로
3224명이 탐색했습니다.

최근에 "[]*T", "*[]T", "*[]*T" 등 아주 이상한 코드를 봤습니다. 얼핏 보면 다 똑같아 보이지만 자세히 보면, 우리는 그들이 장소가 다르다는 것을 알았습니다. 오늘은 golong의 "[]*T", "*[]T", "*[]*T"를 소개하여 차이점을 함께 살펴보겠습니다.

Go 언어 초보로서 한번 살펴보세요. "이상한" 코드를 보면 궁금할 것입니다. 예를 들어 최근에 본 의사 코드는 다음과 같습니다.

func FindA() ([]*T,error) {
}

func FindB() ([]T,error) {
}

func SaveA(data *[]T) error {
}

func SaveB(data *[]*T) error {
}
로그인 후 복사

Go를 막 시작한 대부분의 초보자는 이러한 코드를 보면 혼란스러워할 것입니다. 가장 혼란스러운 것은 다음과 같습니다.

[]*T
*[]T
*[]*T
로그인 후 복사

이러한 슬라이스 선언의 경우 후자의 두 가지 작성 방법은 살펴보지 않겠습니다. []*T를 보면 여전히 이해하기 쉽습니다. alone: ​​​​
모든 T의 메모리 주소가 이 슬라이스에 저장됩니다. 이는 T 자체를 저장하는 것보다 더 낫습니다. 동시에 []*T는 메서드 내에서 T의 값을 수정할 수 있지만 [] T는 할 수 없습니다.

func TestSaveSlice(t *testing.T) {
    a := []T{{Name: "1"}, {Name: "2"}}
    for _, t2 := range a {
        fmt.Println(t2)
    }
    _ = SaveB(a)
    for _, t2 := range a {
        fmt.Println(t2)
    }

}
func SaveB(data []T) error {
    t := data[0]
    t.Name = "1233"
    return nil
}

type T struct {
    Name string
}
로그인 후 복사

예를 들어, 위의 예는

{1}
{2}
{1}
{2}
로그인 후 복사

를 인쇄합니다. 메소드를

func SaveB(data []*T) error {
    t := data[0]
    t.Name = "1233"
    return nil
}
로그인 후 복사

로 변경해야만 T의 값을 수정할 수 있습니다.

&{1}
&{2}
&{1233}
&{2}
로그인 후 복사

Example

[]*T와 *[]의 차이점에 집중하겠습니다. T, 여기에 두 가지 추가 함수를 작성했습니다.

func TestAppendA(t *testing.T) {
    x:=[]int{1,2,3}
    appendA(x)
    fmt.Printf("main %v\n", x)
}
func appendA(x []int) {
    x[0]= 100
    fmt.Printf("appendA %v\n", x)
}
로그인 후 복사
로그인 후 복사

먼저 첫 번째 것을 살펴보겠습니다. 출력은 다음과 같습니다.

appendA [1000 2 3]
main [1000 2 3]
로그인 후 복사

이는 함수 전송 프로세스 중에 함수 내부의 수정이 외부에 영향을 미칠 수 있음을 의미합니다.


또 다른 예를 살펴보겠습니다.

func appendB(x []int) {
    x = append(x, 4)
    fmt.Printf("appendA %v\n", x)
}
로그인 후 복사

최종 결과는 다음과 같습니다.

appendA [1 2 3 4]
main [1 2 3]
로그인 후 복사

는 외부에 영향을 주지 않습니다.

그리고 다시 조정하면 뭔가 다른 것을 발견하게 됩니다:

func TestAppendC(t *testing.T) {
    x:=[]int{1,2,3}
    appendC(&x)
    fmt.Printf("main %v\n", x)
}
func appendC(x *[]int) {
    *x = append(*x, 4)
    fmt.Printf("appendA %v\n", x)
}
로그인 후 복사

최종 결과:

appendA &[1 2 3 4]
main [1 2 3 4]
로그인 후 복사

슬라이스의 포인터를 전달하면 추가 기능을 사용하여 데이터를 추가하면 외부에 영향을 미친다는 것을 알 수 있습니다.

슬라이스 원리

위의 세 가지 상황을 분석하기 전에 먼저 슬라이스의 데이터 구조를 이해해 봅시다.

소스 코드를 직접 보면 슬라이스가 실제로는 구조체이지만 직접 접근할 수 없다는 것을 알 수 있습니다.

소스 코드 주소 Runtime/slice.go

세 가지 중요한 속성이 있습니다:

속성 의미
array데이터를 저장하는 기본 배열은 포인터입니다.
len슬라이스 길이
cap슬라이스 용량 cap>=len

提到切片就不得不想到数组,可以这么理解:

切片是对数组的抽象,而数组则是切片的底层实现。

其实通过切片这个名字也不难看出,它就是从数组中切了一部分;相对于数组的固定大小,切片可以根据实际使用情况进行扩容。

所以切片也可以通过对数组"切一刀"获得:

x1:=[6]int{0,1,2,3,4,5}
x2 := x[1:4]
fmt.Println(len(x2), cap(x2))
로그인 후 복사

其中 x1 的长度与容量都是6。

x2 的长度与容量则为3和5。

  • x2 的长度很容易理解。

  • 容量等于5可以理解为,当前这个切片最多可以使用的长度。

因为切片 x2 是对数组 x1 的引用,所以底层数组排除掉左边一个没有被引用的位置则是该切片最大的容量,也就是5。

同一个底层数组

以刚才的代码为例:

func TestAppendA(t *testing.T) {
    x:=[]int{1,2,3}
    appendA(x)
    fmt.Printf("main %v\n", x)
}
func appendA(x []int) {
    x[0]= 100
    fmt.Printf("appendA %v\n", x)
}
로그인 후 복사
로그인 후 복사

在函数传递过程中,main 中的 x 与 appendA 函数中的 x 切片所引用的是同个数组。

所以在函数中对 x[0]=100,main函数中也能获取到。

本质上修改的就是同一块内存数据。

值传递带来的误会

在上述例子中,在 appendB 中调用 append 函数追加数据后会发现 main 函数中并没有受到影响,这里我稍微调整了一下示例代码:

func TestAppendB(t *testing.T) {
    //x:=[]int{1,2,3}
    x := make([]int, 3,5)
    x[0] = 1
    x[1] = 2
    x[2] = 3
    appendB(x)
    fmt.Printf("main %v len=%v,cap=%v\n", x,len(x),cap(x))
}
func appendB(x []int) {
    x = append(x, 444)
    fmt.Printf("appendB %v len=%v,cap=%v\n", x,len(x),cap(x))
}
로그인 후 복사
主要是修改了切片初始化方式,使得容量大于了长度,具体原因后续会说明。

输出结果如下:

appendB [1 2 3 444] len=4,cap=5
main [1 2 3] len=3,cap=5
로그인 후 복사

main 函数中的数据看样子确实没有受到影响;但细心的朋友应该会注意到 appendB 函数中的 x 在 append() 之后长度 +1 变为了4。

而在 main 函数中长度又变回了3.

这个细节区别就是为什么 append() "看似" 没有生效的原因;至于为什么要说“看似”,再次调整了代码:

func TestAppendB(t *testing.T) {
    //x:=[]int{1,2,3}
    x := make([]int, 3,5)
    x[0] = 1
    x[1] = 2
    x[2] = 3
    appendB(x)
    fmt.Printf("main %v len=%v,cap=%v\n", x,len(x),cap(x))

    y:=x[0:cap(x)]
    fmt.Printf("y %v len=%v,cap=%v\n", y,len(y),cap(y))
}
로그인 후 복사

在刚才的基础之上,以 append 之后的 x 为基础再做了一个切片;该切片的范围为 x 所引用数组的全部数据。

再来看看执行结果如何:

appendB [1 2 3 444] len=4,cap=5
main [1 2 3] len=3,cap=5
y [1 2 3 444 0] len=5,cap=5
로그인 후 복사

会神奇的发现 y 将所有数据都打印出来,在 appendB 函数中追加的数据其实已经写入了数组中,但为什么 x 本身没有获取到呢?

看图就很容易理解了:

  • 在appendB中确实是对原始数组追加了数据,同时长度也增加了。

  • 但由于是值传递,所以 slice 这个结构体即便是修改了长度为4,也只是对复制的那个对象修改了长度,main 中的长度依然为3.

  • 由于底层数组是同一个,所以基于这个底层数组重新生成了一个完整长度的切片便能看到追加的数据了。

所以这里本质的原因是因为 slice 是一个结构体,传递的是值,不管方法里如何修改长度也不会影响到原有的数据(这里指的是长度和容量这两个属性)。

切片扩容

还有一个需要注意:

刚才特意提到这里的例子稍有改变,主要是将切片的容量设置超过了数组的长度;

如果不做这个特殊设置会怎么样呢?

func TestAppendB(t *testing.T) {
    x:=[]int{1,2,3}
    //x := make([]int, 3,5)
    x[0] = 1
    x[1] = 2
    x[2] = 3
    appendB(x)
    fmt.Printf("main %v len=%v,cap=%v\n", x,len(x),cap(x))

    y:=x[0:cap(x)]
    fmt.Printf("y %v len=%v,cap=%v\n", y,len(y),cap(y))
}
func appendB(x []int) {
    x = append(x, 444)
    fmt.Printf("appendB %v len=%v,cap=%v\n", x,len(x),cap(x))
}
로그인 후 복사

输出结果:

appendB [1 2 3 444] len=4,cap=6
main [1 2 3] len=3,cap=3
y [1 2 3] len=3,cap=3
로그인 후 복사

这时会发现 main 函数中的 y 切片数据也没有发生变化,这是为什么呢?

这是因为初始化 x 切片时长度和容量都为3,当在 appendB 函数中追加数据时,会发现没有位置了。

这时便会进行扩容:

  • 将老数据复制一份到新的数组中。

  • 追加数据。

  • 将新的数据内存地址返回给 appendB 中的 x .

同样的由于是值传递,所以 appendB 中的切片换了底层数组对 main 函数中的切片没有任何影响,也就导致最终 main 函数的数据没有任何变化了。

传递切片指针

有没有什么办法即便是在扩容时也能对外部产生影响呢?

func TestAppendC(t *testing.T) {
    x:=[]int{1,2,3}
    appendC(&x)
    fmt.Printf("main %v len=%v,cap=%v\n", x,len(x),cap(x))
}
func appendC(x *[]int) {
    *x = append(*x, 4)
    fmt.Printf("appendC %v\n", x)
}
로그인 후 복사

输出结果为:

appendC &[1 2 3 4]
main [1 2 3 4] len=4,cap=6
로그인 후 복사

这时外部的切片就能受到影响了,其实原因也很简单;

刚才也说了,因为 slice 本身是一个结构体,所以当我们传递指针时,就和平时自定义的 struct 在函数内部通过指针修改数据原理相同。

最终在 appendC 中的 x 的指针指向了扩容后的结构体,因为传递的是 main 函数中 x 的指针,所以同样的 main 函数中的 x 也指向了该结构体。

总结

所以总结一下:

  • 切片是对数组的抽象,同时切片本身也是一个结构体。

  • 参数传递时函数内部与外部引用的是同一个数组,所以对切片的修改会影响到函数外部。

  • 如果发生扩容,情况会发生变化,同时扩容会导致数据拷贝;所以要尽量预估切片大小,避免数据拷贝。

  • 슬라이스나 배열을 다시 생성할 때 동일한 기본 배열을 공유하므로 데이터가 서로 영향을 미치게 된다는 점에 유의해야 합니다.

  • 슬라이스도 포인터를 전달할 수 있지만 시나리오가 적고 불필요한 오해를 불러일으킬 수 있으므로 길이와 용량이 메모리를 많이 차지하지 않으므로 값별로 전달하는 것이 좋습니다.

슬라이싱을 사용해 본 적이 있다면 Java의 ArrayList와 매우 유사하다는 것을 알게 될 것입니다. 또한 배열 구현을 기반으로 하며 데이터를 확장하고 복사하는 것도 언어일 뿐입니다. 상위 계층의 경우 일부 일반적인 기본 구현은 모든 사람과 유사합니다.

이때, 제목의 []*T *[]T *[]*T를 보면 서로 관련은 없지만 매우 비슷해 허세 부리기 쉬운 내용임을 알 수 있습니다.

필요하시면 golong튜토리얼

을 시청하세요

위 내용은 대규모 혼란 현장에서 []*T란 무엇일까요? *[]T란 무엇인가요? *[]*T뭔데요?의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

관련 라벨:
원천:segmentfault.com
본 웹사이트의 성명
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.
인기 튜토리얼
더>
최신 다운로드
더>
웹 효과
웹사이트 소스 코드
웹사이트 자료
프론트엔드 템플릿