Golang通脉之指针

指针的概念

指针是存储另一个变量的内存地址的变量。

变量是一种使用方便的占位符,用于引用计算机内存地址。

一个指针变量可以指向任何一个值的内存地址。

Golang通脉之指针

在上面的图中,变量b的值为156,存储在内存地址 0x1040a124变量a持有b的地址,现在a被认为指向b

区别于C/C++中的指针,Go语言中的指针不能进行偏移和运算,是安全指针。

要搞明白Go语言中的指针需要先知道3个概念:指针地址、指针类型和指针取值。

Go语言中的指针不能进行偏移和运算,因此Go语言中的指针操作非常简单,只需要记住两个符号: &(取地址)和 *(根据地址取值)。

声明指针

声明指针,*T是指针变量的类型,它指向T类型的值。

var var_name *var-type

var-type 为指针类型,var_name 为指针变量名,* 号用于指定变量是作为一个指针。

var ip *int        /* 指向整型*/
var fp *float32    /* 指向浮点型 */

示例代码:

func main() {
   var a int= 20   /* 声明实际变量 */
   var ip *int        /* 声明指针变量 */

   ip = &a  /* 指针变量的存储地址 */
   fmt.Printf("a 变量的地址是: %x\n", &a  )
   /* 指针变量的存储地址 */
   fmt.Printf("ip 变量的存储地址: %x\n", ip )
   /* 使用指针访问值 */
   fmt.Printf("*ip 变量的值: %d\n", *ip )
}

运行结果:

a 变量的地址是: 20818a220
ip 变量的存储地址: 20818a220
*ip 变量的值: 20

示例代码:

type name int8
type first struct {
    a int
    b bool
    name
}

func main() {
    a := new(first)
    a.a = 1
    a.name = 11
    fmt.Println(a.b, a.a, a.name)
}

运行结果:

false 1 11

未初始化的变量自动赋上初始值

type name int8
type first struct {
    a int
    b bool
    name
}

func main() {
    var a = first{1, false, 2}
    var b *first = &a
    fmt.Println(a.b, a.a, a.name, &a, b.a, &b, (*b).a)
}

运行结果:

false 1 2 &{1 false 2} 1 0xc042068018 1

获取指针地址在指针变量前加&的方式

指针地址和指针类型

每个变量在运行时都拥有一个地址,这个地址代表变量在内存中的位置。Go语言中使用 &字符放在变量前面对变量进行”取地址”操作。 Go语言中的值类型(int、float、bool、string、array、struct)都有对应的指针类型,如: *int*int64*string等。

取变量指针的语法如下:

ptr := &v    // v的类型为T       取v的地址   其实就是把v的地址引用给了ptr,此时v和ptr指向了同一块内存地址,任一变量值的修改都会影响另一个变量的值

其中:

  • v:代表被取地址的变量,类型为 T
  • ptr:用于接收地址的变量,ptr的类型就为 *T,称做T的指针类型。*代表指针。
func main() {
   var a int = 10
   fmt.Printf("变量a的地址: %x\n", &a  )
   b := &a
   fmt.Printf("变量b: %v\n", b  )
   fmt.Printf("变量b的地址: %v\n", &b  )
}

运行结果:

变量a的地址: 0x20818a220
变量b: 0x20818a220
变量b的地址: 0x263e94530

b := &a的图示:

Golang通脉之指针

指针取值

在对普通变量使用&操作符取地址后会获得这个变量的指针,然后可以对指针使用*操作,也就是指针取值,代码如下。

func main() {
    //指针取值
    a := 10
    b := &a // 取变量a的地址,将指针保存到b中
    fmt.Printf("type of b:%T\n", b)
    c := *b // 指针取值(根据指针去内存取值)
    fmt.Printf("type of c:%T\n", c)
    fmt.Printf("value of c:%v\n", c)
}

输出如下:

type of b:*int
type of c:int
value of c:10

总结: 取地址操作符 & 和取值操作符 * 是一对互补操作符, & 取出地址, * 根据地址取出地址指向的值。

变量、指针地址、指针变量、取地址、取值的相互关系和特性如下:

  • 对变量进行取地址(&)操作,可以获得这个变量的指针变量。
  • 指针变量的值是指针地址。
  • 对指针变量进行取值(*)操作,可以获得指针变量指向的原变量的值。

使用指针传递函数的参数

func change(val *int) {
    *val = 55
}
func main() {
    a := 58
    fmt.Println("value of a before function call is",a)
    b := &a
    change(b)
    fmt.Println("value of a after function call is", a)
}

运行结果

value of a before function call is 58
value of a after function call is 55

不要将一个指向数组的指针传递给函数。使用切片。

假设想对函数内的数组进行一些修改,并且对调用者可以看到函数内的数组所做的更改。一种方法是将一个指向数组的指针传递给函数。

func modify(arr *[3]int) {
    (*arr)[0] = 90
}

func main() {
    a := [3]int{89, 90, 91}
    modify(&a)
    fmt.Println(a)
}

运行结果

[90 90 91]

示例代码:

func modify(arr *[3]int) {
    arr[0] = 90
}

func main() {
    a := [3]int{89, 90, 91}
    modify(&a)
    fmt.Println(a)
}

运行结果

[90 90 91]

虽然将指针传递给一个数组作为函数的参数并对其进行修改,但这并不是实现这一目标的惯用方法。切片是首选:

func modify(sls []int) {
    sls[0] = 90
}

func main() {
    a := [3]int{89, 90, 91}
    modify(a[:])
    fmt.Println(a)
}

运行结果:

[90 90 91]

Go不支持指针算法。

func main() {
    b := [...]int{109, 110, 111} p := &b p++
}

nvalid operation: p++ (non-numeric type *[3]int)

指针数组

有一种情况,我们可能需要保存数组,这样就需要使用到指针。

const MAX int = 3

func main() {
   a := []int{10,100,200}
   var i int
   var ptr [MAX]*int;

   for  i = 0; i < MAX; i++ {
      ptr[i] = &a[i] /* 整数地址赋值给指针数组 */
   }

   for  i = 0; i < MAX; i++ {
      fmt.Printf("a[%d] = %d\n", i,*ptr[i] )
   }
}
结果
a[0] = 10
a[1] = 100
a[2] = 200

指针的指针

如果一个指针变量存放的又是另一个指针变量的地址,则称这个指针变量为指向指针的指针变量。

func main() {

   var a int
   var ptr *int
   var pptr **int

   a = 3000

   /* 指针 ptr 地址 */
   ptr = &a

   /* 指向指针 ptr 地址 */
   pptr = &ptr

   /* 获取 pptr 的值 */
   fmt.Printf("变量 a = %d\n", a )
   fmt.Printf("指针变量 *ptr = %d\n", *ptr )
   fmt.Printf("指向指针的指针变量 **pptr = %d\n", **pptr)
}
结果
变量 a = 3000
指针变量 *ptr = 3000
指向指针的指针变量 **pptr = 3000

指针作为函数参数

package main

import "fmt"

func main() {
   /* 定义局部变量 */
   var a int = 100
   var b int= 200

   fmt.Printf("交换前 a 的值 : %d\n", a )
   fmt.Printf("交换前 b 的值 : %d\n", b )

   /* 调用函数用于交换值
   * &a 指向 a 变量的地址
   * &b 指向 b 变量的地址
   */
   swap(&a, &b);

   fmt.Printf("交换后 a 的值 : %d\n", a )
   fmt.Printf("交换后 b 的值 : %d\n", b )
}

func swap(x *int, y *int) {
   var temp int
   temp = *x    /* 保存 x 地址的值 */
   *x = *y      /* 将 y 赋值给 x */
   *y = temp    /* 将 temp 赋值给 y */
}
结果
交换前 a 的值 : 100
交换前 b 的值 : 200
交换后 a 的值 : 200
交换后 b 的值 : 100

空指针

Go 空指针 当一个指针被定义后没有分配到任何变量时,它的值为 nil。 nil 指针也称为空指针。 nil在概念上和其它语言的null、None、nil、NULL一样,都指代零值或空值。 一个指针变量通常缩写为 ptr。

空指针判断:

if(ptr != nil)     /* ptr 不是空指针 */
if(ptr == nil)    /* ptr 是空指针 */
func main() {
   var sp *string
   *sp = "张三"
   fmt.Println(*sp)
}

运行这些代码,会看到如下错误信息:

panic: runtime error: invalid memory address or nil pointer dereference

这是因为指针类型的变量如果没有分配内存,就默认是零值 nil,它没有指向的内存,所以无法使用,强行使用就会得到以上 nil 指针错误。

指针使用

  1. 指针可以修改指向数据的值;
  2. 在变量赋值,参数传值的时候可以节省内存。

注意事项

  1. 不要对 map、slice、channel 这类引用类型使用指针;
  2. 如果需要修改方法接收者内部的数据或者状态时,需要使用指针;
  3. 如果需要修改参数的值或者内部数据时,也需要使用指针类型的参数;
  4. 如果是比较大的结构体,每次参数传递或者调用方法都要内存拷贝,内存占用多,这时候可以考虑使用指针;
  5. 像 int、bool 这样的小数据类型没必要使用指针;
  6. 如果需要并发安全,则尽可能地不要使用指针,使用指针一定要保证并发安全;
  7. 指针最好不要嵌套,也就是不要使用一个指向指针的指针,虽然 Go 语言允许这么做,但是这会使代码变得异常复杂。

new 和 make

我们知道对于值类型来说,即使只声明一个变量,没有对其初始化,该变量也会有分配好的内存。

func main() {
   var s string
   fmt.Printf("%p\n",&s)
}

结构体也是值类型,比如 var wg sync.WaitGroup 声明的变量 wg ,不进行初始化也可以直接使用,Go 语言自动分配了内存,所以可以直接使用,不会报 nil 异常。

于是可以得到结论:如果要对一个变量赋值,这个变量必须有对应的分配好的内存,这样才可以对这块内存操作,完成赋值的目的。

其实不止赋值操作,对于指针变量,如果没有分配内存, 取值操作一样会报 nil 异常,因为没有可以操作的内存

所以一个变量必须要经过声明、内存分配才能赋值,才可以在声明的时候进行初始化。 指针类型在声明的时候,Go 语言并没有自动分配内存,所以不能对其进行赋值操作,这和值类型不一样。map 和 chan 也一样,因为它们本质上也是指针类型。

要分配内存,就引出来了内置函数new()和make()。 Go语言中new和make是内建的两个函数,主要用来分配内存。

new

new是一个内置的函数,它的函数签名如下:

// The new built-in function allocates memory. The first argument is a type,
// not a value, and the value returned is a pointer to a newly
// allocated zero value of that type.

func new(Type) *Type

其中,

  • Type表示类型,new函数只接受一个参数,这个参数是一个类型
  • *Type表示类型指针,new函数返回一个指向该类型内存地址的指针。

它的作用就是 根据传入的类型申请一块内存,然后返回指向这块内存的指针,指针指向的数据就是该类型的零值:

func main() {
    a := new(int)
    b := new(bool)
    fmt.Printf("%T\n", a) // *int
    fmt.Printf("%T\n", b) // *bool
    fmt.Println(*a)       // 0
    fmt.Println(*b)       // false
}

通过 new 函数分配内存并返回指向该内存的指针后,就可以通过该指针对这块内存进行赋值、取值等操作。

make

make也是用于内存分配的,区别于 new,它只用于 slicemap以及 chan的内存创建, 而且它返回的类型就是这三个类型本身,而不是他们的指针类型,因为这三种类型就是引用类型,所以就没有必要返回他们的指针了。make函数的函数签名如下:

func make(t Type, size ...IntegerType) Type

在使用 make 函数创建 map 的时候,其实调用的是 makemap 函数:

// makemap implements Go map creation for make(map[k]v, hint).

func makemap(t *maptype, hint int, h *hmap) *hmap{
  //省略无关代码
}

makemap 函数返回的是 *hmap 类型,而 hmap 是一个结构体,它的定义如下所示:

// A header for a Go map.

type hmap struct {
   // Note: the format of the hmap is also encoded in cmd/compile/internal/gc/reflect.go.

   // Make sure this stays in sync with the compiler's definition.

   count     int // # live cells == size of map.  Must be first (used by len() builtin)
   flags     uint8
   B         uint8  // log_2 of # of buckets (can hold up to loadFactor * 2^B items)
   noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for details
   hash0     uint32 // hash seed
   buckets    unsafe.Pointer // array of 2^B Buckets. may be nil if count==0.

   oldbuckets unsafe.Pointer // previous bucket array of half the size, non-nil only when growing
   nevacuate  uintptr        // progress counter for evacuation (buckets less than this have been evacuated)
   extra *mapextra // optional fields
}

可以看到, map 关键字其实非常复杂,它包含 map 的大小 count、存储桶 buckets 等。要想使用这样的 hmap,不是简单地通过 new 函数返回一个 *hmap 就可以,还需要对其进行初始化,这就是 make 函数要做的事情:

m:=make(map[string]int,10)

其实 make 函数就是 map 类型的工厂函数,它可以根据传递它的 K-V 键值对类型,创建不同类型的 map,同时可以初始化 map 的大小。

make 函数不只是 map 类型的工厂函数,还是 chan、slice 的工厂函数。它同时可以用于 slice、chan 和 map 这三种类型的初始化。

make函数是无可替代的,在使用slice、map以及channel的时候,都需要使用make进行初始化,然后才可以对它们进行操作。

new与make的区别

  1. 二者都是用来做内存分配的。
  2. make只用于slice、map以及channel的初始化,返回的还是这三个引用类型本身;
  3. new用于类型的内存分配,并且内存对应的值为类型零值,返回的是指向对应类型零值的指针。

Original: https://www.cnblogs.com/drunkery/p/15427647.html
Author: 羌
Title: Golang通脉之指针

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/516548/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • Go基础知识梳理(三)

    Go基础知识梳理(三) 结构 type Person struct { name string sex int } func main() { //&#x63A8;&amp…

    Go语言 2023年5月25日
    062
  • Go xmas2020 学习笔记 01-14 上篇

    课程地址 go-class-slides/xmas-2020 at trunk · matt4biz/go-class-slides (github.com) 主讲老师 Matt …

    Go语言 2023年5月25日
    071
  • Golang(go语言)开发环境配置

    VSCode开发环境配置 (1)把vscode安装软件准备好 如果不清楚选64位还是32位可以在我的电脑->右击->点属性->即可查看 (2)双击安装文件就可以一…

    Go语言 2023年5月25日
    049
  • Go – 如何编写 ProtoBuf 插件 (三) ?

    上篇文章《Go – 如何编写 ProtoBuf 插件 (二) 》,分享了基于 &#x81EA;&#x5B9A;&#x4E49;&#x90…

    Go语言 2023年5月25日
    062
  • 服务注册与发现的原理和实现

    什么是服务注册发现? 对于搞微服务的同学来说,服务注册、服务发现的概念应该不会太陌生。 简单来说,当服务A需要依赖服务B时,我们就需要告诉服务A,哪里可以调用到服务B,这就是服务注…

    Go语言 2023年5月25日
    053
  • go语言四 channel和gorotime

    goroutine go中使用Goroutine来实现并发concurrently。 Goroutine是Go语言特有的名词。区别于进程Process,线程Thread,协程Cor…

    Go语言 2023年5月29日
    050
  • Go学习【02】:理解Gin,搭一个web demo

    Go Gin 框架 说Gin是一个框架,不如说Gin是一个类库或者工具库,其包含了可以组成框架的组件。这样会更好理解一点。 举个🌰 端口监听 用于监听请求,也就是服务 请求处理 请…

    Go语言 2023年5月25日
    069
  • go更新腾讯云DNSPod的解析记录

    纯粹练手用的,大家轻喷 获取SecretId,SecretKey 打开腾讯云,登录之后打开 https://console.cloud.tencent.com/cam/capi,然…

    Go语言 2023年5月25日
    060
  • 微服务-限流:一.golang实现令牌桶算法

    起初是因为要去拉取一些第三方的数据,而第三方的API接口都有限流措施。比如6000/分钟,500/分钟。想着拉取数据就用多个协程的方式。但是容易超频,所以想着写一个限流的东东。网上…

    Go语言 2023年5月25日
    045
  • 读 Go 源码,可以试试这个工具

    原文链接: 读 Go 源码,可以试试这个工具 编程发展至今,从面向过程到面向对象,再到现在的面向框架。写代码变成了一件越来越容易的事情。 学习基础语法,看看框架文档,几天时间搞出一…

    Go语言 2023年5月25日
    044
  • golang的defer踩坑汇总

    变量捕获 defer中的变量会被提前捕获,后续的修改不会影响到已捕获的值,举个例子: 结果defer语句中打印的值是修改前的值。: 最后输出值: 10 Defer运行值: 0 变量…

    Go语言 2023年5月25日
    047
  • HTTP 尝试获取 Client IP

    HTTP 中获取 Client IP 相关策略需求, 在当下网络环境中多数只能提供建议作用. 更多的是 通过其它唯一标识来挖掘更多潜在价值. 本文主要就一个内容, 如何最大可能尝试…

    Go语言 2023年5月25日
    053
  • Go能实现AOP吗?

    hello~大家好,我是小楼,今天分享的话题是 Go&#x662F;&#x5426;&#x80FD;&#x5B9E;&#x73B0;AOP?…

    Go语言 2023年5月25日
    075
  • Go微服务框架go-kratos学习05:分布式链路追踪 OpenTelemetry 使用

    一、分布式链路追踪发展简介 1.1 分布式链路追踪介绍 关于分布式链路追踪的介绍,可以查看我前面的文章 微服务架构学习与思考(09):分布式链路追踪系统-dapper论文学习(ht…

    Go语言 2023年5月25日
    045
  • Go 的 golang.org/x/ 系列包和标准库包有什么区别?

    在开发过程中可能会遇到这样的情况,有一些包是引入自不同地方的,比如: golang.org/x/net/html 和 net/html, golang.org/x/crypto 和…

    Go语言 2023年5月25日
    062
  • 基于LSM的Key-Value数据库实现WAL篇

    上篇文章简单的实现了基于LSM数据库的初步版本,在该版本中如数据写入到内存表后但还未持久化到SSTable排序字符串表,此时正好程序崩溃,内存表中暂未持久化的数据将会丢失。 引入W…

    Go语言 2023年5月25日
    055
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球