当前位置: 首页 > article >正文

04 go语言(golang) - 变量和赋值过程

变量

在Go语言中,变量的定义和初始化是编程的基础部分。Go提供了多种方式来声明和初始化变量,以适应不同的使用场景。

基本变量声明

  1. 使用var关键字
    使用var关键字可以在函数内部或外部声明变量。如果在函数外部声明,该变量为全局变量。

    var str1 string = "变量1"
    var int1 int = 123
    fmt.Println(str1, int1)
    

    如果已经明确了初始值,则可以省略类型信息,让编译器自动推断类型:

    var str2 = "变量2"
    var int2 = 123
    fmt.Println(str2, int2)
    
  2. 短变量声明
    在函数内部,你可以使用更简洁的短变量声明(Short variable declarations)方式 :=。这种方式同时进行了声明和初始化,并且类型由右侧表达式推断得出。

    func main() {
        str3 := "变量3"
        int3 := 123
        fmt.Println(str3, int3)
    }
    

多重赋值

Go支持多个变量同时赋值:

	//var a, b, c = "1", 2, 3.55
	a, b, c := "1", 2, 3.55
	// 使用 reflect 包中的 .TypeOf 函数。返回变量的类型信息
	fmt.Println("a的类型为:", reflect.TypeOf(a))
	fmt.Println("b的类型为:", reflect.TypeOf(b))
	fmt.Println("c的类型为:", reflect.TypeOf(c))
	// 为相同类型的变量在同一行上声明和赋值
	//var a, b, c int = "1", 2, 3 // 错误,不能将字符串赋值给整数变量。 Go 不支持在同一行中为不同类型变量分配类型。
	var a, b, c int = 1, 2, 3
	fmt.Println("a的类型为:", reflect.TypeOf(a))
	fmt.Println("b的类型为:", reflect.TypeOf(b))
	fmt.Println("c的类型为:", reflect.TypeOf(c))

块声明(block declaration)语法。这种方式允许在一个 var 语句中声明多个变量,并为它们赋初始值。

	var (
		a = 1111
		b = "bbb"
		c bool
	)
	fmt.Println(a, b, c)

零值(Zero Values)

在Go中,如果你只是声明一个变量而没有给它赋初值,则会自动被赋予该类型的零值:

  • 数字类型(整型、浮点型等)的零值是 0
  • 布尔型的零值是 false
  • 字符串的零值是空字符串 ""
  • 指针、切片、映射、通道、函数以及接口的零值都是 nil

例如:

	var a string
	var b int
	var c float64
	var d bool

	fmt.Printf("a的值:【%v】\n", a)
	fmt.Printf("b的值:【%v】\n", b)
	fmt.Printf("c的值:【%v】\n", c)
	fmt.Printf("d的值:【%v】\n", d)
	/*
		输出结果为:
		a的值:【】
		b的值:【0】
		c的值:【0】
		d的值:【false】
	*/

作用域

在Go语言中,作用域(Scope)指的是一个变量或者函数名字在程序中可以被访问的区域。

  1. 全局作用域

    • 如果一个变量是在所有函数之外定义的,则它被认为是全局变量,可以在整个包内部任何位置使用。如果需要在其他包中使用这些全局变量或函数,则它们必须首字母大写。

    test1.go

    package main
    
    import "fmt"
    
    var name = "全局变量"
    
    func main() {
    	// 在同一包下的不同文件中可以访问并且修改全局变量
    	printGlobalVar()
    	fmt.Println(name)
    }
    

    test2.go

    package main
    
    import "fmt"
    
    func printGlobalVar() {
    	fmt.Println(name)
    	name = "修改后的全局变量"
    }
    
  2. 包级别作用域

    • 在同一个包内部,当变量或者常数被定义在所有函数之外时(但不一定非得在文件最顶端),这些标识符将对整个包内部可见。这意味着同一包下不同文件中的代码也可以访问这些变量。
    • 全局作用域的变量首字母小写,其实就相当于包级别作用域的变量
  3. 文件级别作用域

    • 这个概念在 Go 语言中并不存在。Go 语言中最小的作用域单位是包级别作用域。
  4. 局部作用域

    • 在函数内定义的变量只能在该函数内部访问,并且这种限制也适应于该函数内定义的其他嵌套块结构如if条件、循环等。
    func printLocalVar() {
    	var localVar = "局部变量"
    	fmt.Println(localVar)
    }
    
  5. 块级别作用域

    • Go 语言支持诸如 if、for、switch 等控制结构,并且每个结构都会引入新的块范围。例如,在 if 语句中声明并初始化一个新变量,在该 if 块及其子块中该变量是可见和可访问的,但超出此范围后就不再可见。
    func printBlockVar() {
    	var localVar = "局部变量"
      // 代码块
    	{
    		var blockVar = "块级变量"
    		fmt.Println(blockVar)
    		fmt.Println(localVar) // 可以访问局部变量
    	}
    	//fmt.Println(blockVar) // 报错,无法访问块级变量
    }
    

赋值过程

赋值过程及内存相关的知识,通过以下代码做分析

	name := "这是一段字符串"
	nickname := name
  1. 声明并初始化 name
    • name 被声明为一个新的局部变量,并且类型被推断为字符串(string)。它指向一个内存位置,该位置存储着字符串 "这是一段字符串"
  2. 声明并初始化 nickname
    • 接下来,另一个新变量 nickname 被声明,并使用短变量声明方式同时进行初始化。
    • 在这个赋值操作中,由于字符串在Go中是不可变的,并且赋值操作只是复制了引用(而非数据本身),因此 nickname 也会指向同一块内存地址,即指向 "这是一段字符串" 的那块内存。
    • 注意!namenickname 都指向同一个字符串字面值 "这是一段字符串" 的实际数据。这意味着他们共享相同的底层字节数组。然而,每个字符串变量(如 namenickname)本身也是一个独立的实体,拥有自己的内存地址。
  3. 内存共享
    • 重要的是理解,在这种情况下,没有新的字符串数据被创建。两个变量 namenickname 实际上共享相同的底层数据。这意味着对于只读数据如字符串来说非常高效。
  4. 垃圾回收考虑
    • 在Go语言中,当没有任何引用指向某块内存时(即不再有任何方式访问该内存),这块内存就可能被标记为可回收。只要任何一个变量仍然引用着那块内存(即使其中一个已经超出了作用域),那么该内存就不会被垃圾回收器回收。只有当所有引用(name和nickname)都消失时才会释放相关资源。

结合代码说明

package main

import (
	"fmt"
	"unsafe" // 代码使用 `unsafe` 包将变量的地址转换为 `stringHeader` 类型的指针,以便访问字符串的底层数据指针。
)

func main() {
	// 情况1 变量地址不同,指向的值地址相同
	name := "这是一段字符串"
	nickname := name

	fmt.Println("============打印变量的地址信息============")
	fmt.Printf("变量'name'  地址:【%p】\n", &name)
	fmt.Printf("变量'nickname'  地址:【%p】\n", &nickname)
	// 使用unsafe包来获取字符串底层数据指针
	nameDataPointer1 := (*stringHeader)(unsafe.Pointer(&name)).Data
	nicknameDataPointer1 := (*stringHeader)(unsafe.Pointer(&nickname)).Data
	fmt.Printf("'name'数据的指向地址: %x\n", nameDataPointer1)
	fmt.Printf("'nickname'数据的指向地址: %x\n\n", nicknameDataPointer1)

	// 情况2 变量地址不同,指向的值地址也相同,因为字符串是不可变的
	nickname = "这是一段字符串"

	fmt.Println("============打印变量的地址信息============")
	fmt.Printf("变量'name'  地址:【%p】\n", &name)
	fmt.Printf("变量'nickname'  地址:【%p】\n", &nickname)
	// 使用unsafe包来获取字符串底层数据指针
	nameDataPointer2 := (*stringHeader)(unsafe.Pointer(&name)).Data
	nicknameDataPointer2 := (*stringHeader)(unsafe.Pointer(&nickname)).Data
	fmt.Printf("'name'数据的指向地址: %x\n", nameDataPointer2)
	fmt.Printf("'nickname'数据的指向地址: %x\n\n", nicknameDataPointer2)

	// 情况3 变量地址不同,指向的值地址不同
	nickname = "这是一段字符串!"

	fmt.Println("============打印变量的地址信息============")
	fmt.Printf("变量'name'  地址:【%p】\n", &name)
	fmt.Printf("变量'nickname'  地址:【%p】\n", &nickname)
	// 使用unsafe包来获取字符串底层数据指针
	nameDataPointer3 := (*stringHeader)(unsafe.Pointer(&name)).Data
	nicknameDataPointer3 := (*stringHeader)(unsafe.Pointer(&nickname)).Data
	fmt.Printf("'name'数据的指向地址: %x\n", nameDataPointer3)
	fmt.Printf("'nickname'数据的指向地址: %x\n\n", nicknameDataPointer3)

}

/*
stringHeader用于获取字符串的底层数据指针,这个结构体是 Go 语言中字符串的内部表示。
`Data` 是一个 `uintptr` 类型的字段,它用来存储指向字符串数据的指针
`Len` 存储字符串的长度。
*/
type stringHeader struct {
	Data uintptr // 指向实际数据的指针
	Len  int     // 字符串的长度
}

打印结果

============打印变量的地址信息============
变量'name'  地址:【0xc000014070】
变量'nickname'  地址:【0xc000014080】
'name'数据的指向地址: 2f2a8fd
'nickname'数据的指向地址: 2f2a8fd

============打印变量的地址信息============
变量'name'  地址:【0xc000014070】
变量'nickname'  地址:【0xc000014080】
'name'数据的指向地址: 2f2a8fd
'nickname'数据的指向地址: 2f2a8fd

============打印变量的地址信息============
变量'name'  地址:【0xc000014070】
变量'nickname'  地址:【0xc000014080】
'name'数据的指向地址: 2f2a8fd
'nickname'数据的指向地址: 2f2b3eb

这段 Go 语言代码演示了字符串在不同情况下的内存地址和底层数据指针的变化。代码分为三个部分,每部分都打印了变量的内存地址和它们指向的字符串数据的地址。这里使用了 unsafe 包来操作指针和内存,这是 Go 语言中一个特殊的包,允许程序绕过类型安全性的限制。

  1. 情况1:创建了两个变量 namenickname,它们被赋予了同一个字符串字面量。由于字符串是不可变的,nickname 实际上是对 name 所指向的字符串的一个别名。因此,尽管 namenickname 作为变量有不同的内存地址,它们指向的字符串数据的地址是相同的。

  2. 情况2nickname 被重新赋值为与 name 相同的字符串字面量。由于字符串是不可变的,这个新的字符串字面量可能会指向与 name 相同的内存地址,或者可能会创建一个新的字符串实例,这取决于 Go 运行时的优化。在这个例子中,由于字符串内容相同,nickname 可能指向了与 name 相同的内存地址。

  3. 情况3nickname 被重新赋值为一个不同的字符串字面量,这次字符串内容与 name 不同。由于字符串是不可变的,这个新的字符串字面量将创建一个新的内存地址来存储这个新的字符串内容。因此,nickname 将指向一个新的内存地址,与 name 所指向的地址不同。

待后面我们展开学习go中的 *(指针)与&(地址)后,再回头来理解上面的赋值和内存相关,会容易理解很多。


http://www.kler.cn/news/353451.html

相关文章:

  • 配置MAC地址安全
  • 探索 Python 的测试与调试技巧
  • R语言手工实现主成分分析 PCA | 奇异值分解(svd) 与PCA | PCA的疑问和解答
  • 锐评 Nodejs 设计模式 - 创建与结构型
  • FFmpeg源码:avformat_new_stream函数分析
  • 2.4.ReactOS系统运行级别降低IRQL级别KfLowerIrql 函数
  • 搜维尔科技:SenseGlove Nova 2触觉反馈手套开箱测评
  • 【LaTex】12 usenix投稿模版 删除接受信息
  • android app执行shell命令视频课程补充android 10/11适配-千里马android
  • 三、ElementPlus下拉搜索加弹窗组件的封装
  • Python 基础之条件语句
  • 【JavaScript】LeetCode:66-70
  • Linux在命令行直接使用密码来远程执行命令和远程复制 + 新安装的库找不到问题解决方式
  • HDFS开启审计日志
  • 自动驾驶系列—厘米级精度:RTK技术如何革新自动驾驶定位
  • Android终端GB28181音视频实时回传设计探讨
  • 嵌入式系统---看门狗
  • 选项模式(Option Pattern)在 Go 语言中的应用
  • Unity RPG梦幻场景素材(附下载链接)
  • React中的useState和useEffect解析