Go 语言中的程序实体包括:变量、常量、函数、结构体和接口。
Go 语言是静态类型的编程语言,因此咱们在声明变量或常量的时候,都须要指定它们的类型,或者给予足够的信息,这样才可让 Go 语言可以推导出它们的类型,在 Go 语言中,变量的类型能够是其预约义的那些类型,也能够是程序自定义的函数、结构体或接口。常量的合法类型很少,只能是那些 Go 语言预约义的基本类型。它的声明方式也更简单一些。golang
package main import ( "flag" "fmt" ) func main() { var name string //var name string这种声明变量name的方式 // [1] flag.StringVar(&name, "name", "everyone", "The greeting object.") // [2] // 方式1。 //var name = flag.String("name", "everyone", "The greeting object.") // 方式2。 //name := flag.String("name", "everyone", "The greeting object.") flag.Parse() fmt.Printf("Hello, %v!\n", name) // 适用于方式1和方式2。 //fmt.Printf("Hello, %v!\n", *name) }
第一种方式中的代码在声明变量name的同时,还为它赋了值,而这时声明中并无显式指定name的类型。这里利用了 Go 语言自身的类型推断,而省去了对该变量的类型的声明。编程
把被调用的函数由flag.StringVar改成flag.String,传参的列表也须要随之修改,这是为了[1]和[2]处代码合并的准备工做。c#
注意,flag.String函数返回的结果值的类型是string而不是string。类型string表明的是字符串的指针类型,而不是字符串类型。所以,这里的变量name表明的是一个指向字符串值的指针。数据结构
咱们能够经过操做符把这个指针指向的字符串值取出来了。所以,在这种状况下,那个被用来打印内容的函数调用就须要微调一下,把其中的参数name改成name,即:fmt.Printf("Hello, %v!\n", *name)。编程语言
第二种方式与第一种方式很是相似,它基于第一种方式的代码,赋值符号=右边的代码不动,左边只留下name,再把=变成:=ide
var name = flag.String("name", "everyone", "The greeting object.")
第一种方式中的代码在声明变量name的同时,还为它赋了值,而这时声明中并无显式指定name的类型。函数
这里利用了 Go 语言自身的类型推断,而省去了对该变量的类型的声明。学习
简单地说,类型推断是一种编程语言在编译期自动解释表达式类型的能力。什么是表达式?详细的解释你能够参看 Go 语言规范中的表达式https://golang.google.cn/ref/spec#Expressions 和表达式语句https://golang.google.cn/ref/spec#Expression_statements 章节网站
表达式类型就是对表达式进行求值后获得结果的类型。Go 语言中的类型推断是很简约的,这也是 Go 语言总体的风格。ui
它只能用于对变量或常量的初始化,就像上述回答中描述的那样。对flag.String函数的调用其实就是一个调用表达式,而这个表达式的类型是*string,即字符串的指针类型。
这也是调用flag.String函数后获得结果的类型。随后,Go 语言把这个调用了flag.String函数的表达式类型,直接做为了变量name的类型,这就是“推断”一词所指代的操做了。
name := flag.String("name", "everyone", "The greeting object.")
至于第二种方式所用的短变量声明,实际上就是 Go 语言的类型推断再加上一点点语法糖。
咱们只能在函数体内部使用短变量声明
。在编写if、for或switch语句的时候,咱们常常把它安插在初始化子句中,并用来声明一些临时的变量。而相比之下,第一种方式更加通用,它能够被用在任何地方。
先看一段代码:
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.") } //上面函数的实现也能够是这样的。 //func getTheFlag() *int { // return flag.Int("num", 1, "The number of greeting object.") //}
go run demo8.go -name huaihe Hello, huaihe!
name能不能是数字呢?
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.") // } //上面函数的实现也能够是这样的。 func getTheFlag() *int { return flag.Int("name", 1, "The number of greeting object.") }
name输出已是一个数字了
go run demo8.go -name=2 Hello, 2!
咱们能够用getTheFlag函数包裹(或者说包装)那个对flag.String函数的调用,并把其结果直接做为getTheFlag函数的结果,结果的类型是*string。
这样一来,var name =右边的表达式,能够变为针对getTheFlag函数的调用表达式了。这其实是对“声明并赋值name变量的那行代码”的重构。
一般把不改变某个程序与外界的任何交互方式和规则,而只改变其内部实现”的代码修改方式,叫作对该程序的重构。重构的对象能够是一行代码、一个函数、一个功能模块,甚至一个软件系统。
好了,在准备工做作完以后,你会发现,你能够随意改变getTheFlag函数的内部实现,及其返回结果的类型,而不用修改main函数中的任何代码。
这个命令源码文件依然能够经过编译,而且构建和运行也都不会有问题。也许你能感受获得,这是一个关于程序灵活性的质变。
咱们不显式地指定变量name的类型,使得它能够被赋予任何类型的值。也就是说,变量name的类型能够在其初始化时,由其余程序动态地肯定。
在你改变getTheFlag函数的结果类型以后,Go 语言的编译器会在你再次构建该程序的时候,自动地更新变量name的类型。
经过这种类型推断,你能够体验到动态类型编程语言所带来的一部分优点,即程序灵活性的明显提高。但在那些编程语言中,这种提高能够说是用程序的可维护性和运行效率换来的。
Go 语言是静态类型的,因此一旦在初始化变量时肯定了它的类型,以后就不可能再改变。这就避免了在后面维护程序时的一些问题。另外,请记住,这种类型的肯定是在编译期完成的,所以不会对程序的运行效率产生任何影响。
总结:
Go 语言的类型推断能够明显提高程序的灵活性,使得代码重构变得更加容易,同时又不会给代码的维护带来额外负担(实际上,它偏偏能够避免散弹式的代码修改),更不会损失程序的运行效率。
变量声明。经过使用它,咱们能够对同一个代码块中的变量进行重声明。
说到了代码块,我先来解释一下它。在 Go 语言中,代码块通常就是一个由花括号括起来的区域,里面能够包含表达式和语句。Go 语言自己以及咱们编写的代码共同造成了一个很是大的代码块,也叫全域代码块。
这主要体如今,只要是公开的全局变量,均可以被任何代码所使用。相对小一些的代码块是代码包,一个代码包能够包含许多子代码包,因此这样的代码块也能够很大。
接下来,每一个源码文件也都是一个代码块,每一个函数也是一个代码块,每一个if语句、for语句、switch语句和select语句都是一个代码块。甚至,switch或select语句中的case子句也都是独立的代码块。走个极端,我就在main函数中写一对紧挨着的花括号算不算一个代码块?固然也算,这甚至还有个名词,叫“空代码块”。
变量重声明的前提条件以下:
变量重声明其实算是一个语法糖(或者叫便利措施)。它容许咱们在使用短变量声明时不用理会被赋值的多个变量中是否包含旧变量。能够想象,若是不这样会多写很多代码。
package main import ( "fmt" "io" "os" ) func main() { var err error n, err := io.WriteString(os.Stdout, "Hello, everyone!\n") // 这里对`err`进行了重声明。 if err != nil { fmt.Printf("Error: %v\n", err) } fmt.Printf("%d byte(s) were written.\n", n) }
使用短变量声明对新变量n和旧变量err进行了“声明并赋值”,这时也是对后者的重声明。
在本篇中,咱们聚焦于最基本的 Go 语言程序实体:变量。并详细解说了变量声明和赋值的基本方法,及其背后的重要概念和知识。咱们使用关键字var和短变量声明,均可以实现对变量的“声明并赋值”。
这两种方式各有千秋,有着各自的特色和适用场景。前者能够被用在任何地方,然后者只能被用在函数或者其余更小的代码块中。
不过,经过前者咱们没法对已有的变量进行重声明,也就是说它没法处理新旧变量混在一块儿的状况。不过它们也有一个很重要的共同点,即:基于类型推断,Go 语言的类型推断只应用在了对变量或常量的初始化方面。
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) }
执行结果:
go run demo10.go The block is inner. The block is function.
程序实体的访问权限有三种:包级私有的、模块级私有的和公开的,包级私有和模块级私有访问权限对应的都是代码包代码块,公开的访问权限对应的是全域代码块。
这个命令源码文件中有四个代码块,它们是:全域代码块、main包表明的代码块、main函数表明的代码块,以及在main函数中的一个用花括号包起来的代码块。后三个代码块中分别声明了一个名为block的变量,并分别把字符串值"package"、"function"和"inner"赋给了它们。此外,我在后两个代码块的最后分别尝试用fmt.Printf函数打印出“The block is %s.”。这里的“%s”只是为了占位,程序会用block变量的实际值替换掉。
首先,代码引用变量的时候总会最优先查找当前代码块中的那个变量。注意,这里的“当前代码块”仅仅是引用变量的代码所在的那个代码块,并不包含任何子代码块。
其次,若是当前代码块中没有声明以此为名的变量,那么程序会沿着代码块的嵌套关系,从直接包含当前代码块的那个代码块开始,一层一层地查找。
通常状况下,程序会一直查到当前代码包表明的代码块。若是仍然找不到,那么 Go 语言的编译器就会报错了。
从做用域的角度也能够说,虽然经过var block = "package"声明的变量做用域是整个main代码包,可是在main函数中,它却被那两个同名的变量“屏蔽”了。
虽然main函数首先声明的block的做用域,是整个main函数,可是在最内层的那个代码块中,它倒是不可能被引用到的。
最内层的{ }代码块会使用当前代码块{ }的变量block := "inner",因此第一次打印The block is inner.。
最内层代码块中的block也不可能被该块以外的main代码引用到,因此第二行打印“The block is function.”
方便描述,把不一样代码块中的重名变量叫作“可重名变量”。注意,在同一个代码块中不容许出现重名的变量,这违背了 Go 语言的语法。
(1)变量重声明中的变量必定是在某一个代码块内的。注意,这里的“某一个代码块内”并不包含它的任何子代码块,不然就变成了“多个代码块之间”。而可重名变量指的正是在多个代码块之间由相同的标识符表明的变量。
(2)变量重声明是对同一个变量的屡次声明,这里的变量只有一个。而可重名变量中涉及的变量确定是有多个的。
(3)不论对变量重声明多少次,其类型必须始终一致,具体听从它第一次被声明时给定的类型。而可重名变量之间不存在相似的限制,它们的类型能够是任意的。
(4)若是可重名变量所在的代码块之间,存在直接或间接的嵌套关系,那么它们之间必定会存在“屏蔽”的现象。可是这种现象绝对不会在变量重声明的场景下出现。
既然可重名变量的类型能够是任意的,那么当它们之间存在“屏蔽”时你就更须要注意了。不一样类型的值大都有着不一样的特性和用法。当你在某一种类型的值上施加只有在其余类型值上才能作的操做时,Go 语言编译器必定会告诉你:“这不能够”。
看个例子,两个都叫作container的变量,分别位于main包代码块和main函数代码块。main包代码块中的变量是切片(slice)类型的,另外一个是字典(map)类型的。在main函数的最后,我试图打印出container变量的值中索引为1的那个元素:
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]) }
go run demo11.go The element is "one".
若是修改下代码,把:
package main import "fmt" var container = []string{"zero", "one", "two"} func main() { container := map[int]string{0: "zero", 1: "1", 2: "two"} //这里 修改1:"one" 为 1:"1" fmt.Printf("The element is %q.\n", container[1]) }
输出是1,说明代码执行使用的内层{ }代码块中的变量。
go run demo11.go The element is "1".
答案是使用“类型断言”表达式。具体怎么写呢?
value, ok := interface{}(container).([]string)
赋值语句的赋值符号的右边,是一个类型断言表达式,它包括了用来把container变量的值转换为空接口值的interface{}(container)。以及一个用于判断前者的类型是否为切片类型 []string 的 .([]string)。
这个表达式的结果能够被赋给两个变量,在这里由value和ok表明。变量ok是布尔(bool)类型的,它将表明类型判断的结果,true或false。
若是是true,那么被判断的值将会被自动转换为[]string类型的值,并赋给变量value,不然value将被赋予nil(即“空”)。
顺便提一下,这里的ok也能够没有。也就是说,类型断言表达式的结果,能够只被赋给一个变量,在这里是value。可是这样的话,当判断为否时就会引起异常。
类型断言表达式的语法形式是x.(T)。其中的x表明要被判断类型的值。这个值当下的类型必须是接口类型的,不过具体是哪一个接口类型实际上是无所谓的。因此,当这里的container变量类型不是任何的接口类型时,咱们就须要先把它转成某个接口类型的值。
若是container是某个接口类型的,那么这个类型断言表达式就能够是container.([]string)。这样看是否是清晰一些了?
interface{}表明空接口,任何类型都是它的实现类型。我在下个模块,会再讲接口及其实现类型的问题。如今你只要知道,任何类型的值均可以很方便地被转换成空接口的值就好了。
你可能会对这里的{}产生疑惑,为何在关键字interface的右边还要加上这个东西?
请记住,一对不包裹任何东西的花括号,除了能够表明空的代码块以外,还能够用于表示不包含任何内容的数据结构(或者说数据类型)。
好比你从此确定会遇到的struct{},它就表明了不包含任何字段和方法的、空的结构体类型。而空接口interface{}则表明了不包含任何方法定义的、空的接口类型。固然了,对于一些集合类的数据类型来讲,{}还能够用来表示其值不包含任何元素,好比空的切片值[]string{},以及空的字典值map[int]string{}。
最右边看。圆括号中[]string是一个类型字面量。所谓类型字面量,就是用来表示数据类型自己的若干个字符。
好比,string是表示字符串类型的字面量,uint8是表示 8 位无符号整数类型的字面量。
再复杂一些的就是咱们刚才提到的[]string,用来表示元素类型为string的切片类型,以及map[int]string,用来表示键类型为int、值类型为string的字典类型。
首先,对于整数类型值、整数常量之间的类型转换,原则上只要源值在目标类型的可表示范围内就是合法的。好比,之因此uint8(255)能够把无类型的常量255转换为uint8类型的值,是由于255在 [0, 255] 的范围内。但须要特别注意的是,源整数类型的可表示范围较大,而目标类型的可表示范围较小的状况,好比把值的类型从int16转换为int8。请看下面这段代码:
var srcInt = int16(-255) dstInt := int8(srcInt)
变量srcInt的值是int16类型的-255,而变量dstInt的值是由前者转换而来的,类型是int8。int16类型的可表示范围可比int8类型大了很多。
问题是,dstInt的值是多少?首先你要知道,整数在 Go 语言以及计算机中都是以补码的形式存储的。这主要是为了简化计算机对整数的运算过程。补码其实就是原码各位求反再加 1。好比,int16类型的值-255的补码是1111111100000001。若是咱们把该值转换为int8类型的值,那么 Go 语言会把在较高位置(或者说最左边位置)上的 8 位二进制数直接截掉,从而获得00000001。又因为其最左边一位是0,表示它是个正整数,以及正整数的补码就等于其原码,因此dstInt的值就是1。
必定要记住,当整数值的类型的有效范围由宽变窄时,只需在补码形式下截掉必定数量的高位二进制数便可。
相似的快刀斩乱麻规则还有:当把一个浮点数类型的值转换为整数类型值时,前者的小数部分会被所有截掉。
第二,虽然直接把一个整数值转换为一个string类型的值是可行的,但值得关注的是,被转换的整数值应该能够表明一个有效的 Unicode 代码点,不然转换的结果将会是"�"(仅由高亮的问号组成的字符串值)。
字符'�'的 Unicode 代码点是U+FFFD。它是 Unicode 标准中定义的 Replacement Character,专用于替换那些未知的、不被承认的以及没法展现的字符。我确定不会去问“哪一个整数值转换后会获得哪一个字符串”,这太变态了!可是我会写下:
string(-1)
并询问会获得什么?这但是彻底不一样的问题啊。因为-1确定没法表明一个有效的 Unicode 代码点,因此获得的总会是"�"。在实际工做中,咱们在排查问题时可能会遇到�,你须要知道这多是因为什么引发的。
第三个知识点是关于string类型与各类切片类型之间的互转的。
你先要理解的是,一个值在从string类型向[]byte类型转换时表明着以 UTF-8 编码的字符串会被拆分红零散、独立的字节。除了与 ASCII 编码兼容的那部分字符集,以 UTF-8 编码的某个单一字节是没法表明一个字符的。
string([]byte{'\xe4', '\xbd', '\xa0', '\xe5', '\xa5', '\xbd'}) // 你好
好比,UTF-8 编码的三个字节\xe四、\xbd和\xa0合在一块儿才能表明字符'你',而\xe五、\xa5和\xbd合在一块儿才能表明字符'好'。
其次,一个值在从string类型向[]rune类型转换时表明着字符串会被拆分红一个个 Unicode 字符。
string([]rune{'\u4F60', '\u597D'}) // 你好
当你真正理解了 Unicode 标准及其字符集和编码方案以后,上面这些内容就会显得很容易了。什么是 Unicode 标准?我会首先推荐你去它的http://www.unicode.org/ 官方网站一探究竟。
咱们能够用关键字type声明自定义的各类类型。固然了,这些类型必须在 Go 语言基本类型和高级类型的范畴以内。在它们当中,有一种被叫作“别名类型”的类型。咱们能够像下面这样声明它:
type MyString = string
这条声明语句表示,MyString是string类型的别名类型。顾名思义,别名类型与其源类型的区别恐怕只是在名称上,它们是彻底相同的。源类型与别名类型是一对概念,是两个对立的称呼。别名类型主要是为了代码重构而存在的
Go 语言内建的基本类型中就存在两个别名类型。byte是uint8的别名类型,而rune是int32的别名类型。
必定要注意,若是我这样声明:
type MyString2 string // 注意,这里没有等号。
MyString2和string就是两个不一样的类型了。这里的MyString2是一个新的类型,不一样于其余任何类型。这种方式也能够被叫作对类型的再定义。咱们刚刚把string类型再定义成了另一个类型MyString2。
对于这里的类型再定义来讲,string能够被称为MyString2的潜在类型。潜在类型的含义是,某个类型在本质上是哪一个类型。
潜在类型相同的不一样类型的值之间是能够进行类型转换的。所以,MyString2类型的值与string类型的值可使用类型转换表达式进行互转。
但对于集合类的类型[]MyString2与[]string来讲这样作倒是不合法的,由于[]MyString2与[]string的潜在类型不一样,分别是[]MyString2和[]string。另外,即便两个不一样类型的潜在类型相同,它们的值之间也不能进行判等或比较,它们的变量之间也不能赋值。
package main import ( "fmt" ) var container = []string{"zero", "one", "two"} func main() { container := map[int]string{0: "zero", 1: "one", 2: "two"} // 方式1。 _, ok1 := interface{}(container).([]string) _, ok2 := interface{}(container).(map[int]string) if !(ok1 || ok2) { fmt.Printf("Error: unsupported container type: %T\n", container) return } fmt.Printf("The element is %q. (container type: %T)\n", container[1], container) // 方式2。 elem, err := getElement(container) if err != nil { fmt.Printf("Error: %s\n", err) return } fmt.Printf("The element is %q. (container type: %T)\n", elem, container) } func getElement(containerI interface{}) (elem string, err error) { switch t := containerI.(type) { case []string: elem = t[1] case map[int]string: elem = t[1] //这里若是改成t[0],输出是zero default: err = fmt.Errorf("unsupported container type: %T", containerI) return } return }
go run demo12.go The element is "one". (container type: map[int]string) The element is "one". (container type: map[int]string)
package main import ( "fmt" ) func main() { // 重点1的示例。 var srcInt = int16(-255) // 请注意,之因此要执行uint16(srcInt),是由于只有这样才能获得全二进制的表示。 // 例如,fmt.Printf("%b", srcInt)将打印出"-11111111",后者是负数符号再加上srcInt的绝对值的补码。 // 而fmt.Printf("%b", uint16(srcInt))才会打印出srcInt原值的补码"1111111100000001"。 fmt.Printf("The complement of srcInt: %b (%b)\n", uint16(srcInt), srcInt) dstInt := int8(srcInt) fmt.Printf("The complement of dstInt: %b (%b)\n", uint8(dstInt), dstInt) fmt.Printf("The value of dstInt: %d\n", dstInt) fmt.Println() // 重点2的示例。 fmt.Printf("The Replacement Character: %s\n", string(-1)) fmt.Printf("The Unicode codepoint of Replacement Character: %U\n", '�') fmt.Println() // 重点3的示例。 srcStr := "你好" fmt.Printf("The string: %q\n", srcStr) fmt.Printf("The hex of %q: %x\n", srcStr, srcStr) fmt.Printf("The byte slice of %q: % x\n", srcStr, []byte(srcStr)) fmt.Printf("The string: %q\n", string([]byte{'\xe4', '\xbd', '\xa0', '\xe5', '\xa5', '\xbd'})) fmt.Printf("The rune slice of %q: %U\n", srcStr, []rune(srcStr)) fmt.Printf("The string: %q\n", string([]rune{'\u4F60', '\u597D'})) }
go run demo13.go The complement of srcInt: 1111111100000001 (-11111111) The complement of dstInt: 1 (1) The value of dstInt: 1 The Replacement Character: � The Unicode codepoint of Replacement Character: U+FFFD The string: "你好" The hex of "你好": e4bda0e5a5bd The byte slice of "你好": e4 bd a0 e5 a5 bd The string: "你好" The rune slice of "你好": [U+4F60 U+597D] The string: "你好"
package main import "fmt" func main() { // 示例1。 { type MyString = string str := "BCD" myStr1 := MyString(str) myStr2 := MyString("A" + str) fmt.Printf("%T(%q) == %T(%q): %v\n", str, str, myStr1, myStr1, str == myStr1) fmt.Printf("%T(%q) > %T(%q): %v\n", str, str, myStr2, myStr2, str > myStr2) fmt.Printf("Type %T is the same as type %T.\n", myStr1, str) strs := []string{"E", "F", "G"} myStrs := []MyString(strs) fmt.Printf("A value of type []MyString: %T(%q)\n", myStrs, myStrs) fmt.Printf("Type %T is the same as type %T.\n", myStrs, strs) fmt.Println() } // 示例2。 { type MyString string str := "BCD" myStr1 := MyString(str) myStr2 := MyString("A" + str) _ = myStr2 //fmt.Printf("%T(%q) == %T(%q): %v\n", // str, str, myStr1, myStr1, str == myStr1) // 这里的判等不合法,会引起编译错误。 //fmt.Printf("%T(%q) > %T(%q): %v\n", // str, str, myStr2, myStr2, str > myStr2) // 这里的比较不合法,会引起编译错误。 fmt.Printf("Type %T is different from type %T.\n", myStr1, str) strs := []string{"E", "F", "G"} var myStrs []MyString //myStrs := []MyString(strs) // 这里的类型转换不合法,会引起编译错误。 //fmt.Printf("A value of type []MyString: %T(%q)\n", // myStrs, myStrs) fmt.Printf("Type %T is different from type %T.\n", myStrs, strs) fmt.Println() } // 示例3。 { type MyString1 = string type MyString2 string str := "BCD" myStr1 := MyString1(str) myStr2 := MyString2(str) myStr1 = MyString1(myStr2) myStr2 = MyString2(myStr1) myStr1 = str //myStr2 = str // 这里的赋值不合法,会引起编译错误。 //myStr1 = myStr2 // 这里的赋值不合法,会引起编译错误。 //myStr2 = myStr1 // 这里的赋值不合法,会引起编译错误。 } }
go run demo14.go string("BCD") == string("BCD"): true string("BCD") > string("ABCD"): true Type string is the same as type string. A value of type []MyString: []string(["E" "F" "G"]) Type []string is the same as type []string. Type main.MyString is different from type string. Type []main.MyString is different from type []string.