前言
- 最近从java里偷闲出来学会go,发现当初c语言中不懂的指针,到golang里还是不是很理解,特此用此篇持续记录自己对golang指针的理解。
一、优化基于指针对象方法的内存
谈这个还得从golang的方法传参开始讨论,直接上代码。
type Person struct {
Name string
}
type Son struct {
Sex string
}
/*
Golang中的方法作用在指定的数据类型上的(即:和指定的数据类型绑定),因此自定义类型,都可以有方法,
而不仅仅是struct,比如int ,float32等都可以有方法。
*/
func (p Person) testAddress(son Son) {
fmt.Printf("testAddress: son的指针地址为: %p\n",&son)
fmt.Printf("testAddress: p的指针地址为: %p\n",&p)
}
func main() {
p := Person{Name: "jack"}
son := Son{Sex: "man"}
p.testAddress(son)
fmt.Printf("main: p的指针地址为: %p\n",&p)
fmt.Printf("main: son的指针地址为: %p\n",&son)
}
上面的代码定义了两个结构体Person、Son,以及基于Person的一个结构体方法 testAddress,测试传入参数的地址。主协程main方法里打印传入方法前的地址,用来对比。
- 打印结果:
从打印结果来看,传入方法前的参数与传入方法中的参数地址是不同的,说明其实方法的传参,属于形参,如果是基本类型则复制一个相同的值,如果是引用类型(map、切片、指针…)则复制一个新的地址存储原有的对象属性的拷贝。
因此如果在方法中,对方法所属的结构体,或者传参的属性进行修改是不会影响到原有的参数,或者方法绑定的结构体。上代码:
func (p Person) testAddress(son Son) {
p.Name = "Rin"
son.Sex = "women"
fmt.Printf("testAddress: p的名字为为: %v\n",p.Name)
fmt.Printf("testAddress: son的性别为: %v\n",son.Sex)
fmt.Printf("testAddress: son的指针地址为: %p\n",&son)
fmt.Printf("testAddress: p的指针地址为: %p\n",&p)
}
func main() {
p := Person{Name: "jack"}
son := Son{Sex: "man"}
p.testAddress(son)
fmt.Printf("main: p的名字为为: %v\n",p.Name)
fmt.Printf("main: son的性别为: %v\n",son.Sex)
fmt.Printf("main: p的指针地址为: %p\n",&p)
fmt.Printf("main: son的指针地址为: %p\n",&son)
}
-
输出结果:
-
显而易见:在方法中修改后的属性,在方法后没有任何改变。 也证实了前面的结论。
-
那么如果传参换成指针呢? 继续上代码:
func (p *Person) testAddress(son *Son) {
p.Name = "Rin"
son.Sex = "women"
fmt.Printf("testAddress: p的名字为为: %v\n",p.Name)
fmt.Printf("testAddress: son的性别为: %v\n",son.Sex)
fmt.Printf("testAddress: son的指针地址为: %p\n",son)
fmt.Printf("testAddress: p的指针地址为: %p\n",p)
fmt.Printf("testAddress: son的指针存储地址为: %p\n",&son)
fmt.Printf("testAddress: p的指针存储地址为: %p\n",&p)
}
func main() {
p := Person{Name: "jack"}
son := Son{Sex: "man"}
p.testAddress(&son)
pPoint := &p
sonPoint := &son
fmt.Printf("main: p的名字为为: %v\n",p.Name)
fmt.Printf("main: son的性别为: %v\n",son.Sex)
fmt.Printf("main: son的指针地址为: %p\n",sonPoint)
fmt.Printf("main: p的指针地址为: %p\n",pPoint)
fmt.Printf("main: son的指针存储地址为: %p\n",&pPoint)
fmt.Printf("main: p的指针存储地址为: %p\n",&sonPoint)
}
- 输出结果:
可以看出传入对于指针的传入,打印出指针指向的地址其实是相同。也因此,修改元素的话相当于修改了对象本身。所以对应的对象元素发生了改变,但是其存储指针的地址还是与原来不同的。
因此会有种错误的修改方式:
func (p *Person) testAddress(son *Son) {
p = &Person{Name: "Rin"}
fmt.Printf("testAddress: p的指针地址为: %p\n",p)
}
func main() {
p := Person{Name: "jack"}
son := Son{Sex: "man"}
p.testAddress(&son)
pPoint := &p
fmt.Printf("main: p的名字为为: %v\n",p.Name)
fmt.Printf("main: p的指针地址为: %p\n",pPoint)
}
这种方式相当于:
跟原有的引用没关系了。显然输出的结果不会有变化:
- 那么看到这里就有读者问了,那么这样如果想直接改变参数值,只能通过对参数内的变量一一赋值才能修改成功呢?显然这对复杂对象是件繁琐的事所以有这样一种很简单方式也可以将对象赋给传入参数:
func (p *Person) testAddress(son *Son) {
*p = *&Person{Name: "Rin"}
fmt.Printf("testAddress: p的指针地址为: %p\n",p)
}
只要再将双方指针都指向内容就可以轻松再做到对整个内容的copy了~
- 也因此可以得到优化方法:将指针传入方法函数内部时,不用再去开辟一个新的内存去逐一拷贝元素,而是直接传入一个引向原有对象的地址指针。所以有的时候在传入的对象很复杂的时候,这是一个的内存优化手段。
二、充当类型返回
作为一个javaer学go时我总会将go与java进行对比,而java中的类就相当于go中的结构体,而在我之前的捣鼓中我发现了这样的一个事情:那就是如果一个结构体作为方法的出口,那么这个方法其实不支持返回空的,而java却是可以返回一个null。让我将上面的场景简单改造下,将person换成woman:
type Woman struct {
Name string
}
type Son struct {
Sex string
}
试想我们想编造一个这样的函数:传进一个字符串sex当sex为“man”的话返回一个son,如果不为“man”的话返回一个空:
func (w *Woman) makeSon(sex string) (Son){
if sex != "man" {
return Son{Sex: "sex is man"}
}else {
return nil
}
}
显然这段代码是会报错的:
而这在java是可以返回一个null的,因为null是java中所有引用类型的默认值,它可以被转化为任何类型,属于java中的一个关键字,这代表我们可以返回一个没有实例的对象,因为只是一个这个类型的引用。
- 那么我们要如何做呢?
- 方法一:利用error
让我们将上面的方法稍微改一改:
func (w *Woman) makeSon(sex string) (error,Son){
if sex != "man" {
return errors.New("women can't have a son"),Son{}
}else {
return nil,Son{Sex: "sex is man"}
}
}
main主协程,以及测试log方法:
func log(w Woman,sex string) {
err,son := w.makeSon(sex)
if err != nil {
err = fmt.Errorf("makeSon fun has error : %s", err)
fmt.Println(err.Error())
return
}else {
fmt.Println("has a son:",son)
}
}
func main() {
w := Woman{Name: "Jane"}
log(w,"man")
log(w,"woman")
}
成功实现了这样的测试效果:
- 方法二:利用指针进行返回
在开头中其实说到java中其实是通过null返回这个类的引用类型,那么一提到引用那就是应该联想到go中的指针了,它同样代表一个引用,指向目标的地址。
再来简单看看对nil的解释:
- nil是一个预先声明的标识符,代表指针(pointer)、通道(channel)、函数(func)、接口(interface)、 map、切片(slice)。
- 也可以这么理解:指针、通道、函数、接口、map、切片的零值就是nil,就像布尔类型的零值是false、整型的零值是0。
由此看来我们可以完全将传入参数改为指针传参,如果没有则返回一个空,接下来我们不动main函数变一变makeSon,与log方法:
func (w *Woman) makeSon(sex string) *Son {
if sex != "man" {
return &Son{Sex: "sex is man"}
}else {
return nil
}
}
func log(w Woman,sex string) {
son := w.makeSon(sex)
if son == nil {
err := fmt.Errorf("women can't have a son")
fmt.Println(err.Error())
return
}else {
fmt.Println("has a son:",son)
}
}
显然我们可以得到一个相同的结果:
(最后更新时间:4/18,最近其实都在接触go,但是后面就比较忙要去重回java了,此篇可能暂时不会更新了,但是还是会继续看一些关于go的书,如果觉得此篇讲的对同学有所帮助可以先收藏m住,后续会持续更新…。)