借问变量何处存,牧童笑称用指针,Go lang1.18入门精炼教程,由白丁入鸿儒,go lang类型指针(Pointer)的使用EP05

简介: 指针是指什么?指针是存储另一个变量的内存地址的变量。变量是一种使用方便的占位符,用于引用计算机内存地址,一个指针变量可以指向任何一个值的内存地址它指向那个值的内存地址。类比的话,指针就是书籍中的目录,本身也占据书页,既可以通过目录获得章节内容,又可以指向具体章节的页数(地址)。

指针是指什么?指针是存储另一个变量的内存地址的变量。变量是一种使用方便的占位符,用于引用计算机内存地址,一个指针变量可以指向任何一个值的内存地址它指向那个值的内存地址。类比的话,指针就是书籍中的目录,本身也占据书页,既可以通过目录获得章节内容,又可以指向具体章节的页数(地址)。

指针声明

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

var var_name *var-type

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

例如:

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

之前我们曾经使用&关键字来获取变量在内存中的地址,事实上,该对象就是指针:

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

由此可见,指针变量的类型为 *Type,该指针指向一个 Type 类型的变量。指针变量最大的特点就是存储的某个实际变量的内存地址,通过记录某个变量的地址,从而间接的操作该变量。

& 关键字可以从一个变量中取到其内存地址。

* 关键字如果在赋值操作值的左边,指该指针指向的变量;* 关键字如果在赋值操作符的右边,指从一个指针变量中取得变量值,又称指针的解引用。

指针也分不同的类型:

package main  
  
import "fmt"  
  
func main() {  
    mystr := "字符串"  
    myint := 1  
    mybool := false  
    myfloat := 3.2  
    fmt.Printf("type of &mystr is :%T\n", &mystr)  
    fmt.Printf("type of &myint is :%T\n", &myint)  
    fmt.Printf("type of &mybool is :%T\n", &mybool)  
    fmt.Printf("type of &myfloat is :%T\n", &myfloat)  
}

程序返回:

type of &mystr is :*string  
type of &myint is :*int  
type of &mybool is :*bool  
type of &myfloat is :*float64

但其实,指针的类型,其实就是它所指的变量的基本类型,二者类型是一致的。

空指针

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

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

具体例子:

package main  
  
import "fmt"  
  
func main() {  
    x := "字符串"  
    var ptr *string  
    fmt.Println("ptr is ", ptr)  
    ptr = &x  
    fmt.Println("ptr is ", ptr)  
}

程序返回:

ptr is  <nil>  
ptr is  0xc00003c250

但也需要注意的是,指针的空值和变量的空值一样,都需要用恒等或者非恒等来判断,而并非像Python一样使用is关键字去比对内存的具体地址。

指针操作

获取一个指针意味着访问指针指向的变量的值。语法是:*a

package main  
  
import (  
    "fmt"  
)  
  
func main() {  
    b := 255  
    a := &b  
    fmt.Println("address of b is", a)  
    fmt.Println("value of b is", *a)  
}

程序返回:

address of b is 0xc000014088  
value of b is 255

一般情况下,我们可以通过二次赋值来改变变量的值,现在通过指针也可以:

package main  
  
import (  
    "fmt"  
)  
  
func main() {  
    b := 255  
    a := &b  
    fmt.Println("address of b is", a)  
    fmt.Println("value of b is", *a)  
    *a++  
    fmt.Println("new value of b is", b)  
}

程序返回:

address of b is 0xc0000aa058  
value of b is 255  
new value of b is 256

这里值需要通过对指针进行递增操作,就可以修改变量b的值。

与此同时,在传参过程中,也可以使用指针:

package main  
  
import (  
    "fmt"  
)  
  
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

众所周知,int是值类型数据,如果通过变量进行传递到方法作用域中,方法内作用域内操作的实际上是另外一个对象,比如:

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

返回:

value of a after function call is 58

但如果传参过程中使用指针,将a变量的指针对象传递到方法内,方法内修改的其实是内存地址变量,如此就可以将值类型对象的值对应更改,节省了额外的内存申请空间。

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

package main  
  
import (  
    "fmt"  
)  
  
func modify(arr *[3]int) {  
    (*arr)[0] = 90  
}  
  
func main() {  
    a := [3]int{89, 90, 91}  
    modify(&a)  
    fmt.Println(a)  
}

程序返回:

[90 90 91]

虽然可以用指针传递给一个数组作为方法的实参并对其进行修改,但这并不意味着我们一定得这么干,因为还可以使用切片:

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

程序返回:

[90 90 91]

因为切片与指针一样是引用类型,如果我们想通过一个函数改变一个数组的值,可以将该数组的切片当作参数传给函数,也可以将这个数组的指针当作参数传给函数,显而易见,使用切片更加方便。

此外,指针也可以拥有指针,也就是说,指针也可以指向别的指针所在的内存地址:

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

程序返回:

变量 a = 3000  
指针变量 *ptr = 3000  
指向指针的指针变量 **pptr = 3000

这里指针pptr指向指针ptr,指针ptr执行变量a

当我们改变指针的指针值:

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)  
  
    **pptr = 200  
  
    fmt.Printf("变量 a = %d\n", a)  
    fmt.Printf("指针变量 *ptr = %d\n", *ptr)  
    fmt.Printf("指向指针的指针变量 **pptr = %d\n", **pptr)  
}

程序返回:

变量 a = 3000  
指针变量 *ptr = 3000  
指向指针的指针变量 **pptr = 3000  
变量 a = 200  
指针变量 *ptr = 200  
指向指针的指针变量 **pptr = 200

可以看到发生了连锁反应,起始指向和最终指向都发生了变化,可谓是牵一发而动全身,事实上,指针操作要比重复赋值更加快捷。

结语

简而言之,很多编译型语言都在事实上存在指针,c/c++是真实的指针,而Java中其实是指针的引用,可以理解为不能操作指针的值,不允许指针运算的指针。现实问题是,go lang这种“次时代”的新潮流编程语言,为什么不像Java那样,仅仅实现“引用”,而一定非得给出“指针”的实质概念呢?

在Go lang官网文档中,可以窥得些许端倪:

In a function call, the function value and arguments are evaluated in the usual order. After they are evaluated, the parameters of the call are passed by value to the function and the called function begins execution.
文档地址: https://go.dev/ref/spec#Calls

一望而知,go lang的设计者们在go lang语法设计上存在“完美主义强迫症”,方法传参是绝对的传值,Go lang中方法传参只有值传递一种方式,不存在引用传递,这样一来,必须有明确的指针类型,才可以保证在传值的前提下能对对象进行修改。

其实 Python也在此处做出了妥协,可变数据类型进行引用传递,但go lang作为钢铁直男,宁愿增加更复杂的指针逻辑,也要彻底贯彻值传递逻辑,为的就是在适当的地方使用指针, 对程序运行速度和内存消耗有所增益。

相关文章
|
13天前
|
监控 算法 Go
Golang深入浅出之-Go语言中的服务熔断、降级与限流策略
【5月更文挑战第4天】本文探讨了分布式系统中保障稳定性的重要策略:服务熔断、降级和限流。服务熔断通过快速失败和暂停故障服务调用来保护系统;服务降级在压力大时提供有限功能以保持整体可用性;限流控制访问频率,防止过载。文中列举了常见问题、解决方案,并提供了Go语言实现示例。合理应用这些策略能增强系统韧性和可用性。
70 0
|
13天前
|
分布式计算 Java Go
Golang深入浅出之-Go语言中的分布式计算框架Apache Beam
【5月更文挑战第6天】Apache Beam是一个统一的编程模型,适用于批处理和流处理,主要支持Java和Python,但也提供实验性的Go SDK。Go SDK的基本概念包括`PTransform`、`PCollection`和`Pipeline`。在使用中,需注意类型转换、窗口和触发器配置、资源管理和错误处理。尽管Go SDK文档有限,生态系统尚不成熟,且性能可能不高,但它仍为分布式计算提供了可移植的解决方案。通过理解和掌握Beam模型,开发者能编写高效的数据处理程序。
143 1
|
13天前
|
缓存 测试技术 持续交付
Golang深入浅出之-Go语言中的持续集成与持续部署(CI/CD)
【5月更文挑战第5天】本文介绍了Go语言项目中的CI/CD实践,包括持续集成与持续部署的基础知识,常见问题及解决策略。测试覆盖不足、版本不一致和构建时间过长是主要问题,可通过全面测试、统一依赖管理和利用缓存优化。文中还提供了使用GitHub Actions进行自动化测试和部署的示例,强调了持续优化CI/CD流程以适应项目需求的重要性。
59 1
|
13天前
|
Kubernetes Cloud Native Go
Golang深入浅出之-Go语言中的云原生开发:Kubernetes与Docker
【5月更文挑战第5天】本文探讨了Go语言在云原生开发中的应用,特别是在Kubernetes和Docker中的使用。Docker利用Go语言的性能和跨平台能力编写Dockerfile和构建镜像。Kubernetes,主要由Go语言编写,提供了方便的客户端库与集群交互。文章列举了Dockerfile编写、Kubernetes资源定义和服务发现的常见问题及解决方案,并给出了Go语言构建Docker镜像和与Kubernetes交互的代码示例。通过掌握这些技巧,开发者能更高效地进行云原生应用开发。
64 1
|
13天前
|
负载均衡 监控 Go
Golang深入浅出之-Go语言中的服务网格(Service Mesh)原理与应用
【5月更文挑战第5天】服务网格是处理服务间通信的基础设施层,常由数据平面(代理,如Envoy)和控制平面(管理配置)组成。本文讨论了服务发现、负载均衡和追踪等常见问题及其解决方案,并展示了使用Go语言实现Envoy sidecar配置的例子,强调Go语言在构建服务网格中的优势。服务网格能提升微服务的管理和可观测性,正确应对问题能构建更健壮的分布式系统。
32 1
|
13天前
|
消息中间件 Go API
Golang深入浅出之-Go语言中的微服务架构设计与实践
【5月更文挑战第4天】本文探讨了Go语言在微服务架构中的应用,强调了单一职责、标准化API、服务自治和容错设计等原则。同时,指出了过度拆分、服务通信复杂性、数据一致性和部署复杂性等常见问题,并提出了DDD拆分、使用成熟框架、事件驱动和配置管理与CI/CD的解决方案。文中还提供了使用Gin构建HTTP服务和gRPC进行服务间通信的示例。
39 0
|
13天前
|
Prometheus 监控 Cloud Native
Golang深入浅出之-Go语言中的分布式追踪与监控系统集成
【5月更文挑战第4天】本文探讨了Go语言中分布式追踪与监控的重要性,包括追踪的三个核心组件和监控系统集成。常见问题有追踪数据丢失、性能开销和监控指标不当。解决策略涉及使用OpenTracing或OpenTelemetry协议、采样策略以及聚焦关键指标。文中提供了OpenTelemetry和Prometheus的Go代码示例,强调全面可观测性对微服务架构的意义,并提示选择合适工具和策略以确保系统稳定高效。
149 5
|
13天前
|
负载均衡 算法 Go
Golang深入浅出之-Go语言中的服务注册与发现机制
【5月更文挑战第4天】本文探讨了Go语言中服务注册与发现的关键原理和实践,包括服务注册、心跳机制、一致性问题和负载均衡策略。示例代码演示了使用Consul进行服务注册和客户端发现服务的实现。在实际应用中,需要解决心跳失效、注册信息一致性和服务负载均衡等问题,以确保微服务架构的稳定性和效率。
24 3
|
13天前
|
前端开发 Go
Golang深入浅出之-Go语言中的异步编程与Future/Promise模式
【5月更文挑战第3天】Go语言通过goroutines和channels实现异步编程,虽无内置Future/Promise,但可借助其特性模拟。本文探讨了如何使用channel实现Future模式,提供了异步获取URL内容长度的示例,并警示了Channel泄漏、错误处理和并发控制等常见问题。为避免这些问题,建议显式关闭channel、使用context.Context、并发控制机制及有效传播错误。理解并应用这些技巧能提升Go语言异步编程的效率和健壮性。
30 5
Golang深入浅出之-Go语言中的异步编程与Future/Promise模式
|
13天前
|
监控 负载均衡 算法
Golang深入浅出之-Go语言中的协程池设计与实现
【5月更文挑战第3天】本文探讨了Go语言中的协程池设计,用于管理goroutine并优化并发性能。协程池通过限制同时运行的goroutine数量防止资源耗尽,包括任务队列和工作协程两部分。基本实现思路涉及使用channel作为任务队列,固定数量的工作协程处理任务。文章还列举了一个简单的协程池实现示例,并讨论了常见问题如任务队列溢出、协程泄露和任务调度不均,提出了解决方案。通过合理设置缓冲区大小、确保资源释放、优化任务调度以及监控与调试,可以避免这些问题,提升系统性能和稳定性。
33 6