变量的声明
var s string
var s2 = string("shijie")
s1 := "wenxuwan"
fmt.Println(s,s1,s2)
第一种方式是最传统的变量声明方式,可以显式的看到变量的类型。
第二种是利用go语言的类型推断,在声明s2的时候我们不需要定义s2的类型,它会根据后面表达式返回类型来自动判断s2类型
第三种是在go语言的推断上加了点语法糖,只能在函数内部使用,或者写for,if,switch语句的时候用在初始化语句中来声明一些临时的变量。不能作为全局声明。
go语言的类型推断有哪些好处
1. 可以不用自己敲那么多声明
2. 代码重构的时候(代码重构指的是“不改变某个程序与外界的交互方式和归结,只改变函数内部实现。也就是暴露出来的接口的参数,名字,返回值不变”)方便,不需要关注函数内部返回值的变化。
3. 因为go语言的是静态语言,变量类型在编译期间就确定了,所以不会影响到函数的执行效率。
- 函数重声明主要是用短变量声明对同一个代码块下面的变量进行声明
重声明的前提条件
1.再次声明的时候要和前面声明的类型一致
2.声明和重声明要在同一代码块下,不然就相当于局部变量覆盖全局变量
3.重声明只有在短变量声明的时候才会发生,不然用var声明两个同名的变量会提示冲突
4.重声明的时候一定要保证至少有一个变量是新的。
func main() {
var s string
var s2 = string("old")
s2 := string("new") //error,no new variables on left side of :=
fmt.Println(s,s2)
}
思考题目
如果与当前变量重名的是外层代码中的变量,这意味着什么?
var s2 = string("out")
func main() {
var s string
{
s2 := string("internal")
fmt.Println(s,s2)
}
fmt.Println(s2)
}
output: internal out
局部变量会隐藏外部变量
小结
使用关键字var和短变量声明,都可以实现对变量的“声明并赋值”。
前者可以被用在任何地方,而后者只能被用在函数或者其他更小的代码块中。
前者无法对已有的变量进行声明,就是无法处理新旧变量混在一起的情况。可以使用后者的变量重声明实现。
共同点是,都是基于“类型推断”。
package main
import (
"flag"
"fmt"
)
func main() {
var name string // [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)
}
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.")
//}
引用变量时的查找过程
首先,会在当前代码块中查找变量。不包含任何的子代码块。
其次,如果当前代码块没有什么此变量名,一层一层往上层的代码块查找。
最后,如果都找不到,则编译器会报错。
不同代码块的变量可以重名,并且类型也可以不同。必要时,在使用之前,要先对变量的类型进行检查。
示例
下面代码中的container变量,虽然类型不同,但是都可以使用下标[0]、[1]、[2],获取到值:
package main
import "fmt"
var container = []string{"ZERO", "ONE", "TWO"}
func main() {
container := map[int]string{0: "zero", 1: "one", 2: "two"}
fmt.Println(container[0], container[1], container[2])
}
如果,要判断变量的类型,就要使用“类型断言”表达式。
类型断言
语法:x.(T)
。
其中的x代表要被判断类型的那个值。T是要判断的类型。针对上面示例中的类型断言:
value, ok := interface{}(container).([]string)
上面是一条赋值语句,赋值符号的右边,就是一个类型断言表达式。
先把变量container的值转换为空接口的值interface{}(container)。然后再判断他的类型是否为后面.()中的类型。
有2个返回值,value和ok。ok是布尔类型,代码类型判断的结果:
如果是true,被判断的值自动转换为.()中的类型的值,并且赋值给value。
如果是false,value会赋值为nil,就是空。
不接收ok
这里ok也是可以没有的:
value := interface{}(container).([]string)
这样的话,如果类型不对,就是引发异常panic。
转为空接口的语法
在Go语言中,interface{}代表空接口。任何类型的值都可以很方便地被转换成空接口的值,语法:interface{}(x)。
一对不包裹任何东西的花括号,除了可以代表空的代码块之外,还可以用于表示不包含任何内容的数据结构(或者说数据类型)。
字面量
小括号中[]string是一个类型字面量。所谓类型字面量,就是用来表示数据类型本身的若干个字符。
比如:string是表示字符串类型的字面量,uint8是表示8位无符号整数类型的字面量。
优化示例代码
修改开始的示例,在打印前,先对变量的类型进行判断,只有map或切片类型才进行打印:
package main
import "fmt"
var container = []string{"ZERO", "ONE", "TWO"}
func main() {
container := map[int]string{0: "zero", 1: "one", 2: "two"}
// 打印之前先要做判断,只有map或者切片类型才能通过
_, ok1 := interface{}(container).([]string)
_, ok2 := interface{}(container).(map[int]string)
if !(ok1 || ok2) {
fmt.Printf("ERROR: 类型断言失败 %T\n", container)
return
}
fmt.Println(container[0], container[1], container[2])
}
另外还有一种switch语句的实现形式:
package main
import "fmt"
var container = []string{"ZERO", "ONE", "TWO"}
func main() {
container := map[int]string{0: "zero", 1: "one", 2: "two"}
switch v := interface{}(container).(type) {
case []string:
fmt.Println("[]string:", v)
case map[int]string:
fmt.Println("map[int]string:", v)
default:
fmt.Printf("ERROR: 类型断言失败 %T\n", container)
return
}
}
类型转换的坑
类型转换表达式的语法:T(x)
。
其中的x可以是一个变量,也可以是一个代表值的字面量(比如1.23和struct{}),还可以是一个表达式。如果是表达式,表达式的结果只能是一个值。
x被叫做源值,它的类型就是源类型。T代表的类型是目标类型。
数值类型间互转
对于整数类型值、整数常量之间的类型转换,原则上只要源值在目标类型的可表示范围内就是合法的。
上面说的只是语法上合法,但是转换后的结果可能是可坑。比如,如果源整数类型的可表示范围大,而目标类型的可表示范围小:
package main
import "fmt"
func main() {
var srcInt = int16(-255) // 1111111100000001
dstInt := int8(srcInt) // 00000001,简单粗暴的截掉最前面的8位
fmt.Println(srcInt, dstInt)
}
/* 执行结果
PS H:\Go\src\Go36\article06\example04> go run main.go
-255 1
PS H:\Go\src\Go36\article06\example04>
*/
在计算机系统中,数值一律用补码来表示和存储。原因在于,使用补码,可以将符号位和数值域统一处理;同时,加法和减法也可以统一处理。补码就是原码的各位求反再加1。比如-255:
原码: 1000 0000 1111 1111
反码: 1111 1111 0000 0000 最高位是符号位,不反转。
补码: 1111 1111 0000 0001
类型转换的很简单粗暴,直接把最高的8位截掉,并不处理符号位,结果就是0000 0001,所以转换后的值就变成1了。
浮点类型转换
如果把浮点数转换为整数,则小数部分会被全部截掉:
package main
import "fmt"
func main() {
var x = float64(1.9999999)
y := int(x)
fmt.Println(x, y)
}
/* 执行结果
PS H:\Go\src\Go36\article06\example05> go run main.go
1.9999999 1
PS H:\Go\src\Go36\article06\example05>
*/
整数转字符串
直接把一个整数值转换为一个string类型的值是可行的。但是,被转换的整数值应该是一个有效的Unicode码点,否则转换的结果将会是"�"。字符’�’的Unicode码点是U+FFFD。它是Unicode标准中定义的Replacement Character,专用于替换那些未知的、不被认可的以及无法展示的字符。无效的码点有很多,如果自己要搞一个测试,那么就用-1吧:
package main
import "fmt"
func main() {
fmt.Println(string(-1)) // 一个无效的Unicode码点
fmt.Println(string(65)) // 字符A
fmt.Println(string(24464)) // 中文
}
字符串与切片
一个值在从string类型转为[]byte类型时,其中UTF-8编码的字符串会被拆分成零散、独立的字节。这样只有ASCII码的那部分字符是一个字节代码一个字符的。而其他字符,比如中文(UTF-8里中文字符用3个字节表示),会被拆开成3个字节。而且由于UTF-8的长度是可变的,这样还要想办法判断那几个字节应该是一个字符。
可以转为[]rune类型,这样转换时,每个字符会被拆开成一个个的Unicode字符。
package main
import "fmt"
func main() {
s := "你好"
s1 := []byte(s)
fmt.Println(s1)
s2 := []rune(s)
fmt.Println(s2)
for _, v := range(s1) {
fmt.Print(string(v)) // 乱码
}
fmt.Println()
for _, v := range(s2) {
fmt.Print(string(v))
}
fmt.Println()
}
/* 执行结果
PS H:\Go\src\Go36\article06\example07> go run main.go
[228 189 160 229 165 189]
[20320 22909]
ä½ å¥½
你好
PS H:\Go\src\Go36\article06\example07>
*/
别名类型 和 潜在类型
别名类型声明与类型再定义之间的区别,以及由此带来的它们的值在类型转换、判等、比较和赋值操作方面的不同。
别名类型
可以用关键字type声明自定义的各种类型。比如,可以声明别名类型:
type MyString = string
上面的声明语句表示,MyString是string类型的别名类型。别名类型与其源类型除了在名称上以外,都是完全相同的。别名类型主要是为了代码重构而存在的。
Go语言的基本类型中就存在两个别名类型。byte是uint8的别名类型,而rune是int32的别名类型。
潜在类型
另外一种声明:
type MyString2 string
// 注意,这里没有等号
这种方式也可以被叫做对类型的再定义。这里MyString2是一个新的类型,和string是不同的类型。string可以被称为MyString2的潜在类型。
潜在类型相同的不同类型的值之间是可以进行类型转换的。因此,MyString2类型的值与string类型的值可以使用类型转换表达式进行互转。
但是,[]MyStrings 和 []string 是不同的潜在类型,不能做类型转换。
另外,即使是相同的潜在类型,也不能进行判等或比较,变量之间不能赋值。