初始化

常量初始化

**不管是全局常量还是在函数中声明的常量,常量是在编译器创建的,而且只能是数字类型、字符、字符串或布尔类型。**因为编译时的限制,定义常量的表达式必须是常量表达式,这样编译器在编译时才能计算常量的值,而不能是一些例如函数计算值,因为调用函数是在代码运行阶段才执行的。

变量初始化

变量的初始化可以是一般的表达式,因为这是在运行时才初始化的。

包的初始化首先是解决包级变量的依赖顺序,然后按照包级变量声明出现的顺序依次初始化

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函数执行之前,所有依赖的包都已经完成初始化工作了。
image

接口和其他类型

接口的类型断言如果失败,则返回的变量为该类型的零值:

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:]的操作也可以安全地共享相同的内存,因此字符串切片操作代价也是低廉的。在这两种情况下都没有必要分配新的内存。 这种情况就和切片相似,两个对象指向的是同一个底层数据,并没有开辟新的内存。

image

对字符串使用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,将原始字符串的每个字符都做相应的转换,然后返回新的字符串。