程序实体

Go 语言中的程序实体包括变量、常量、函数、结构体和接口。Go 语言是静态类型的编程语言,所以我们在声明变量或常量的时候,都需要指定它们的类型,或者给予足够的信息,这样才可以让 Go 语言能够推导出它们的类型。

在 Go 语言中,变量的类型可以是其预定义的那些类型,也可以是程序自定义的函数、结构体或接口。常量的合法类型不多,只能是那些 Go 语言预定义的基本类型。它的声明方式也更简单一些。

声明变量的方式

  • 方式1

var name = flag.String("name", "everyone", "The greeting object.")
  • 方式2

name := flag.String("name", "everyone", "The greeting object.")

由于 flag.String 返回值是 *string,需要将 fmt.Printf 修改为:

fmt.Printf("Hello, %v!\n", *name)

Tips:

类型推断是一种编程语言在编译期自动解释表达式类型的能力。其中方式1的表达式类型就是对表达式进行求值后得到结果的类型。Go 语言中的类型推断是很简约的,这也是 Go 语言整体的风格。

第一种方式中的代码在声明变量name的同时,还为它赋了值,而这时声明中并没有显式指定name的类型。

第二种方式所用的短变量声明,实际上就是 Go 语言的类型推断再加上一点点语法糖。

我们只能在函数体内部使用短变量声明。在编写if、for或switch语句的时候,我们经常把它安插在初始化子句中,并用来声明一些临时的变量。而相比之下,第一种方式更加通用,它可以被用在任何地方。

类型推断的优点

Go 语言的类型推断可以明显提升程序的灵活性,使得代码重构变得更加容易,同时又不会给代码的维护带来额外负担(实际上,它恰恰可以避免散弹式的代码修改),更不会损失程序的运行效率。也就是:

  • 省去很多无用的类型声明

  • 易于代码重构

package main

import (
  "flag"
  "fmt"
)

func main() {
  var name = getTheFlag()
  flag.Parse()
  fmt.Printf("Hello, %v!\n", *name)
}

func getTheFlag() *string {
  return flag.String("name", "everyone", "The greeting object.")
}

可以用 getTheFlag 函数包裹(或者说包装)那个对flag.String函数的调用,并把其结果直接作为getTheFlag函数的结果,结果的类型是*string。

这样一来,var name = 右边的表达式,可以变为针对getTheFlag函数的调用表达式了。这实际上是对 “声明并赋值name变量的那行代码” 的重构。

我们可以随意改变getTheFlag函数的内部实现,及其返回结果的类型,而不用修改main函数中的任何代码。

这个命令源码文件依然可以通过编译,并且构建和运行也都不会有问题。也许你能感觉得到,这是一个关于程序灵活性的质变。不显式地指定变量name的类型,使得它可以被赋予任何类型的值。也就是说,变量name的类型可以在其初始化时,由其他程序动态地确定。

在你改变getTheFlag函数的结果类型之后,Go 语言的编译器会在你再次构建该程序的时候,自动地更新变量name的类型。如果你使用过Python或Ruby这种动态类型的编程语言的话,一定会觉得这情景似曾相识。

Tips:

重构即为不改变某个程序与外界的任何交互方式和规则,而只改变其内部实现”的代码修改方式。重构的对象可以是一行代码、一个函数、一个功能模块,甚至一个软件系统。

Go 语言是静态类型的,所以一旦在初始化变量时确定了它的类型,之后就不可能再改变。这就避免了在后面维护程序时的一些问题。另外,请记住,这种类型的确定是在编译期完成的,因此不会对程序的运行效率产生任何影响。

变量的重声明

  • 关于代码块

在 Go 语言中,代码块一般就是一个由花括号括起来的区域,里面可以包含表达式和语句。Go 语言本身以及我们编写的代码共同形成了一个非常大的代码块,也叫全域代码块。这主要体现在,只要是公开的全局变量,都可以被任何代码所使用。

相对小一些的代码块是代码包,一个代码包可以包含许多子代码包,所以这样的代码块也可以很大。每个源码文件也都是一个代码块,每个函数也是一个代码块,每个if语句、for语句、switch语句和select语句都是一个代码块。甚至,switch或select语句中的case子句也都是独立的代码块。走个极端,我就在main函数中写一对紧挨着的花括号算不算一个代码块?当然也算,这甚至还有个名词,叫“空代码块”。

变量重声明的前提条件

  • 由于变量的类型在其初始化时就已经确定了,所以对它再次声明时赋予的类型必须与其原本的类型相同,否则会产生编译错误。

  • 变量的重声明只可能发生在某一个代码块中。如果与当前的变量重名的是外层代码块中的变量,那么就是另外一种含义了。

  • 变量的重声明只有在使用短变量声明时才会发生,否则也无法通过编译。如果要在此处声明全新的变量,那么就应该使用包含关键字var的声明语句,但是这时就不能与同一个代码块中的任何变量有重名了。

  • 被“声明并赋值”的变量必须是多个,并且其中至少有一个是新的变量。这时我们才可以说对其中的旧变量进行了重声明。

变量重声明其实算是一个语法糖(或者叫便利措施)。它允许我们在使用短变量声明时不用理会被赋值的多个变量中是否包含旧变量。

// 短变量重声明
var err error
n, err := io.WriteString(os.Stdout, "Hello, everyone!\n")

这里的短变量声明对新变量n和旧变量err进行了“声明并赋值”,这时也是对后者的重声明。

程序实体的访问权限

  • 包级私有

  • 模块级私有

  • 公开

包级私有和模块级私有访问权限对应的都是代码包代码块,公开的访问权限对应的是全域代码块。然而,这个颗粒度是比较粗的,我们往往需要利用代码块再细化程序实体的作用域。

在一个函数中声明了一个变量,那么在通常情况下,这个变量是无法被这个函数以外的代码引用的。这里的函数就是一个代码块,而变量的作用域被限制在了该代码块中。

一个程序实体的作用域总是会被限制在某个代码块中,而这个作用域最大的用处,就是对程序实体的访问权限的控制。

package main

import "fmt"

var block = "package"

func main() {
  block := "function"
  {
    block := "inner"
    fmt.Printf("The block is %s.\n", block)
  }
  fmt.Printf("The block is %s.\n", block)
}

这个命令源码文件中有四个代码块,它们是:全域代码块、main包代表的代码块、main函数代表的代码块,以及在main函数中的一个用花括号包起来的代码块。

代码块查找变量的过程

  • 代码引用变量的时候总会最优先查找当前代码块中的那个变量。注意,这里的“当前代码块”仅仅是引用变量的代码所在的那个代码块,并不包含任何子代码块。

  • 如果当前代码块中没有声明以此为名的变量,那么程序会沿着代码块的嵌套关系,从直接包含当前代码块的那个代码块开始,一层一层地查找。(往外查)

  • 一般情况下,程序会一直查到当前代码包代表的代码块。如果仍然找不到,那么 Go 语言的编译器就会报错了。

程序在查找代表变量未加限定符的名字(即标识符)的时候,是不会去被导入的代码包中查找的。除非把代码包导入语句写成import . "XXX"的形式(注意中间的那个“.”),那么就会让这个“XXX”包中公开的程序实体,被当前源码文件中的代码,视为当前代码包中的程序实体。

如果在具有嵌套关系的不同代码块中存在重名的变量,那么我们应该特别小心,它们之间可能会发生“屏蔽”的现象。这样你在不同代码块中引用到变量很可能是不同的。可重名变量可以各有各的类型。

变量类型判断

package main

import "fmt"

var container = []string{"zero", "one", "two"}

func main() {
  container := map[int]string{0: "zero", 1: "one", 2: "two"}
  fmt.Printf("The element is %q.\n", container[1])
}

我们可以使用类型断言来判断变量container的类型:

value, ok := interface{}(container).([]string)

在赋值符号的右边,是一个类型断言表达式。它包括了用来把container变量的值转换为空接口值的interface{}(container) 以及一个用于判断前者的类型是否为切片类型 []string 的 .([]string)

这个表达式的结果可以被赋给两个变量,在这里由value和ok代表。变量ok是布尔(bool)类型的,它将代表类型判断的结果,true或false。如果是true,那么被判断的值将会被自动转换为[]string类型的值,并赋给变量value,否则value将被赋予nil(即“空”)。

这里的ok也可以没有。也就是说,类型断言表达式的结果,可以只被赋给一个变量,在这里是value。但是这样的话,当判断为否时就会引发异常。这种异常在 Go 语言中被叫做panic,可翻译为运行时恐慌。因为它是一种在 Go 程序运行期间才会被抛出的异常,而“恐慌”二字是英文 Panic 的中文直译。

除非显式地“恢复”这种“恐慌”,否则它会使 Go 程序崩溃并停止。所以,在一般情况下,我们还是应该使用带ok变量的写法。

关于类型断言

类型断言表达式的语法形式是x.(T)。其中的x代表要被判断类型的值。这个值当下的类型必须是接口类型的,不过具体是哪个接口类型其实是无所谓的。所以,当这里的container变量类型不是任何的接口类型时,我们就需要先把它转成某个接口类型的值。如果container是某个接口类型的,那么这个类型断言表达式就可以是container.([]string)。

在 Go 语言中,interface{}代表空接口,任何类型都是它的实现类型。任何类型的值都可以很方便地被转换成空接口的值。

具体语法是interface{}(x),例如前面展示的interface{}(container)。

记住,一对不包裹任何东西的花括号,除了可以代表空的代码块之外,还可以用于表示不包含任何内容的数据结构(或者说数据类型)。

比如struct{},它就代表了不包含任何字段和方法的、空的结构体类型。

对于一些集合类的数据类型来说,{}还可以用来表示其值不包含任何元素,比如空的切片值[]string{},以及空的字典值map[int]string{}。

圆括号中[]string是一个类型字面量。所谓类型字面量,就是用来表示数据类型本身的若干个字符。比如,string是表示字符串类型的字面量,uint8是表示 8 位无符号整数类型的字面量,[]string 用来表示元素类型为string的切片类型,以及map[int]string,用来表示键类型为int、值类型为string的字典类型。

类型转换规则

语法形式: T(x)

其中的 x 可以是一个变量,也可以是一个代表值的字面量(比如1.23和struct{}{}),还可以是一个表达式。

注意,如果是表达式,那么该表达式的结果只能是一个值,而不能是多个值。

x可以被叫做源值,它的类型就是源类型,而那个T代表的类型就是目标类型。

如果从源类型到目标类型的转换是不合法的,那么就会引发一个编译错误。

更多详见: Golang 类型转换标准

  • 对于整数类型值、整数常量之间的类型转换,原则上只要源值在目标类型的可表示范围内就是合法的。比如,之所以uint8(255)可以把无类型的常量255转换为uint8类型的值,是因为255在[0, 255]的范围内。源整数类型的可表示范围较大,而目标类型的可表示范围较小的情况,比如把值的类型从int16转换为int8。

var srcInt = int16(-255)
dstInt := int8(srcInt)

变量 srcInt 的值是 int16 类型的 -255,而变量 dstInt 的值是由前者转换而来的,类型是int8。int16类型的可表示范围可比int8类型大了不少。

整数在 Go 语言以及计算机中都是以补码的形式存储的。这主要是为了简化计算机对整数的运算过程。补码其实就是原码各位求反再加 1。

比如,int16类型的值-255的补码是1111111100000001。如果我们把该值转换为int8类型的值,那么 Go 语言会把在较高位置(或者说最左边位置)上的 8 位二进制数直接截掉,从而得到00000001。

又由于其最左边一位是0,表示它是个正整数,以及正整数的补码就等于其原码,所以dstInt的值就是1。

当整数值的类型的有效范围由宽变窄时,只需在补码形式下截掉一定数量的高位二进制数即可。

当把一个浮点数类型的值转换为整数类型值时,前者的小数部分会被全部截掉。

  • 虽然直接把一个整数值转换为一个string类型的值是可行的,但值得关注的是,被转换的整数值应该可以代表一个有效的 Unicode 代码点,否则转换的结果将会是"�"(仅由高亮的问号组成的字符串值)。

字符'�'的 Unicode 代码点是U+FFFD。它是 Unicode 标准中定义的 Replacement Character,专用于替换那些未知的、不被认可的以及无法展示的字符。

  • 一个值在从string类型向[]byte类型转换时代表着以 UTF-8 编码的字符串会被拆分成零散、独立的字节。

除了与 ASCII 编码兼容的那部分字符集,以 UTF-8 编码的某个单一字节是无法代表一个字符的。

string([]byte{'\xe4', '\xbd', '\xa0', '\xe5', '\xa5', '\xbd'}) // 你好

UTF-8 编码的三个字节\xe4、\xbd和\xa0合在一起才能代表字符'你',而\xe5、\xa5和\xbd合在一起才能代表字符'好'。

其次,一个值在从string类型向[]rune类型转换时代表着字符串会被拆分成一个个 Unicode 字符。

string([]rune{'\u4F60', '\u597D'}) // 你好

理解了 Unicode 标准)及其字符集和编码方案之后,上面这些内容就会显得很容易了。

别名类型 & 潜在类型

  • 别名类型

type MyString = string

这条声明语句表示,MyString是string类型的别名类型。顾名思义,别名类型与其源类型的区别恐怕只是在名称上,它们是完全相同的。

源类型与别名类型是一对概念,是两个对立的称呼。别名类型主要是为了代码重构而存在的。

See More: https://golang.org/design/18130-type-alias

Go 语言内建的基本类型中就存在两个别名类型。byte 是 uint8 的别名类型,而 rune 是int32的别名类型。

  • 潜在类型

注意!

type MyString2 string // 注意,这里没有等号。

这种方式也可以被叫做对类型的再定义,MyString2 和 string就是两个不同的类型了。这里的MyString2是一个新的类型,不同于其他任何类型。

对于这里的类型再定义来说,string可以被称为MyString2的潜在类型

潜在类型的含义是,某个类型在本质上是哪个类型。潜在类型相同的不同类型的值之间是可以进行类型转换的。因此,MyString2类型的值与string类型的值可以使用类型转换表达式进行互转。

但对于集合类的类型[]MyString2与[]string来说这样做却是不合法的,因为[]MyString2与[]string的潜在类型不同,分别是[]MyString2和[]string。

最后更新于