初始化
常量初始化
**不管是全局常量还是在函数中声明的常量,常量是在编译器创建的,而且只能是数字类型、字符、字符串或布尔类型。**因为编译时的限制,定义常量的表达式必须是常量表达式,这样编译器在编译时才能计算常量的值,而不能是一些例如函数计算值,因为调用函数是在代码运行阶段才执行的。
变量初始化
变量的初始化可以是一般的表达式,因为这是在运行时才初始化的。
包的初始化首先是解决包级变量的依赖顺序,然后按照包级变量声明出现的顺序依次初始化:
var a = b + c // a 第三个初始化, 为 3
var b = f() // b 第二个初始化, 为 2, 通过调用 f (依赖c)
var c = 1 // c 第一个初始化, 为 1
func f() int { return c + 1 }
所以在Go语言里,函数的声明顺序,变量的声明顺序都是无所谓的,没有强制要求先依赖的变量或者函数需要先进行声明和定义,Go语言在编译时会自动检查依赖关系,在依赖关系的基础上,然后再按照代码的声明顺序进行初始化。
init函数
init函数的主要作用:1)初始化不能采用初始化表达式初始化的变量。2)程序运行前的注册。3)实现sync.Once功能。(备注:init函数没有输入参数、返回值。)
例如,可用于执行前对系统环境状态进行判断,决定是否继续执行程序:
var (
home = os.Getenv("HOME")
user = os.Getenv("USER")
gopath = os.Getenv("GOPATH")
)
func init() {
if user == "" {
log.Fatal("$USER not set")
}
if home == "" {
home = "/home/" + user
}
if gopath == "" {
gopath = home + "/go"
}
// gopath may be overridden by --gopath flag on command line.
flag.StringVar(&gopath, "gopath", gopath, "override default GOPATH")
}
init函数是在main函数执行前先执行的,总体的执行顺序为:
各变量的声明和初始化--》init函数--》main函数
一个包里可以有多个init函数,一个文件里也可以同时拥有多个init函数,在同一个文件中的多个init函数按照代码编写的顺序执行;而同一个包里多个文件都有init函数的情况下,则按照文件名字符串的比较后,从小到大执行,例如先执行a.go里的init,再执行b.go里的init函数。
导入的包会自动执行init函数,执行的顺序和包的依赖关系有关:
初始化工作是自下而上进行的,main包最后被初始化。以这种方式,可以确保在main函数执行之前,所有依赖的包都已经完成初始化工作了。
接口和其他类型
接口的类型断言如果失败,则返回的变量为该类型的零值:
val,ok:=myinter.(string)// 如果接口现在承载的不是string,则ok为false,val为字符串的空值,为空字符串
定义函数类型别名
func main(){
type HandlerFunc func(int)
f := HandlerFunc(my_func)
f(5)
}
func my_func(int) {
fmt.Println("my_func")
}
//output
my_func
下划线
下划线的作用是因为Go语言如果变量或者导入的包没有用到,就无法通过编译,或者我们有时候确实不需要某个函数的返回值或某个对象,可以使用下划线_来忽略这个对象或者包,以通过编译。
因为如果没有用到的包我们又import了,若这种语法合法,那么编译器的工作量会很大,而且会多余的将这些包一起编译;没用到的变量同理,变量需要去计算它的值,但又后续用不到它,就是浪费资源。
使用下划线_就好比在linux系统下将东西写道/dev/null文件中,都是忽略不要的。
常见的用法:
_,ret:=my_func()
//忽略my_func函数的第一个返回值
也可以,但更多的是用上面的方式,下面累赘:
val,ret:=my_func()
_=val
如果导入某个包,但是又忽略不用,却要用到它的一些init函数,则可以:
import _ fmt
//忽略fmt,在代码中无法使用fmt包
//This form of import makes clear that the package is being imported for its side effects, because there is no other possible use of the package: in this file, it doesn't have a name.
也可通过下面的方式对来消除编译器的报错提醒:
import(
"fmt"
"time"
)
var _ = fmt.Printf //这里的Printf是个函数,相当于把这个函数的句柄赋给了_
var _ time.Duration //这里的Duration是个类型,这个语句就是普通的变量声明语句
//使用这种方法,就可以避免编译器报错,虽然这里用了_,但是Printf、Duration可以在代码中使用
在Go语言中,很多类型实现了接口,就可以通过接口变量来调用这些类型的实现的接口方法,在编译期间,编译器可以检查大多数的类型是否实现了接口,以判断是否通过编译,但也有些必须在运行时才能判断(例如下面的运行时类型判断):
if _, ok := val.(json.Marshaler); ok {
fmt.Printf("value %v of type %T implements json.Marshaler\n", val, val)
}
这种情况编译器是无能为力的,如果运行时才发现类型没有实现接口方法,但又调用了接口的方法,则会出错。
可以通过下划线_来实现一个检测,下面Marshaler是接口,右侧则是一个类型,下面这个语句可以实现在编译阶段判断(*RawMessage)类型有没有实现接口:
var _ json.Marshaler = (*RawMessage)(nil)
虽然通过这种方法可以在编译期间判断某个类型是否实现了接口,但没必要对每个类型都这么写,只要在用的地方做好类型断言就好了。
嵌套
在Go语言中可以嵌套interface,也可以嵌套struct
若嵌套的struct A 实现了接口inter,则嵌套后,structB也实现了接口inter。但是如果在调用A实现的接口方法或者A自身的方法时,接收者都是A而不是B:
type A struct{
}
type B struct{
A
}
func(a A) print(){
fmt.Println("A's method")
}
如果嵌套的时候,还同时指定了这个嵌套类A的名字,此时则把这个看成一个成员:
type B struct{
a A
}
这时候就单纯理解为structB有个类型A的成员就行了,此时如果想要访问A的方法,只能通过普通的成员访问来实现,所以此时就不能说structB也实现了接口inter,要注意这里的成员和单纯嵌套(必须是匿名成员)的区别:
b:=B{}
b.a.AMethod()
因此,使用structA的指针类型嵌套也是可以的,也相当于实现了接口inter,:
type B struct{
*A
}
对于嵌套时,内部类型同名与外层类型种同名的方法或者变量,如果直接访问,则访问的是外层的数据对象:
type A struct{
hello string
}
type B struct{
A
hello string
}
func main(){
b:=B{}
fmt.Println(b.hello) //访问的是B中的hello,而不是A中的hello
}
struct中嵌套的类型名和别的类型后者成员不能重名,当然也有例外情况,例外情况现在没搞懂:
type st2 struct{
*st1
st1 string
}//这是不允许的,编译过不了
Second, if the same name appears at the same nesting level, it is usually an error; it would be erroneous to embed log.Logger
if the Job
struct contained another field or method called Logger
. However, if the duplicate name is never mentioned in the program outside the type definition, it is OK. This qualification provides some protection against changes made to types embedded from outside; there is no problem if a field is added that conflicts with another field in another subtype if neither field is ever used.
作用域
内部作用域可以访问外部作用域中的对象,反之则不行;可以存在同名的对象,只要他们的作用域不同即可。
一个程序可能包含多个同名的声明,只要它们在不同的词法域就没有关系。例如,你可以声明一个局部变量,和包级的变量同名。或者是像2.3.3节的例子那样,你可以将一个函数参数的名字声明为new,虽然内置的new是全局作用域的。但是物极必反,如果滥用不同词法域可重名的特性的话,可能导致程序很难阅读。
当编译器遇到一个名字引用时,它会对其定义进行查找,查找过程从最内层的词法域向全局的作用域进行。如果查找失败,则报告“未声明的名字”这样的错误。如果该名字在内部和外部的块分别声明过,则内部块的声明首先被找到。在这种情况下,内部声明屏蔽了外部同名的声明,让外部的声明的名字无法被访问:
func f() {}
var g = "g"
func main() {
f := "f"
fmt.Println(f) // "f"; local var f shadows package-level func f
fmt.Println(g) // "g"; package-level var
fmt.Println(h) // compile error: undefined: h
}
if和switch语句也会在条件部分创建隐式词法域,还有它们对应的执行体词法域。下面的if-else测试链演示了x和y的有效作用域范围:
if x := f(); x == 0 {
fmt.Println(x)
} else if y := g(x); x == y {
fmt.Println(x, y)
} else {
fmt.Println(x, y)
}
fmt.Println(x, y) // compile error: x and y are not visible here
第二个if语句嵌套在第一个内部(除了第一个if,其他if分支都是同级的作用域,嵌套在第一个if作用域下),因此第一个if语句条件初始化词法域声明的变量在第二个if中也可以访问。switch语句的每个分支也有类似的词法域规则:条件部分为一个隐式词法域,然后是每个分支的词法域。
type 类型名 底层数据结构
这个需要注意的是,即使底层数据结构是相同的,不同的类型名,go当成不同的类型,所以即使底层数据结构相同,想在两种类型名之间进行运算,需要进行强制转换。
通过这种方式生成新的类型名,底层数据结构支持的运算符,新的类型名也是支持的。
字符串需要注意的点
更详细的教程见http://books.studygolang.com/gopl-zh/ch3/ch3-05.html
字符串是一个不可变对象,不像C++中的string可以append,go的字符串是不可变的,但是可以通过重新赋值改变对象的值:
s:="hello world"
s[1]='L' //error 不可变对象,不可修改
s="new hello world" //correct 可以重新赋值
不变性意味着如果两个字符串共享相同的底层数据的话也是安全的,这使得复制任何长度的字符串代价是低廉的。同样,一个字符串s和对应的子字符串切片s[7:]的操作也可以安全地共享相同的内存,因此字符串切片操作代价也是低廉的。在这两种情况下都没有必要分配新的内存。 这种情况就和切片相似,两个对象指向的是同一个底层数据,并没有开辟新的内存。

对字符串使用len函数,返回的是底层数据的字节数,而不是字符数(例如一个中文字计算的是3)。
对一个字符串对象,可以像切片那样,获取部分子串,以形成一个新的字符串(左闭右开):
s:="hello,world"
s1:=s[:5]
s2:=s[5:]
s3:=s[0:5]
字符串的比较运算是按照逐字节比较的,所以必须是在同一字符集编码下才有意义。
字符串的转义符:
\a 响铃
\b 退格
\f 换页
\n 换行
\r 回车
\t 制表符
\v 垂直制表符
\' 单引号(只用在 '\'' 形式的rune符号面值中)
\" 双引号(只用在 "..." 形式的字符串面值中)
\\ 反斜杠
可以通过十六进制和八进制转移值来指定字符串每个字节的值, 一个十六进制的转义形式是\xhh
,其中两个h表示十六进制数字(大写或小写都可以)。一个八进制转义形式是\ooo
,包含三个八进制的o数字(0到7),但是不能超过\377
(译注:对应一个字节的范围,十进制为255)。 注意在十六进制和八进制转义中,都是数字,例如\x22,\035。
原生字符串,使用反引号``括起来,此时,括起来的内容和字面值一样,可以理解成你写了什么,这个字符串就是什么内容,如果需要在原生字符串中出现反引号,可以通过+"`"连接字符串。 唯一的特殊处理是会删除回车以保证在所有平台上的值都是一样的,包括那些把回车也放入文本文件的系统 。
s := `
hello
world
nihao
\x23
`
fmt.Println(s + "`")
//output
hello
world
nihao
\x23
`
**原生字符串面值用于编写正则表达式会很方便,因为正则表达式往往会包含很多反斜杠。**原生字符串面值同时被广泛应用于HTML模板、JSON面值、命令行提示信息以及那些需要扩展到多行的场景。
Unicode编码
使用Unicode( http://unicode.org ),它收集了这个世界上所有的符号系统,包括重音符号和其它变音符号,制表符和回车符,还有很多神秘的符号,每个符号都分配一个唯一的Unicode码点,Unicode码点对应Go语言中的rune整数类型(译注:rune是int32等价类型)。
我们可以将一个符文序列表示为一个int32序列。这种编码方式叫UTF-32或UCS-4,每个Unicode码点都使用同样大小的32bit来表示。这种方式比较简单统一,但是它会浪费很多存储空间,因为大多数计算机可读的文本是ASCII字符,本来每个ASCII字符只需要8bit或1字节就能表示。而且即使是常用的字符也远少于65,536个,也就是说用16bit编码方式就能表达常用字符。但是,还有其它更好的编码方法吗? ==》UTF8
UTF8
UTF8是一个将Unicode码点编码为字节序列的变长编码。
UTF8编码使用1到4个字节来表示每个Unicode码点,ASCII部分字符只使用1个字节,常用字符部分使用2或3个字节表示。每个符号编码后第一个字节的高端bit位用于表示编码总共有多少个字节。如果第一个字节的高端bit为0,则表示对应7bit的ASCII字符,ASCII字符每个字符依然是一个字节,和传统的ASCII编码兼容。如果第一个字节的高端bit是110,则说明需要2个字节;后续的每个高端bit都以10开头。更大的Unicode码点也是采用类似的策略处理。 前四位高端位的1的个数代表某个字符编码的字节总数。
0xxxxxxx runes 0-127 (ASCII)
110xxxxx 10xxxxxx 128-2047 (values <128 unused)
1110xxxx 10xxxxxx 10xxxxxx 2048-65535 (values <2048 unused)
11110xxx 10xxxxxx 10xxxxxx 10xxxxxx 65536-0x10ffff (other values unused)
"世界"
"\xe4\xb8\x96\xe7\x95\x8c"
"\u4e16\u754c"
"\U00004e16\U0000754c"
上述的三种写法得到的结果都是“世界”,首先第一行其实就是utf8编码值对应的十六进制值,这个很好理解。
第二行则是utf8的转义值,这里详细解释下,以“世”为例,其对应的二进制值为:
11100100 10111000 10010110,根据编码方式(如上所示),去掉第一个字节的前四位,第二和第三字节的前两位,然后再拼到一起,得到:0100 1110 0001 0110 对应的就是\u4e16
再举一个unicode码点为4个字节的例子,码点值为65536时,对应有四个字节:
s := string(65536)
fmt.Printf("%x\n", s)
fmt.Println(len(s))
fmt.Println("\U00010000" == s)
//output
f0908080
4
true
s对应的十六进制为f0908080,则相应的二进制为:
11110000 10010000 10000000 10000000
一样,去掉高位11110和后面字节的10后:
000 010000 000000 000000 因为位数不足8的整数倍(也即不是字节的倍数),所以前面在最前面补0:
0000 0001 0000 0000 0000 0000 因此对应的utf的转义值就是\U00010000,所以上述代码中的相等比较是true
因为不同字符对应的编码字节数不一定相等,所以变长的编码无法直接通过索引来访问第n个字符。
没有任何字符的编码是其它字符编码的子串,或是其它编码序列的字串,因此搜索一个字符时只要搜索它的字节编码序列即可,不用担心前后的上下文会对搜索结果产生干扰。 (每个字符的编码都不相同,都不是各自的子串,匹配搜索的相率更高,无干扰。)
Go语言的源文件采用UTF8编码,并且Go语言处理UTF8编码的文本也很出色。
有很多Unicode字符很难直接从键盘输入,并且还有很多字符有着相似的结构;有一些甚至是不可见的字符(译注:中文和日文就有很多相似但不同的字)。Go语言字符串面值中的Unicode转义字符让我们可以通过Unicode码点输入特殊的字符。 有两种形式:\uhhhh
对应16bit的码点值,\Uhhhhhhhh
对应32bit的码点值,其中h是一个十六进制数字;一般很少需要使用32bit的形式。每一个对应码点的UTF8编码。
得益于UTF8编码优良的设计,诸多字符串操作都不需要解码操作。 例如查找前缀,由于均由utf8编码,不用担心匹配的子串和原字符串存在编码上的区别,可以直接进行比对:
func HasPrefix(s, prefix string) bool {
return len(s) >= len(prefix) && s[:len(prefix)] == prefix
}
Go语言的range循环,在遍历一个utf8字符串时,会进行隐式的解码,遍历的是每个unicode字符,并不是遍历每个字节,得到的是字符的Unicode码点值:
us := "hello, 中国"
for _, val := range us {
fmt.Printf("%q\t%d\n", val, val)
}
//output
'h' 104
'e' 101
'l' 108
'l' 108
'o' 111
',' 44
' ' 32
'中' 20013
'国' 22269
**文本字符串采用UTF8编码只是一种惯例,**但是对于循环的真正字符串并不是一个惯例,这是正确的。如果用于循环的字符串只是一个普通的二进制数据,或者是含有错误编码的UTF8数据,将会发生什么呢?
每一个UTF8字符解码,不管是显式地调用utf8.DecodeRuneInString解码或是在range循环中隐式地解码,如果遇到一个错误的UTF8编码输入,将生成一个特别的Unicode字符\uFFFD
,在印刷中这个符号通常是一个黑色六角或钻石形状,里面包含一个白色的问号"?"。当程序遇到这样的一个字符,通常是一个危险信号,说明输入并不是一个完美没有错误的UTF8字符串。
utf8编码的字符串是可以和rune数据或切片进行互相转换的:
一个[]byte(s)转换是分配了一个新的字节数组用于保存字符串数据的拷贝,然后引用这个底层的字节数组。
us := "hello, 中国"
rs := []rune(us)
nus:=string(rs)
将一个整数转型为字符串意思是生成以只包含对应Unicode码点字符的UTF8字符串,由于utf8兼容ascii,所以使用ascii码对应的值来转换生成字符串是对的:
fmt.Println(string(65)) // "A", not "65"
fmt.Println(string(0x4eac)) // "京"
如果对应码点的字符是无效的,则用\uFFFD
无效字符作为替换:
fmt.Println(string(1234567)) // "?"
字符串和数字的转换
1.使用fmt.Sprintf
x:=123
y:=fmt.Sprintf("%d",x)
2.使用strconv包的Itoa函数
x:=123
y:=strconv.Itoa(x)
3.将数字格式化为指定进制的字符串值
x:=123
y:=strconv.FormatInt(int64(x),2) //二进制 "1111011"
4.将字符串转为数字, 以使用strconv包的Atoi或ParseInt函数,还有用于解析无符号整数的ParseUint函数 :
x, err := strconv.Atoi("123") // x is an int
y, err := strconv.ParseInt("123", 10, 64) // base 10, up to 64 bits
ParseInt函数的第三个参数是用于指定整型数的大小;例如16表示int16,0则表示int。在任何情况下,返回的结果y总是int64类型,你可以通过强制类型转换将它转为更小的整数类型。
标准库中有四个包对字符串处理尤为重要:bytes、strings、strconv和unicode包。strings包提供了许多如字符串的查询、替换、比较、截断、拆分和合并等功能。
bytes包也提供了很多类似功能的函数,但是针对和字符串有着相同结构的[]byte类型。因为字符串是只读的,因此逐步构建字符串会导致很多分配和复制。在这种情况下,使用bytes.Buffer类型将会更有效,稍后我们将展示。
strconv包提供了布尔型、整型数、浮点数和对应字符串的相互转换,还提供了双引号转义相关的转换。
unicode包提供了IsDigit、IsLetter、IsUpper和IsLower等类似功能,它们用于给字符分类。每个函数有一个单一的rune类型的参数,然后返回一个布尔值。而像ToUpper和ToLower之类的转换函数将用于rune字符的大小写转换。所有的这些函数都是遵循Unicode标准定义的字母、数字等分类规范。strings包也有类似的函数,它们是ToUpper和ToLower,将原始字符串的每个字符都做相应的转换,然后返回新的字符串。