文章目录
1. 前言
Go 语言中的程序实体包括变量、常量、函数、结构体和接口。
Go 是静态类型的编程语言,在声明变量或常量的时候,需要指定类型,或给予足够的信息,才可以让 Go 语言推导出它们的类型。
2. 声明变量
国际惯例,上代码,下面主要介绍比较典型的2种方式
package main
import (
"flag"
"fmt"
)
func main() {
var name string // [1]
flag.StringVar(&name, "name", "everyone", "The greeting object.") // [2]
fmt.Printf("Hello, %v!\n", name)
}
(1) var name = xxx
[1][2]合并[3],把被调用的函数由flag.StringVar
改为flag.String
,传参的列表也需要随之修改
var name = flag.String("name", "everyone", "The greeting object.") // [3]
fmt.Printf("Hello, %v!\n", *name)
原因:flag.String
函数返回的结果值的类型是*string
,是字符串的指针类型。因此,变量name
代表一个指向字符串值的指针。关于Go 语言的指针,后面会专门介绍。
(2) name := xxx
[3]改为[4],两种方式非常类似,基于[3],赋值符号=
右边的代码不动,左边只留下name
,再把=
变成:=
。
name := flag.String("name", "everyone", "The greeting object.") // [4]
fmt.Printf("Hello, %v!\n", *name)
(3) 知识点
- Go 语言自身的类型推断,而省去了对该变量的类型的声明。
- 短变量声明,Go 语言的类型推断再加上一点点语法糖。
我们只能在函数体内部使用短变量声明。在编写if、for或switch语句的时候,我们经常把它安插在初始化子句中,并用来声明一些临时的变量。
相比之下,第一种方式更加通用,它可以被用在任何地方。
类型推断的好处
- 少敲代码(当然,这不是最重要的)
- 便于代码重构
对于重构的理解,先看一段代码
package main
import (
"flag"
"fmt"
)
func main() {
var name = getTheFlag() // [5]
flag.Parse()
fmt.Printf("Hello, %v!\n", *name)
}
func getTheFlag() *string {
return flag.String("name", "everyone", "The greeting object.")
}
如果代码[3]函数不是flag.String
,而是自定义的函数,比如[5]中的getTheFlag
用getTheFlag
函数包裹对flag.String函数的调用,把结果直接作为getTheFlag
函数的结果
我们通常把不改变某个程序与外界的任何交互方式和规则,而只改变其内部实现”的代码修改方式,叫做对该程序的重构。重构的对象可以是一行代码、一个函数、一个功能模块,甚至一个软件系统。
可以随意改变getTheFlag
函数的内部实现,及其返回结果的类型,不用修改main
函数中的任何代码。
name
的类型,是在构建程序的时候,自动更新,可以理解成类型是在编译的时候确定的。
变量的重声明
涉及了短变量声明,可以对同一个代码块中的变量进行重声明,算是一个语法糖(或者叫便利措施)。
- 代码块:一个由花括号括起来的区域,
{}
称为空代码块 - 变量的重声明只可能发生在某一个代码块中
- 变量的重声明只有在使用短变量声明时才会发生,否则也无法通过编译
- 被“声明并赋值”的变量必须是多个,并且其中至少有一个是新的变量。这时我们才可以说对其中的旧变量进行了重声明。
重要代码展示:
var err error
n, err := io.WriteString(os.Stdout, "Hello, everyone!\n")
fmt.Printf("n=%v, err=%v\n", n, err)
执行结果:
n=17, err=<nil>
由于变量的类型在其初始化时就已经确定了,所以对它再次声明时赋予的类型必须与其原本的类型相同,否则会产生编译错误。
var err int
n, err := io.WriteString(os.Stdout, "Hello, everyone!\n") // cannot assign error to err (type int) in multiple assignment
fmt.Printf("n=%v, err=%v\n", n, err)
(4) 特点
两种方式各有千秋,有着各自的特点和适用场景。
前者可以被用在任何地方,而后者只能被用在函数或者其他更小的代码块中。
3. 作用域
程序实体的访问权限有三种:
- 包级私有
- 模块级私有
- 公开
(1) 一个变量与其外层代码块中的变量重名
继续上代码
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)
print()
}
func print() {
fmt.Printf("The block is %s.\n", block)
}
代码输出结果:
The block is inner.
The block is function.
The block is package.
看到这里,或许会觉得奇怪,声明重名的变量是无法通过编译的,用短变量声明对已有变量进行重声明除外,但这只是对于同一个代码块而言的。
对于不同的代码块来说,变量重名依然可以通过编译。
(2) 引用变量的机制
- 首先,代码引用变量的时候总会最优先查找当前代码块中的那个变量
- 其次,如果当前代码块中没有声明以此为名的变量,那么程序会沿着代码块的嵌套关系,从直接包含当前代码块的那个代码块开始,一层一层地查找。
- 一般情况下,程序会一直查到当前代码包代表的代码块。如果仍然找不到,那么 Go 语言的编译器就会报错了。
4. 变量类型相关
(1) 类型断言
依旧先上一段Demo代码
package main
import "fmt"
var container = []string{"zero", "one", "two"}
func main() {
value1, ok := interface{}(container).([]string)
fmt.Printf("value1:%v, ok:%v.\n", value1, ok)
container := map[int]string{0: "zero", 1: "one", 2: "two"} // [1]
value2, ok := interface{}(container).([]string)
fmt.Printf("value2:%v, ok:%v.\n", value2, ok)
// [2]
fmt.Printf("The element is %q.\n", container[1])
}
运行结果:
value1:[zero one two], ok:true.
value2:[], ok:false.
The element is "one".
根据以上情况,继续在[2]的位置添加代码,使[1]的类型判断正确:
value3, ok := interface{}(container).(map[int]string)
fmt.Printf("value3:%v, ok:%v.\n", value3, ok)
运行结果:
value1:[zero one two], ok:true.
value2:[], ok:false.
value3:map[0:zero 1:one 2:two], ok:true.
The element is "one".
类型断言表达式
interface{}(x)
interface{}
代表空接口,任何类型都是它的实现类型- 任何类型的值都可以很方便地被转换成空接口的值
- 对不包裹任何东西的花括号,除了可以代表空的代码块之外,还可以用于表示不包含任何内容的数据结构(或者说数据类型)
interface{}(x)
即上面代码中的interface{}(container)
x.(T)
- 类型断言表达式的语法形式是
x.(T)
x
代表要被判断类型的值,这个值当下的类型必须是接口类型的
表达式结果
- 表达式的结果可以被赋给两个变量,在这里由
value
和ok
代表。 - 变量
ok
是布尔bool
类型的,它将代表类型判断的结果,true
或false
。 - 如果
ok
是true
,那么被判断的值将会被自动转换为[]string
类型的值,并赋给变量value
。 - 如果
ok
是false
,那么value
将被赋予nil
(即“空”)。
(2) 类型转换
原则上只要源值在目标类型的可表示范围内就是合法的。
所以uint8(255)可以把无类型的常量255转换为uint8类型的值,是因为255在[0, 255]的范围内。
int16转换为int8
代码示例:
package main
import "fmt"
func main() {
var srcInt = int16(-255)
fmt.Printf("srcInt:%v\n", srcInt)
dstInt := int8(srcInt)
fmt.Printf("dstInt:%v\n", dstInt)
}
要想知道正确的结果,需要知道以下内容:
- 整数在 Go 语言以及计算机中都是以补码的形式存储的
- 原因:为了简化计算机对整数的运算过程。
- 补码:原码各位求反再加 1。
- int16类型的值-255的补码是1111111100000001
- 转换为int8类型,会把高位(最左边位置)上的 8 位二进制数截掉,即为:00000001
- 00000001,最左边一位是0,表示正整数
- 正整数的补码就等于其原码,所以dstInt的值就是1
运行结果:
srcInt:-255
dstInt:1
总结:
- 当整数值的类型的有效范围由宽变窄时,只需在补码形式下截掉一定数量的高位二进制数即可。
- 当把一个浮点数类型的值转换为整数类型值时,前者的小数部分会被全部截掉。
整数转string
fmt.Printf("string(-1):%v\n", string(-1)) // 执行结果 string(-1):�
fmt.Printf("str:%v\n", strconv.Itoa(-1)) // 执行结果 str:-1
字符'�'
的 Unicode
代码点是U+FFFD
。
它是 Unicode
标准中定义的 Replacement Character
,专用于替换未知的、不被认可的以及无法展示的字符。
提这个是因为在排查问题时也可能会遇到�
,你需要知道这可能是由于什么引起的。
string与切片
一个值在从string类型向[]byte类型转换时代表着以 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'}) // 你好
(3) 别名类型/潜在类型
关键字type
声明自定义的各种类型
别名类型
type MyString = string
作用:主要是为了代码重构而存在的
潜在类型
type MyString2 string // 注意,这里没有等号。
- 这种方式也可以被叫做对类型的再定义
MyString2
和string
是两个不同的类型MyString2
是一个新的类型string
被称为MyString2
的潜在类型
为了便于理解,上一段代码:
func main() {
type MyString2 string
var sttr1 MyString2
sttr1 = "sttr1"
fmt.Printf("sttr1:%v\n", sttr1)
var sttr2 string
sttr2 = "sttr2"
fmt.Printf("sttr2:%v\n", sttr2)
// [1]
sttr2 = sttr1 // cannot use sttr1 (type MyString2) as type string in assignment
fmt.Printf("sttr2:%v\n", sttr2)
}
以上代码无法编译,报错如[1]所示,如果换成type MyString2 = string
则ok
潜在类型相同的不同类型的值之间是可以进行类型转换的,如下代码,可正常编译
func main() {
type MyString2 string
var sttr1 MyString2
sttr1 = "sttr1"
type MyString3 string
var sttr2 MyString3
sttr2 = "sttr2"
sttr2 = MyString3(sttr1) // 如果是 sttr2 = string(sttr1) 无法编译通过
fmt.Printf("sttr2:%v\n", sttr2)
}
相互关系见下图:
Reference:
https://golang.google.cn/ref/spec#Conversions
极客时间 - Go语言核心36讲