函数或方法中定义传参或返回值时,何时用指针

写在前面

台风”山竹“在周末作案,让当前在深圳的我无法出门溜达。实在无聊透顶,于是打开VSCode,随手敲了几行代码,发现了一些有趣的东西。

平日里大部分精力都放在Golang的宏观特性进行研究,比如goroutinechannel等,对老生常谈的类型探索的则不够深入。本文尝试用简单易懂的方式对Golang中的数据类型进行一定的探索,加深对这些数据类型的理解。

适应人群

入门√——初级——中级——高级;本文适应入门及以上。

源码展示及分析

一段又臭又长的源码

下面就是我用来试验的源码,基本上遵循了:1)声明某个类型的slice并初始化;2)println函数打印slice的地址及各个元素的地址。

package main

func main() {
	myBool := []bool{false, false, true}
	println("----myBool---")
	println(&myBool)
	println(&myBool[0])
	println(&myBool[1])
	println(&myBool[2])
	// ----myBool---
	// 0xc42004b9e8
	// 0xc42004b82d
	// 0xc42004b82e
	// 0xc42004b82f

	myInt := []int{1, 2, 3}
	println("----myInt---")
	println(&myInt)
	println(&myInt[0])
	println(&myInt[1])
	println(&myInt[2])
	// ----myInt---
	// 0xc42004b9a0
	// 0xc42004b858
	// 0xc42004b860
	// 0xc42004b868

	myComplex128 := []complex128{1 + 1i, 2 + 1i, 3 + 1i}
	println("----myComplex128---")
	println(&myComplex128)
	println(&myComplex128[0])
	println(&myComplex128[1])
	println(&myComplex128[2])
	// ----myComplex128---
	// 0xc42004b9b8
	// 0xc42004b870
	// 0xc42004b880
	// 0xc42004b890

	myString := []string{"123123123", "2342", "234356"}
	println("----myString---")
	println(&myString)
	println(&myString[0])
	println(&myString[1])
	println(&myString[2])
	// ----myString---
	// 0xc42004b940
	// 0xc42004bb40
	// 0xc42004bb50
	// 0xc42004bb60

	myChannel := []chan int{make(chan int), make(chan int), make(chan int)}
	println("----myChannel---")
	println(&myChannel)
	println(&myChannel[0])
	println(&myChannel[1])
	println(&myChannel[2])
	// ----myChannel---
	// 0xc42004b9d0
	// 0xc42004ba00
	// 0xc42004ba08
	// 0xc42004ba10

	type MyStruct struct {
		Name string
	}
	var myStruct = []MyStruct{MyStruct{Name: "1"}, MyStruct{Name: "2"}, MyStruct{Name: "4"}}
	println("----myStruct---")
	println(&myStruct)
	println(&myStruct[0])
	println(&myStruct[1])
	println(&myStruct[2])
	// ----myStruct---
	// 0xc42004b928
	// 0xc42004bb10
	// 0xc42004bb20
	// 0xc42004bb30

	type MyStruct2 struct {
		Name string
		Age  int
	}
	var myStruct2 = []MyStruct2{MyStruct2{Name: "1"}, MyStruct2{Name: "2"}, MyStruct2{Name: "4"}}
	println("----myStruct2---")
	println(&myStruct2)
	println(&myStruct2[0])
	println(&myStruct2[1])
	println(&myStruct2[2])
	// ----myStruct2---
	// 0xc42004b910
	// 0xc42004bc00
	// 0xc42004bc18
	// 0xc42004bc30

	mySlice := [][]int{[]int{1, 2, 3}, []int{2}, []int{3}}
	println("----mySlice---")
	println(&mySlice)
	println(&mySlice[0])
	println(&mySlice[1])
	println(&mySlice[2])
	// ----mySlice---
	// 0xc42004b970
	// 0xc42004bbb8
	// 0xc42004bbd0
	// 0xc42004bbe8

	mySlice2 := [][]string{[]string{"1", "2", "3"}, []string{"3"}, []string{"1", "3"}}
	println("----mySlice2---")
	println(&mySlice2)
	println(&mySlice2[0])
	println(&mySlice2[1])
	println(&mySlice2[2])
	// ----mySlice2---
	// 0xc42004b958
	// 0xc42004bb70
	// 0xc42004bb88
	// 0xc42004bba0

	myMap := []map[string]interface{}{
		make(map[string]interface{}),
		make(map[string]interface{}),
		make(map[string]interface{}),
	}
	println("----myMap---")
	println(&myMap)
	println(&myMap[0])
	println(&myMap[1])
	println(&myMap[2])
	// ----myMap---
	// 0xc42004b988
	// 0xc42004ba18
	// 0xc42004ba20
	// 0xc42004ba28
}

上面的代码表达了什么意思

上面的代码通过把各种数据类型的实例添加到slice中,并打印slice中每个元素的指针地址,可以推断出每个类型的元素在内存中的存储模型。

对于bool、int、float等数据类型,很容易理解所打印的内存地址处保存的就是他们所对应的值。不过对于string、channel、struct、slice、map这些类型的值,所打印的内存地址处保存的是什么呢?可以通过打印的内存地址规律得出来。

string类型

// ----myString---
// 0xc42004b940
// 0xc42004bb40
// 0xc42004bb50
// 0xc42004bb60

从上面指针递增的规律可以看出来,每个string元素占用了16个字节的数据。由于声明时每个元素的长度刻意设置了不同的值,因此可以推断出string类型的元素以”引用“(或指针)的形式索引底层的数据。

即,在编写Golang的函数与方法编时,无论是传入string的参数或返回string的结果,没有必要传入string的指针或返回string的指针,因为string的本质就是指针。

channel、slice、map

采用类似string的分析方法,可以发现channel、slice、map等类型的元素也是以”引用“(或指针)的形式索引底层的数据。也就是说,无论作为参数还是返回值,也没有必要显式指定返回它们的指针,因为他们的本质也是指针。

特立独行的struct

从试验结果看,struct类型的元素和string、channel、slice和map不同。


	type MyStruct struct {
		Name string
	}
// ----myStruct---
// 0xc42004b928
// 0xc42004bb10
// 0xc42004bb20
// 0xc42004bb30

////----VS----/////

	type MyStruct2 struct {
		Name string
		Age  int
	}
// ----myStruct2---
// 0xc42004b910
// 0xc42004bc00
// 0xc42004bc18
// 0xc42004bc30

因为MyStruct与MyStruct2的结构不同,他们的元素所占用的内存大小也表现出不同,标明struct类型的元素直接保存在当前地址。当struct的字段很多时,每次传递struct的元素成本就很大了(需要重新申请内存并初始化各变量)。

因此,在编写Golang的函数与方法编时,应该根据实际情况尽量传入struct的指针或返回struct的指针,一定程度上能节省内存占用,同时提升代码效率。

小结

本文是”山竹“吹来时,无聊的消遣产物。

主要尝试分析了bool、int、float、string、channel、slice、struct、map等类型的数据在内存中的存储模型。通过分析基本可以得出结论:在定义传参或返回值时,除了struct,其他类型的数据均可直接使用,没有必要使用指针类型的数据

参考