1.1 变量与常量

296 阅读7分钟

1.常量

常量使用关键字 const 定义,用于存储不会改变的数据。

存储在常量中的数据类型只可以是布尔型、数字型(整数型、浮点型和复数)和字符串型。

常量的定义格式:const identifier [type] = value,例如:

const Pi = 3.14159

在 Go 语言中,你可以省略类型说明符 [type],因为编译器可以根据变量的值来推断其类型。

  • 显式类型定义: const b string = "abc"
  • 隐式类型定义: const b = "abc"

一个没有指定类型的常量被使用时,会根据其使用环境而推断出它所需要具备的类型。换句话说,未定义类型的常量会在必要时刻根据上下文来获得相关类型。

var n int
f(n + 5) // 无类型的数字型常量 “5” 它的类型在这里变成了 int

常量的值必须是能够在编译时就能够确定的;你可以在其赋值表达式中涉及计算过程,但是所有用于计算的值必须在编译期间就能获得。

  • 正确的做法:const c1 = 2/3
  • 错误的做法:const c2 = getNumber() // 引发构建错误: getNumber() used as value

因为在编译期间自定义函数均属于未知,因此无法用于常量的赋值,但内置函数可以使用,如:len()。

数字型的常量是没有大小和符号的,并且可以使用任何精度而不会导致溢出:

const Ln2 = 0.693147180559945309417232121458\176568075500134360255254120680009
const Log2E = 1/Ln2 // this is a precise reciprocal
const Billion = 1e9 // float constant
const hardEight = (1 << 100) >> 97

根据上面的例子我们可以看到,反斜杠可以在常量表达式中作为多行的连接符使用。

与各种类型的数字型变量相比,你无需担心常量之间的类型转换问题,因为它们都是非常理想的数字。

不过需要注意的是,当常量赋值给一个精度过小的数字型变量时,可能会因为无法正确表达常量所代表的数值而导致溢出,这会在编译期间就引发错误。另外,常量也允许使用并行赋值的形式:

const beef, two, c = "eat", 2, "veg"
const Monday, Tuesday, Wednesday, Thursday, Friday, Saturday = 1, 2, 3, 4, 5, 6
const (
	Monday, Tuesday, Wednesday = 1, 2, 3
	Thursday, Friday, Saturday = 4, 5, 6
)

常量还可以用作枚举:

const (
	Unknown = 0
	Female = 1
	Male = 2
)

现在,数字 0、1 和 2 分别代表未知性别、女性和男性。这些枚举值可以用于测试某个变量或常量的实际值,比如使用 switch/case 结构.

在这个例子中,iota 可以被用作枚举值:

const (
	a = iota
	b = iota
	c = iota
)

第一个 iota 等于 0,每当 iota 在新的一行被使用时,它的值都会自动加 1,并且没有赋值的常量默认会应用上一行的赋值表达式:

// 赋值一个常量时,之后没赋值的常量都会应用上一行的赋值表达式
const (
	a = iota  // a = 0
	b         // b = 1
	c         // c = 2
	d = 5     // d = 5   
	e         // e = 5
)

// 赋值两个常量,iota 只会增长一次,而不会因为使用了两次就增长两次
const (
	Apple, Banana = iota + 1, iota + 2 // Apple=1 Banana=2
	Cherimoya, Durian                  // Cherimoya=2 Durian=3
	Elderberry, Fig                    // Elderberry=3, Fig=4

)

// 使用 iota 结合 位运算 表示资源状态的使用案例
const (
	Open = 1 << iota  // 0001
	Close             // 0010
	Pending           // 0100
)

const (
	_           = iota             // 使用 _ 忽略不需要的 iota
	KB = 1 << (10 * iota)          // 1 << (10*1)
	MB                             // 1 << (10*2)
	GB                             // 1 << (10*3)
	TB                             // 1 << (10*4)
	PB                             // 1 << (10*5)
	EB                             // 1 << (10*6)
	ZB                             // 1 << (10*7)
	YB                             // 1 << (10*8)
)

iota 也可以用在表达式中,如:iota + 50。在每遇到一个新的常量块或单个常量声明时, iota 都会重置为 0( 简单地讲,每遇到一次 const 关键字,iota 就重置为 0 )。

当然,常量之所以为常量就是恒定不变的量,因此我们无法在程序运行过程中修改它的值;如果你在代码中试图修改常量的值则会引发编译错误。

2.变量

声名变量的5种方式

第一种 :一行声明一个变量

var <name> <type>
var name string = "Go编程"
var rate float32 = 0.89

第二种:多个变量一起声明

声明多个变量,除了可以按照上面写成多行之外,还可以写成下面这样

var (
    name string
    age int
    gender string
)

第三种:声明和初始化一个变量

使用 := (推导声明写法或者短类型声明法:编译器会自动根据右值类型推断出左值的对应类型。),可以声明一个变量,并对其进行(显式)初始化。

name := "Go编程"

// 等价于

var name string = "Go编程"

// 等价于

var name = "Go编程"

第四种:声明和初始化多个变量

name, age := "wangbm", 28

这种方法,也经常用于变量的交换

var a int = 100
var b int = 200
b, a = a, b

第五种:new 函数声明一个指针变量

在这里要先讲一下,指针的相关内容。

变量分为两种 普通变量 和 指针变量

普通变量,存放的是数据本身,而指针变量存放的是数据的地址。

如下代码,age 是一个普通变量,存放的内容是 28,而 ptr 是 存放变量age值的内存地址:0xc000010098

func main()  {
    var age int = 28
    var ptr = &age  // &后面接变量名,表示取出该变量的内存地址
    fmt.Println("age: ", age)
    fmt.Println("ptr: ", ptr)
}

输出

age:  28
ptr:  0xc000010098

而这里要说的 new 函数,是 Go 里的一个内建函数。

使用表达式 new(Type) 将创建一个Type类型的匿名变量,初始化为Type类型的零值,然后返回变量地址,返回的指针类型为*Type

func main()  {
    ptr := new(int)
    fmt.Println("ptr address: ", ptr)
    fmt.Println("ptr value: ", *ptr)  // * 后面接指针变量,表示从内存地址中取出值
}

输出

ptr address:  0xc000010098
ptr value:  0

用new创建变量和普通变量声明语句方式创建变量没有什么区别,除了不需要声明一个临时变量的名字外,我们还可以在表达式中使用new(Type)。换言之,new函数类似是一种语法糖,而不是一个新的基础概念。

如下两种写法,可以说是等价的

// 使用 new
func newInt() *int {
    return new(int)
}

// 使用传统的方式
func newInt() *int {
    var dummy int
    return &dummy
}

以上不管哪种方法,变量/常量都只能声明一次,声明多次,编译就会报错。

3. 打印

函数 Printf 可以在 fmt 包外部使用,这是因为它以大写字母 P 开头,该函数主要用于打印输出到控制台。通常使用的格式化字符串作为第一个参数:

func Printf(format string, list of variables to be printed)

在示例 4.5 中,格式化字符串为:"The operating system is: %s\n"

这个格式化字符串可以含有一个或多个的格式化标识符,例如:%..,其中 .. 可以被不同类型所对应的标识符替换,如 %s 代表字符串标识符、%v 代表使用类型的默认输出格式的标识符。这些标识符所对应的值从格式化字符串后的第一个逗号开始按照相同顺序添加,如果参数超过 1 个则同样需要使用逗号分隔。使用这些占位符可以很好地控制格式化输出的文本。

函数 fmt.Sprintf 与 Printf 的作用是完全相同的,不过前者将格式化后的字符串以返回值的形式返回给调用者,因此你可以在程序中使用包含变量的字符串

函数 fmt.Print 和 fmt.Println 会自动使用格式化标识符 %v 对字符串进行格式化,两者都会在每个参数之间自动增加空格,而后者还会在字符串的最后加上一个换行符。例如:

fmt.Print("Hello:", 23)

将输出:Hello: 23

4. init 函数

变量除了可以在全局声明中初始化,也可以在 init 函数中初始化。这是一类非常特殊的函数,它不能够被人为调用,而是在每个包完成初始化后自动执行,并且执行优先级比 main 函数高。

每个源文件都只能包含一个 init 函数。初始化总是以单线程执行,并且按照包的依赖关系顺序执行。

一个可能的用途是在开始执行程序之前对数据进行检验或修复,以保证程序状态的正确性。