Go 学习笔记(13)— 指针定义、指针特点、空指针、指针数组、指向指针的指针、指针作为函数入参

1. 复合数据类型

Go 语言基本的复合数据类型有指针、数组、切片、字典、通道、结构和接口等。格式如下:

* pointerType	//	指针类型, 
[n]elementType	//	数组类型,
[]elementType	//	切片类型,
map [keyType]valueType	//	字典类型
chan valueType	//	通道类型

//	结构体类型
struct {
    fieldType fieldType
    fieldType fieldType
    ...
}

//	接口类型
interface {
    method1(inputParams) (returnParams)
    method2(inputParams) (returnParams)
    ...
}

2. 指针定义

每个变量在运行时都拥有一个地址,这个地址代表变量在内存中的位置。Go 语言中使用在变量名前面添加&操作符(前缀)来获取变量的内存地址(取地址操作),格式如下:

ptr := &v    // v 的类型为 T

其中 v 代表被取地址的变量,变量 v 的地址使用变量 ptr 进行接收, ptr 的类型为*T,称做 T 的指针类型,* 代表指针。

Go 语言的取地址符是 &放到一个变量前使用就会返回相应变量的内存地址。
获取变量在内存中地址:

package main

import "fmt"

func main() {
	var a int = 10

	fmt.Printf("变量的地址: %x\n", &a) // 变量的地址: c000018068
	fmt.Printf("%p", &a)          // %p 打印变量内存地址,指针的值是带有0x十六进制前缀的一组数据。
}

一个指针变量指向了一个值的内存地址。在使用指针前你需要声明指针。指针声明格式如下:

var varName *varType

varType 为指针类型, varName 为指针变量名, * 号用于指定变量是作为一个指针。以下是有效的指针声明:

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

指针类型是依托某一个类型而存在的,比如:一个整型为 int,那么它对应的整型指针就是 *int,也就是在 int 的前面加上一个星号。没有 int 类型,就不会有 *int 类型。而 int 也被称为 *int 指针类型的基类型。

指针类型的这个定义:如果我们拥有一个类型 T,那么以 T 作为基类型的指针类型为 *T

不过 Go 中也有一种指针类型是例外,它不需要基类型,它就是 unsafe.Pointerunsafe.Pointer 类似于 C 语言中的 void*,用于表示一个通用指针类型,也就是任何指针类型都可以显式转换为一个 unsafe.Pointer,而 unsafe.Pointer 也可以显式转换为任意指针类型,如下面代码所示:

var p *T
var p1 = unsafe.Pointer(p) // 任意指针类型显式转换为unsafe.Pointer
p = (*T)(p1)               // unsafe.Pointer也可以显式转换为任意指针类型

指针从图中我们看到,Go 为指针变量 p 分配的内存单元中存储的是整型变量 a 对应的内存单元的地址。也正是由于指针类型变量存储的是内存单元的地址,指针类型变量的大小与其基类型大小无关,而是和系统地址的表示长度有关。

它所指向的值的内存地址在 32 和 64 位机器上分别占用 48 个字节,占用字节的大小与所指向的值的大小无关。

package main

import "unsafe"

type foo struct {
    id   string
    age  int8
    addr string
}

func main() {
    var p1 *int
    var p2 *bool
    var p3 *byte
    var p4 *[20]int
    var p5 *foo
    var p6 unsafe.Pointer
    println(unsafe.Sizeof(p1)) // 8 
    println(unsafe.Sizeof(p2)) // 8
    println(unsafe.Sizeof(p3)) // 8
    println(unsafe.Sizeof(p4)) // 8
    println(unsafe.Sizeof(p5)) // 8
    println(unsafe.Sizeof(p6)) // 8
}

unsafe 包的 Sizeof 函数原型如下:

func Sizeof(x ArbitraryType) uintptr

Go 语言中 uintptr 类型的大小就代表了指针类型的大小。

当一个指针被定义后没有分配到任何变量时,它的默认值为 nil 。指针变量通常缩写为 ptr

变量、指针和地址三者的关系是,每个变量都拥有地址,指针的值就是地址。

多个指针变量可以指向同一个变量的内存单元的,这样通过其中一个指针变量对内存单元的修改,是可以通过另外一个指针变量的解引用反映出来的,比如下面例子:

var a int = 5
var p1 *int = &a // p1指向变量a所在内存单元
var p2 *int = &a // p2指向变量b所在内存单元
(*p1) += 5       // 通过p1修改变量a的值
println(*p2)     // 10 对变量a的修改可以通过另外一个指针变量p2的解引用反映出来

3. 指针使用

指针使用流程:

  • 定义指针变量
  • 为指针变量赋值
  • 访问指针变量中指向地址的值

在指针类型前面加上 * 号(前缀)来获取指针所指向的内容。

使用示例:

package main

import "fmt"

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

以下几点使用指针的建议:

  • 不要对 mapslicechannel 这类引用类型使用指针;

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

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

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

  • intbool 这样的小数据类型没必要使用指针;

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

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

4. 指针特点

  1. 在赋值语句中,*p 出现在 = 左边表示是指针声明,*p 出现在 = 右边表示取指针指向的值;

  2. 结构体指针访问结构体字段仍然使用 . 点操作符, Go 不支持 -> 操作符;

  3. Go 不支持指针的运算;

C 语言中,我们可以通过指针运算实现各种高级操作,比如简单的数组元素的遍历:

#include <stdio.h>
  
int main() {
    int a[] = {1, 2, 3, 4, 5};
    int *p = &a[0];
    for (int i = 0; i < sizeof(a)/sizeof(a[0]); i++) {
            printf("%d\n", *p);
            p = p + 1; 
    }
}

Go 在语法层面抛弃了指针运算这个特性。下面的代码将得到 Go 编译器的报错信息

package main

func main() {
    var arr = [5]int{1, 2, 3, 4, 5}
    var p *int = &arr[0]
    println(*p)
    p = p + 1  // 编译器报错:cannot convert 1 (untyped int constant) to *int
    println(*p)
}

如果我们非要做指针运算,Go 依然提供了 unsafe 的途径,比如下面通过 unsafe 遍历数组的代码:


package main

import "unsafe"

func main() {
    var arr = [5]int{11, 12, 13, 14, 15}
    var p *int = &arr[0]
    var i uintptr
    for i = 0; i < uintptr(len(arr)); i++ {
        p1 := (*int)(unsafe.Pointer(uintptr(unsafe.Pointer(p)) + i*unsafe.Sizeof(*p)))
        println(*p1)
    }           
}

上面这段代码就通过 unsafe.Pointeruintptr 的相互转换,间接实现了“指针运算”。但即便我们可以使用 unsafe 方法实现“指针运算”,Go 编译器也不会为开发人员提供任何帮助,开发人员需要自己告诉编译器要加减的绝对地址偏移值,而不是像前面 C 语言例子中那样,可以根据指针类型决定指针运算中数值 1 所代表的实际地址偏移值。

  1. 函数中允许返回变量的地址;
package main

import "fmt"

// Phone is struct type 注释开头必须是 结构体或者方法的名字,后面再加空格
type Phone struct { //exported type Phone should have comment or be unexportedgo-lint
	model string
	color string
	price int
}

func main() {
	var a *int = nil
	var b *int
	c := 10
	b = &c
	e := *b

	phone := Phone{"huawei", "blue", 1000}
	p := &phone

	// p++  invalid operation: p++ (non-numeric type *Phone)
	fmt.Printf("a is %v, a type is %T\n", a, a)
	fmt.Printf("b is %v, b type is %T\n", b, b)
	fmt.Printf("e is %v, e type is %T\n", e, e)
	fmt.Printf("p is %v, p type is %T\n", p, p)
	fmt.Printf("p.model is %v, p.price is %v\n", p.model, p.price)
	result := sum(5, 3)
	fmt.Printf("result is %v, result type is %T\n", *result, result)

}

func sum(a, b int) *int {
	ret := a + b
	return &ret
}

输出结果:

a is <nil>, a type is *int
b is 0xc000016068, b type is *int
e is 10, e type is int
p is &{huawei blue 1000}, p type is *main.Phone
p.model is huawei, p.price is 1000
result is 8, result type is *int
  1. 对变量进行取地址操作使用 & 操作符,可以获得这个变量的指针变量;

  2. 对指针变量进行取值操作使用 * 操作符,可以获得指针变量指向的原变量的值;

  3. 限制了显式指针类型转换;

在 C 语言中,我们可以像下面代码这样实现显式指针类型转换:

#include <stdio.h>
  
int main() {
    int a = 0x12345678;
    int *p = &a;
    char *p1 = (char*)p; // 将一个整型指针显式转换为一个char型指针
    printf("%x\n", *p1); 
}

但是在 Go 中,这样的显式指针转换会得到 Go 编译器的报错信息:

package main

import (
    "fmt"
    "unsafe"
)

func main() {
    var a int = 0x12345678
    var pa *int = &a
    var pb *byte = (*byte)(pa) // 编译器报错:cannot convert pa (variable of type *int) to type *byte
    fmt.Printf("%x\n", *pb)
}

如果我们“一意孤行”,非要进行这个转换,Go 也提供了 unsafe 的方式,因为我们需要使用到 unsafe.Pointer,如下面代码:

func main() {                                                                         
    var a int = 0x12345678                                                            
    var pa *int = &a                                                                  
    var pb *byte = (*byte)(unsafe.Pointer(pa)) // ok
    fmt.Printf("%x\n", *pb) // 78                                                          
} 

如果我们使用 unsafe 包中类型或函数,代码的安全性就要由开发人员自己保证,也就是开发人员得明确知道自己在做啥!

5. Go 空指针

当一个指针被定义后没有分配到任何变量时,它的值为 nilnil 指针也称为空指针。 nil 在概念上和其它语言的 nullNonenilNULL 一样,都指代零值或空值。

空指针值为 nil,即没有内存地址,是不能进行赋值操作的,比如下面的示例:

var intP *int
*intP =10

运行的时候会提示 invalid memory address or nil pointer dereference。这时候该怎么办呢?其实只需要通过 new 函数给它分配一块内存就可以了,如下所示:

var intP *int = new(int)
//更推荐简短声明法,这里是为了演示
//intP:=new(int)

一个指针变量通常缩写为 ptr 。空指针判断方法:

if(ptr != nil)     /* ptr 不是空指针 */
if(ptr == nil)    /* ptr 是空指针 */

使用示例:

package main

import "fmt"

func main() {
   var  ptr *int

   fmt.Printf("ptr 的值为 : %x\n", ptr  )
}

输出结果为:

ptr 的值为 : 0

6. Go 指针数组

ptr 为整型指针数组。因此每个元素都指向了一个值。以下实例的三个整数将存储在指针数组中:

package main

import "fmt"

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] )
   }
}

7. Go 指向指针的指针

如果一个指针变量存放的又是另一个指针变量的地址,则称这个指针变量为指向指针的指针变量。当定义一个指向指针的指针变量时,第一个指针存放第二个指针的地址,第二个指针存放变量的地址:

访问指向指针的指针变量值需要使用两个 * 号,如下所示:

package main

import "fmt"

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)
}

package main

func main() {
    var a int = 5
    var p1 *int = &a
    println(*p1) // 5
    var b int = 55
    var p2 *int = &b
    println(*p2) // 55

    var pp **int = &p1
    println(**pp) // 5
    pp = &p2      
    println(**pp) // 55
}  

二级指针我们看到,**int 类型的变量 pp 中存储的是 *int 型变量的地址,这和前面的 *int 型变量存储的是 int 型变量的地址的情况,其实是一种原理。**int 被称为二级指针,也就是指向指针的指针,那自然,我们可以理解 *int 就是一级指针了。

对一级指针解引用,我们得到的其实是指针指向的变量。而对二级指针 pp 解引用一次,我们得到将是 pp 指向的指针变量:

println((*pp) == p1) // true

那么对 pp 解引用二次,我们将得到啥呢?对 pp 解引用两次,其实就相当于对一级指针解引用一次,我们得到的是 pp 指向的指针变量所指向的整型变量:

println((**pp) == (*p1)) // true
println((**pp) == a)     // true

8. Go 指针作为函数参数

Go 语言允许向函数传递指针,只需要在函数定义的参数上设置为指针类型即可。

ackage 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 */
}

指针无论是在 Go 中,还是在其他支持指针的编程语言中,存在的意义就是为了是“可改变”。在 Go 中,我们使用 *T 类型的变量调用方法、以 *T 类型作为函数或方法的形式参数、返回 *T 类型的返回值等的目的,也都是因为指针可以改变其指向的内存单元的值。

当然,指针的好处,还包括它传递的开销是常数级的(在 x86-64 平台上仅仅是 8 字节的拷贝),可控可预测。无论指针指向的是一个字节大小的变量,还是一个拥有 10000 个元素的 [10000]int 型数组,传递指针的开销都是一样的。

  • 4
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
《Go语言学习笔记.pdf》是一本关于Go语言学习学习笔记,内容丰富且简洁明了。本书从基础知识开始,逐步介绍了Go语言的语法、特性和常用库函数等。在学习笔记中,作者通过实际的示例和练习帮助读者理解Go语言的概念和用法。 第一章介绍了Go语言的起源和发展,为读者提供了对Go语言背景的整体了解。第二章讲解了Go语言的基本语法,例如变量声明、循环和条件语句等。通过大量的代码示例,读者能够更好地理解Go语言的语法和结构。 接下来的章节重点介绍了Go语言的并发编程和高级特性。第三章详细介绍了Go语言中的goroutine和channel,这是Go语言并发编程的核心机制。作者通过生动的示例代码和实际应用案例,向读者展示了如何使用goroutine和channel实现并发编程。 第四章和第五章分别介绍了Go语言中的面向对象编程和函数式编程。通过深入讲解Go语言中的结构体、接口和函数,读者能够更好地应用这些特性进行代码设计和开发。 最后几章则介绍了Go语言中常用的库函数和工具。例如,第六章介绍了Go语言中用于网络编程的net包和http包。读者可以学习到如何使用这些库函数构建基于网络的应用程序。 总的来说,《Go语言学习笔记.pdf》是一本非常实用的Go语言学习资料。通过阅读这本书,读者能够系统地学习和理解Go语言的基本概念和高级特性,为之后的Go语言开发打下坚实的基础。无论是初学者还是有一定编程经验的开发者,都能从中获得丰富的知识和经验。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值