Go语言什么时候该使用指针?指针使用分析与讲解

2022年3月22日 346点热度 0人点赞 0条评论

最近在学习Golang,所以将学习过程记为笔记,以后翻阅的时候也方便,顺便也给大家做一点分享,希望能坚持下去。


关注本公众号,即可领取视频教程

2022年GO语言全套精讲系列-入门到精通96集


学习与交流:Go语言技术交流微信群


现在就开始你的Go语言学习之旅吧!人生苦短,let’s Go.



图片

图片

什么是指针
我们都知道,程序运行时的数据是存放在内存中的,每一个存储在内存中的数据都有一个编号,这个编号就是内存地址。我们可以根据这个内存地址来找到内存中存储的数据,而内存地址可以被赋值给一个指针。我们也可以简单的理解为指针就是内存地址。

指针的声明和定义
在Go语言中,获取一个指针,直接使用取地址符&就可以。
示例:

func main() {
  name := "Go语言圈"
  nameP := &name //取地址
  fmt.Println("name变量值为:", name)
  fmt.Println("name变量的内存地址为:", nameP)
}
//运行结果:
//name变量值为:Go语言圈
//name变量的内存地址为: 0xc00004e240

nameP 指针的类型是 string
Go语言中,
类型名表示一个对应的指针类型

图片

从上面表格可以看到:

  • 普通变量 name 的值是Go语言圈,存放在内存地址为 0xc00004e240 的内存中

  • 指针变量 namep 的值是普通变量的内存地址 0xc00004e240

  • 指针变量 nameP 的值存放在 内存地址为 0xc00004e360 的内存中

  • 普通变量存的是数据,指针变量存的是数据的地址

var 关键字声明
我们也可以使用 var 关键字声明

var nameP *string
nameP = &name

new 函数声明

nameP := new(string)
nameP = &name

可以传递类型给这个内置的 new 函数,它会返回对应的指针类型。

指针的操作
这里强调一下:

指针变量是一个变量,这个变量的值是指针(内存地址)!
指针变量是一个变量,这个变量的值是指针(内存地址)!
指针变量是一个变量,这个变量的值是指针(内存地址)!

获取指针指向的值:
只需要在指针变量钱加 * 号即可获得指针变量值所对应的数据:

nameV := *nameP
fmt.Println("nameP指针指向的值为:",nameV) //nameP指针指向的值为: Go语言圈

修改指针指向的值:

*nameP = "公众号:Go语言圈" //修改指针指向的值
fmt.Println("nameP指针指向的值为:",*nameP)
fmt.Println("name变量的值为:",name)
//运行结果:
//nameP指针指向的值为: 公众号:Go语言圈
//name变量的值为: 公众号:Go语言圈
  • 我们发现nameP 指针指向的值被改变了,变量 name 的值也被改变了

  • 因为变量 name 存储数据的内存就是指针 nameP 指向的内存,这块内存被 nameP 修改后,变量 name 的值也被修改了。

通过 var 关键字直接定义的指针变量是不能进行赋值操作的,因为它的值为 nil,也就是还没有指向的内存地址

//错误示例
var intP *int
*intP = 10  //错误,应该先给分配一块内存,内存地址作为变量 intP 的值,这个内存就可以存放 10 了。

//应该使用
var intP *int  //声明int类型的指针变量 intP
intP = new(int// 给指针分配一块内存
*intP = 66 
fmt.Println(":::",intP)  //::: 0xc0000ac088
fmt.Println(*intP) //66
//简短写法
var intP := new(int)
*intP=66

指针参数
当给一个函数使用指针作为参数的时候,就可以在函数中,通过形参改变实参的值:

func main() {
    name := "疯子"
    modify(&name)
    fmt.Println("name的值为:",name)
}
func modify(name *string)  {
    *name = "wucs"
}
//运行结果:
//name的值为: wucs

指针接收者

  • 如果接收者类型是 map、slice、channel 这类引用类型,不使用指针;

  • 如果需要修改接收者,那么需要使用指针;

  • 如果接收者是比较大的类型,可以考虑使用指针,因为内存拷贝廉价,所以效率高。

普通指针
和C语言一样, 允许用一个变量来存放其它变量的地址, 这种专门用于存储其它变量地址的变量, 我们称之为指针变量.

和C语言一样, Go语言中的指针无论是什么类型占用内存都一样(32位4个字节, 64位8个字节)

package main

import (
    "fmt"
    "unsafe"
)

func main() {

    var p1 *int;
    var p2 *float64;
    var p3 *bool;
    fmt.Println(unsafe.Sizeof(p1)) // 8
    fmt.Println(unsafe.Sizeof(p2)) // 8
    fmt.Println(unsafe.Sizeof(p3)) // 8
}

和C语言一样, 只要一个指针变量保存了另一个变量对应的内存地址, 那么就可以通过*来访问指针变量指向的存储空间

package main

import (
    "fmt"
)

func main() {

    // 1.定义一个普通变量
    var num int = 666
    // 2.定义一个指针变量
    var p *int = &num
    fmt.Printf("%p\n", &num) // 0xc042064080
    fmt.Printf("%p\n", p) // 0xc042064080
    fmt.Printf("%T\n", p) // *int
    // 3.通过指针变量操作指向的存储空间
    *p = 888
    // 4.指针变量操作的就是指向变量的存储空间
    fmt.Println(num) // 888
    fmt.Println(*p) // 888
}

指向数组指针
在Go语言中通过数组名无法直接获取数组的内存地址

package main
import "fmt"

func main() {
    var arr [3]int = [3]int{135}
    fmt.Printf("%p\n", arr) // 乱七八糟东西
    fmt.Printf("%p\n", &arr) // 0xc0420620a0
    fmt.Printf("%p\n", &arr[0]) // 0xc0420620a0
}

在Go语言中, 因为只有数据类型一模一样才能赋值, 所以只能通过&数组名赋值给指针变量, 才代表指针变量指向了这个数组

package main

import "fmt"

func main() {
    // 1.错误, 在Go语言中必须类型一模一样才能赋值
    // arr类型是[3]int, p1的类型是*[3]int
    var p1 *[3]int
    fmt.Printf("%T\n", arr)
    fmt.Printf("%T\n", p1)
    p1 = arr // 报错
    p1[1] = 6
    fmt.Println(arr[1])

    // 2.正确, &arr的类型是*[3]int, p2的类型也是*[3]int
    var p2 *[3]int
    fmt.Printf("%T\n", &arr)
    fmt.Printf("%T\n", p2)
    p2 = &arr
    p2[1] = 6
    fmt.Println(arr[1])

    // 3.错误, &arr[0]的类型是*int, p3的类型也是*[3]int
    var p3 *[3]int
    fmt.Printf("%T\n", &arr[0])
    fmt.Printf("%T\n", p3)
    p3 = &arr[0// 报错
    p3[1] = 6
    fmt.Println(arr[1])
}

注意点:
Go语言中的指针, 不支持C语言中的+1 -1和++ – 操作

package main

import "fmt"

func main() {


    var arr [3]int = [3]int{135}
    var p *[3]int
    p = &arr
    fmt.Printf("%p\n", &arr) // 0xc0420620a0
    fmt.Printf("%p\n", p) // 0xc0420620a0
    fmt.Println(&arr) // &[1 3 5]
    fmt.Println(p) // &[1 3 5]
    // 指针指向数组之后操作数组的几种方式
    // 1.直接通过数组名操作
    arr[1] = 6
    fmt.Println(arr[1])
    // 2.通过指针间接操作
    (*p)[1] = 7
    fmt.Println((*p)[1])
    fmt.Println(arr[1])
    // 3.通过指针间接操作
    p[1] = 8
    fmt.Println(p[1])
    fmt.Println(arr[1])

    // 注意点: Go语言中的指针, 不支持+1 -1和++ --操作
    *(p + 1) = 9 // 报错
    fmt.Println(*p++) // 报错
    fmt.Println(arr[1])
}

指向切片的指针
值得注意点的是切片的本质就是一个指针指向数组, 所以指向切片的指针是一个二级指针

package main

import "fmt"

func main() {
    // 1.定义一个切片
    var sce[]int = []int{135}
    // 2.打印切片的地址
    // 切片变量中保存的地址, 也就是指向的那个数组的地址 sce = 0xc0420620a0
    fmt.Printf("sce = %p\n",sce )
    fmt.Println(sce) // [1 3 5]
    // 切片变量自己的地址, &sce = 0xc04205e3e0
    fmt.Printf("&sce = %p\n",&sce )
    fmt.Println(&sce) // &[1 3 5]
    // 3.定义一个指向切片的指针
    var p *[]int
    // 因为必须类型一致才能赋值, 所以将切片变量自己的地址给了指针
    p = &sce
    // 4.打印指针保存的地址
    // 直接打印p打印出来的是保存的切片变量的地址 p = 0xc04205e3e0
    fmt.Printf("p = %p\n", p)
    fmt.Println(p) // &[1 3 5]
    // 打印*p打印出来的是切片变量保存的地址, 也就是数组的地址 *p = 0xc0420620a0
    fmt.Printf("*p = %p\n", *p)
    fmt.Println(*p) // [1 3 5]

    // 5.修改切片的值
    // 通过*p找到切片变量指向的存储空间(数组), 然后修改数组中保存的数据
    (*p)[1] = 666
    fmt.Println(sce[1])
}

指向字典指针
与普通指针并无差异

package main
import "fmt"
func main() {

    var dict map[string]string = map[string]string{"name":"lnj""age":"33"}
    var p *map[string]string = &dict
    (*p)["name"] = "zs"
    fmt.Println(dict)
}

指向结构体指针
Go语言中指向结构体的指针和C语言一样
结构体和指针
创建结构体指针变量有两种方式

package main
import "fmt"
type Student struct {
    name string
    age int
}
func main() {
  // 创建时利用取地址符号获取结构体变量地址
  var p1 = &Student{"lnj"33}
  fmt.Println(p1) // &{lnj 33}

  // 通过new内置函数传入数据类型创建
  // 内部会创建一个空的结构体变量, 然后返回这个结构体变量的地址
  var p2 = new(Student)
  fmt.Println(p2) // &{ 0}
}

利用结构体指针操作结构体属性

package main
import "fmt"
type Student struct {
    name string
    age int
}
func main() {
  var p = &Student{}
  // 方式一: 传统方式操作
  // 修改结构体中某个属性对应的值
  // 注意: 由于.运算符优先级比*高, 所以一定要加上()
  (*p).name = "lnj"
  // 获取结构体中某个属性对应的值
  fmt.Println((*p).name) // lnj

  // 方式二: 通过Go语法糖操作
  // Go语言作者为了程序员使用起来更加方便, 在操作指向结构体的指针时可以像操作接头体变量一样通过.来操作
  // 编译时底层会自动转发为(*p).age方式
  p.age = 33
  fmt.Println(p.age) // 33
}

什么情况下使用指针

  • 不要对 map、slice、channel 这类引用类型使用指针;

  • 如果需要修改方法接收者内部的数据或者状态时,需要使用指针;

  • 如果需要修改参数的值或者内部数据时,也需要使用指针类型的参数;

  • 如果是比较大的结构体,每次参数传递或者调用方法都要内存拷贝,内存占用多,这时候可以考虑使用指针;

  • 像 int、bool 这样的小数据类型没必要使用指针;

  • 如果需要并发安全,则尽可能地不要使用指针,使用指针一定要保证并发安全;

  • 指针最好不要嵌套,也就是不要使用一个指向指针的指针,虽然 Go 语言允许这么做,但是这会使你的代码变得异常复杂。


文章链接:https://www.jb51.net/article/233949.htm

更多相关Go语言的技术文章或视频教程,请关注本公众号获取并查看,感谢你的支持与信任!

76680Go语言什么时候该使用指针?指针使用分析与讲解

这个人很懒,什么都没留下

文章评论