变量是程序的基本组成部分,我们在“数据类型”一章,已经使用了变量。本章,会详细介绍如何创建变量,如何使用变量,以及变量的使用范围。还会介绍到常量的声明与使用。指针数据类型的声明与使用。

3.1 什么是变量

在代码中,变量很常见,是对值的引用,通俗讲就是给数据起个名字。变量是构造编程逻辑的基础之一。在“数据类型”一章,我们介绍 Go 是一种静态类型的编程语言,在声明变量时会显示或隐式的分配一个类型,也就是说 Go 支持类型推断,在声明变量时如果没有指定变量的类型,Go 会自动推断并分配合理的类型。

3.2 变量声明

Go 支持多种声明变量的方式,下面这段代码,声明了一个变量名为 superstar、类型是 string 的变量:

package main

import (
  "fmt"
)

func main() {
  var superstar string = "Stephen Curry"
  fmt.Println(superstar)
}

代码释义:

  1. 使用 var 关键字声明变量

  2. 变量名为 superstar

  3. 变量类型为 string

  4. 使用=赋值,=右边为值

  5. 字符串 Stephen Curry 是变量 superstar 的值

  6. 通过变量名 superstar,将值 Stephen Curry 传递给了 fmt 标准库的 Println 函数

  7. Stephen Curry 被打印到终端

变量的类型很重要,因为变量类型决定了我们可以为变量分配哪些值。例如,不能为 string 类型的变量分配 int 值,不能为 bool 类型的变量分配 string 值。如果为变量分配了不正确的值,会引发编译错误。我们来看一段为 string 类型的变量分配 int 值的示例:

package main

import "fmt"

func main() {
   var s string
   s = 30  
   fmt.Println(s)
}

运行这段代码,返回编译错误 cannot use 30 (type untyped int) as type string in assignment,因为代码试图为 string 类型的变量分配一个 int 值。

3.2.1 速写变量声明法

我们可以在同一代码行声明多个变量:

package main

import "fmt"

func main() {
   var c, k string = "Curry", "Klay"
   var name, number = "James", 23

   fmt.Println(c, k)
   fmt.Println(name, number)
}

以组的方式声明变量:

package main

import "fmt"

func main() {
var (
name     string = "Curry"
number   int    = 30
champion bool   = true
)
fmt.Printf("Name: %s, Number: %d, Champion: %v", name, number, champion)
}

变量声明后可以重新赋值,但不允许被再次声明,否则会导致编译时错误:

package main

import "fmt"

func main() {
   var number int = 8
   var number int = 24
   fmt.Println(number)
}

运行这段代码,会返回 number redeclared in this block 的错误。

3.2.2 短变量声明

在函数体内,Go 支持使用短变量的方式声明变量:

package main

import "fmt"

func main() {
   blog := "https://www.aiops.red"
   fmt.Println(blog)
}

代码释义:

  1. 声明了一个名为 blog 的变量,但我们省略了 var 关键字

  2. :=表示短变量声明,而无需使用 var 关键字或指定变量类型,:=右侧是变量的值

  3. 字符串 https://www.aiops.red 是变量 blog 的值

使用短变量方式声明时,不能指定数据类型,编译器会根据值自动推断。另外需要注意一点,短变量声明的方式只能在函数内部使用。

3.3 变量的零值

在声明时如果未给变量赋值,Go 会分配一个零值,零值和变量类型有关:

package main

import "fmt"

func main() {
  var (
    i int
    s string
    b bool
    f float64
)
  fmt.Printf("int: %d, string: %q, bool: %v, float: %v\n", i, s, b, f)
}

运行这段代码,会打印不同类型的零值:int: 0, string: “”, bool: false, float: 0。

在 Go 中,不应该检查变量是否为 nil,而应该检查其零值,因为 Go 禁止在初始化变量时分配 nil 值,这会导致编译时错误。string 类型的零值是””。

package main

import "fmt"

func main() {
   var s string
   if s == "" {
       fmt.Println("s has not been assigned a value and is zero valued")
  }
}

3.4 变量的作用域

变量作用域不是指声明变量的位置,而是可以引用该变量的区域。当变量被声明时,变量的作用域也就随之产生了。只要变量在作用域内,代码就可以访问它,如果超出了作用域,访问该变量就会报错。

变量作用域的作用之一就是,不同作用域的变量可以使用相同的名称。你能想象程序中的每个变量名字都不同吗?尤其在大型程序中。

在研究源码时,确定作用域也有帮助,因为你不需要记住所有变量。一旦某个变量超出了作用域,就可以将其抛诸脑后了。

在 Go 中,{}决定了作用域的开始与结束,{}表示一个 block,限定了作用域的范围。代码基本是从前向后执行的,所以子块可以引用父块中的变量,而父块不能引用子块的变量。

可以简单的总结为:

  • Go 中的{}表示一个 block

  • {}中声明的变量,可以在该 block 中的任何位置访问

  • {}内的{}表示一个新的 block,称为内部块,也叫子块

  • 内部块可以访问外部块的变量

  • 外部块不能够访问内部块的变量

  • 代码缩进反映了块作用域的级别,对于每个块,代码都会有缩进

下面的示例,展示了代码结构和变量的可用范围,由{}括起来,任何一对新的{}都表示一个新块:

package main

import "fmt"

var blog = "https://www.aiops.red"

func main() {

   fmt.Printf("%v from outer block\n", blog)

   b := true

   if b {

       fmt.Printf("%v from outer block\n", b)

       i := 666

       if b != false {
           fmt.Printf("%v from outer block\n", i)

      }
  }
}

变量 blog 不在{}内,但却在内部块中可用,这是因为 Go 将文件视为一个块,是一级大括号。

3.5 指针

指针是和变量有关的另一个重点。在 Go 中声明变量,计算机会在内存中为其分配地址。这样就可以存储、修改和检索变量的值。通过在变量名前加&符查看变量存储位置的地址。下列示例将变量的存储位置打印到终端:

package main

import "fmt"

func main() {
   blog := "https://www.aiops.red"
   fmt.Println(&blog)
}

接下来我们将一个变量传递给函数,并打印出传递前后变量的内存地址,你认为它们的内存地址相同吗?

package main

import "fmt"

func showMemoryAddress(x int) {
   fmt.Println(&x)
   return
}

func main() {
   i := 1
   fmt.Println(&i)
   showMemoryAddress(i)
}

执行这段代码,会打印两个不同的内存地址,这是因为变量传递给函数时,会进行新的内存分配,并将值的副本写入其中。现在,变量在不同的存储位置中有两个实例。通常,这不是我们想要的,因为它消耗了更多的内存,并且原始变量现在有多个副本,很容易引发异常。为此,Go 提供了指针。指针指向变量的存储位置,并且是 Go 中的一种数据类型。在变量名之前使用 * 声明指针。可以将上面的示例修改为使用指针,如下所示:

package main

import "fmt"

func showMemoryAddress(x *int) {
   fmt.Println(x)
   return
}

func main() {
   i := 1
   fmt.Println(&i)
   showMemoryAddress(&i)
}

代码释义:

  1. 传递给函数 showMemoryAddress 的参数从从 i 变成了 &i。前面加 & 符用来获取变量的存储地址

  2. showMemoryAddress 函数的第一个参数的类型从 int 改为 *int。前面的星号表示该类型现在是指向 int 的指针,而不是 int

  3. 当在 showMemoryAddress 函数中打印变量时,不需要使用星号,因为它是一个指针

如果需要使用指针的变量值而不是内存地址怎么办?可以通过在任何指针类型的变量名前加上星号。使用星号,将打印值,而不是内存地址:

func showMemoryAddress(x *int) {
   fmt.Println(*x)
   return
}

3.6 常量

常量在声明时必须赋值,且值一旦定义后,不能修改。在声明常量时,类型同样可以省略。常量的作用域和变量相同。

使用 const 关键字定义常量:

package main

import "fmt"

const superstar string = "Stephen Curry"

func main() {
   fmt.Println(superstar)
}

定义一批常量,下面常量的值可以省略,它们会使用前一个常量的值进行初始化:

const (
    A = "weiwendi"
    B //使用前一个常量的值进行初始化
    C //使用前一个常量的值进行初始化
    D = "sretech"
    E //使用前一个常量的值进行初始化
    F //使用前一个常量的值进行初始化
)

常量可以用来快速设置连续的值,比如定义周一到周日的常量:

package main

import "fmt"

const (
   Monday = iota + 1
   Tuesday
   Wednesday
   Thursday
   Friday
   Saturday
   Sunday
)

func main() {
   fmt.Println(Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday)
}

在常量组中,第一次使用 iota 时,值会初始化为0。

3.7 总结

本章介绍了变量的声明、零值和作用域,介绍了常量的声明,以及如何使用常量替换枚举。也简单了介绍到了指针,关于指针的知识,在后续的章节还会有所介绍。在函数体内声明的变量,必须被使用,不然会导致编译时错误。虽然常量声明后不使用可以正常编译,但不要在代码中声明不需要的变量或常量,保持代码的简洁与可阅读性。

文档更新时间: 2021-11-08 09:30   作者:admin