4.1 文件名、关键词与标识符
Go源文件是以.go为后缀,文件名以字母和下划线组成,不包含空格或特殊字符。
Go的源文件是没有限制大小的。
Go的标识符是区分大小写的。
有效的标识符是字母、数字和下划线组成,不能以数字开头。
下划线_是一个特殊的标识符,它有一个特殊的用途,叫做空白标识符,是一个只写变量,通常用来接收不处理的返回值,因为Go中的变量声明了就一定要使用的,如果用不到,就可以将返回值往下划线里写。
25个关键词或保留字:
| break | default | func | interface | select |
|---|---|---|---|---|
| case | defer | go | map | struct |
| chan | else | goto | package | switch |
| const | fallthrough | if | range | type |
| continue | for | import | return | var |
36个预定义标识符:
| append | bool | byte | cap | close | complex |
|---|---|---|---|---|---|
| complex64 | complex128 | uint16 | copy | false | float32 |
| float64 | imag | int | int8 | int16 | unit32 |
| int32 | int64 | iota | len | make | new |
| nil | panic | unit64 | println | real | |
| recover | string | true | uint | uint8 | uintptr |
如果要把多行语句写在同一行,则需要使用分号;隔开,但不鼓励。
4.2 基本结构和要素
4.2.1 包的概念、导入与可见性
类似于其他语言的类库或命名空间,每个Go文件都属于一个包,比如main包,一个包可以包含很多个go源文件。
一个包不是一个文件夹,但是通常情况下,我们都会以子文件夹的命名作为包名,这样在导入的时候很方便。
通常,我们将main.go程序入口文件单独放项目根目录,其他的文件会放在子文件夹或者或其他包中。
在导入子包的时候,需要连父目录也导入,比如下面的例子:
main.go单独放在项目TEST目录下,其他的业务文件dd.go放在子文件夹testdir下,dd.go:
package testdirimport "fmt"func Test() {fmt.Println("这里是testdir包")}
main.go:
package mainimport ("test/testdir" //这里需要连父目录test也导入,而不是testdir)func main() {testdir.Test()}
dd.go属于testdir包,也是目录名,但是这不是强制的,dd.go可以改包名为任意的,如test123包,那么在mian.go导入的时候需要这样:
//dd.gopackage test123import "fmt"func Test() {fmt.Println("这里是testdir包")}//main.gopackage mainimport (test123 "test/testdir" //这里需要连父目录test也导入,而不是testdir)func main() {test123.Test()}
如果一个包进行了更改或重新编译,那么导入该包的程序都要重新编译。
一个包是编译的一个单元,所以通常在开发中,一个目录只包含一个包。
依赖包之间的编译顺序:比如a.go依赖b.go,b.go又依赖c.go,那么
- 编译c.go,b.go,然后是a.go- 为了编译a.go,编译器读取的是b.go,而不是c.go
这种机制可以显著提高编译速度,至于为什么?不知道。
包的查找规则:如果包名不是以 . 或 / 开头,如 “fmt” 或者 “container/list”,则 Go 会在全局文件进行查找;如果包名以 ./ 开头,则 Go 会在相对目录中查找;如果包名以 / 开头(在 Windows 下也可以这样使用),则会在系统的绝对路径中查找。
可见性规则,相当于其他语言的公有属性,私有属性的实现。
当标识符(变量名,函数名,类型等)是以大写字母开头时,此标识符的对象就可以被外部使用,前提是导入了这个包;如果是小写字母开头,那么就是不可见的,私有的。
4.2.2 注释
- 单行注释//
- 多行注释/ /
4.2.3 函数
一个函数是可以完成某个功能的代码块。Go中的函数可以有多少参数,也可以返回多个参数。
package mainimport "fmt"func add(a int, b int) (int, int) { //加法函数,多参数,多返回值return 1, a + b}func main() {_, sum := add(5, 6) //舍弃返回值1fmt.Println(sum)}
Go中的左花括号{必须紧跟着函数的第一行。花括号的使用规则是通用的,比如if,struct。
4.2.4 程序执行
程序的执行顺序:
- 按顺序导入所有被main包引用的其他包,然后在每个包中执行如下流程:- 如果该包又导入了其他的包,则从第一步开始递归执行,每个包只会被导入一次。- 以相反的顺序在每个包中初始化常量和变量,如果该包有init()函数的话,则调用该函数。- 在完成之后,main也执行相同的流程,最后调用main()开始执行程序。
4.2.5 类型
- 基本类型:int,float,bool,string。
- 复合类型:struct,array,slice,map,channel,interface。
函数也可以是一个类型,所以函数也可以作为返回值。
当使用var关键词来声明类型时,会自动赋值各自类型的零值。
类型别名,使用type关键词来定义:
type myint int,即定义一个int类型的myint类型,但是这里并不是真正意义的别名,是不相等的。不知道实际用处。
4.2.6 类型转换
Go不存在隐式转换。在类型转换中,类型可以看作是一种函数。
在同种类型中(值类型,字符类型),类型转换只要加上类型名即可,如int转int64:int64(a);string转[]byte,[]byte(a)。
package mainimport ("fmt""reflect")func main() {var a stringa = "1"fmt.Println(a)fmt.Println([]byte(a))fmt.Println(reflect.TypeOf([]byte(a))) //利用发射来查看变量的类型}
但是,如果是int和string之间的转换,需要用到strcov内置函数。
package mainimport ("fmt""reflect""strconv")func main() {var a intvar b stringa = 1b = "2"c := strconv.Itoa(a) //int转stringd, _ := strconv.Atoi(b) //string转intfmt.Println(reflect.TypeOf(c))fmt.Println(reflect.TypeOf(d))}
4.3 常量
常量使用const关键词定义,定义一些不会改变的量。
常量只可以是数字型,布尔型,字符串型。
- 显示定义:const a string = "1"- 隐式定义:const b = "2"
习惯,也推荐显示定义,隐式定义比那一起会自动推断它的类型。
常量的值必须是编译时能够确定的,所以函数返回值不能赋值给常量,因为函数要经过计算的,但是内置的函数可以,比如len()。
常量值如果太长,可以使用\来分行。
常量也允许并行定义,并行赋值:const a,b,c = 1,2,”d”。
iota是一个用来枚举的特殊字符。每经过一行就自动+1,遇到const就会置0:
package mainfunc main() {const (a = iota //a = 0b //b = 1c = iota //c = 2d = iota + 5 //d = 8)const e = iota //d = 0}
4.4 变量
4.4.1 简介
变量的声明一般使用var关键词:var a,b string,也可以使用简短声明:a := 123,简短声明只能在函数体内用。
变量声明之后,系统自动赋予该类型的零值,int为0,float为0.0,bool为false,string为空字符串,指针为nil。所有的内存空间在Go中都是经过初始化的。
变量的作用域:
- 全局变量:声明在函数体外。- 局部变量:声明在一对花括号中,可能是函数,可能是if for等结构。只能作用在这对花括号内。
下面的程序,会出错:
package mainimport "fmt"func main() {a := 1if a == 1 {b := 2 //只作用在if语句中fmt.Println(b)}fmt.Println(b) //未声明}
因为全局变量可以作用在包内的任何地方,所有可以在函数内修改全局变量的值,不像其他语言需要类似于global关键词来引用。
4.4.2 值类型和引用类型
int,float,bool,string,array这些都属于值类型,值类型的变量存储的是内存中的值。
值类型的变量在赋值给另一个变量时,是拷贝操作。
可以通过&a来获取变量a的内存地址。
值类型的变量的值存储在栈中。
slice,map,channel,interface,指针,函数这些属于引用类型,引用类型的变量存储的是值所在的内存地址,或者内存地址中第一个字所有的位置。每个字都指示了下一个字所在的内存地址。
引用类型的变量在赋值给另一个变量时,是传递内存地址,不是值,所以都有有关的变量的更改都会收到同样的影响。
引用类型的变量存储在堆中,以便进行垃圾回收,且堆比栈内存空间更大。
4.4.3 打印
主要是fmt包中的打印函数:
- fmt.Printf:格式化打印:fmt.Printf("a 的值为 %d", a)- fmt.Sprintf:也是格式化打印,但把字符串返回:b := fmt.Sprintf("b 的值为 %d", a)- fmt.Print:标准格式化打印,自动使用%v。- fmt.Println:比fmt.Print多了一个换行。
在格式化输出中:
- %v:按值的本来值输出。- %+v:在 %v 基础上,对结构体字段名和值进行展开。- %#v:输出 Go 语言语法格式的值。- %T:输出 Go 语言语法格式的类型和值。- %%:输出 % 本体。- %b:整型以二进制方式显示。- %o:整型以八进制方式显示。- %d:整型以十进制方式显示。- %x:整型以十六进制方式显示。- %X:整型以十六进制、字母大写方式显示。- %U:Unicode 字符。- %f:浮点数。- %p:指针,十六进制方式显示。
4.4.4 init函数
变量除了可以在全局声明中初始化,也可以在init函数内初始化。
init函数不能被人为调用。
init函数执行优先级比main函数高。
一个源文件可以有多个init函数。从上到下顺序执行。
如果一个包中多个源文件都有init函数,则执行顺序不明。
4.5 基本类型和运算符
4.5.1 布尔型
Go是强类型,两个值之间的比较必须保证都是相同的类型。
Go中的&&和||具有快捷性质的运算符,当运算符左边的表达式的值已经可以满足整个表达式的值的时候,右边的表达式就不执行了,所以在编码中,如果有多个判断条件,应当把能快速得出结论的,较为简单计算的放左边。
在格式化输出中,可以使用%t输出布尔型的变量。
4.5.2 数字型
即整型int和浮点型float,复数型。
数字型的位运算,采用补码的形式。
int,uint,uintptr这些是基于架构的类型,也就是在不同的操作系统,使用的字节不同。
- ·int 和 uint 在 32 位操作系统上,它们均使用 32 位(4 个字节),在 64 位操作系统上,它们均使用 64位(8 个字节)。
- uintptr 的长度被设定为足够存放一个指针即可。
Go没有float类型,只有float32和float64。float32精确到小数点后7位,float64是精确到后15位。在比较的时候,需要特别注意精度丢失,应尽量使用float64。
各类型的范围:
- 整数:
- int8(-128 -> 127)
- int16(-32768 -> 32767)
- int32(-2,147,483,648 -> 2,147,483,647)
- int64(-9,223,372,036,854,775,808 -> 9,223,372,036,854,775,807)
- 无符号整数:
- uint8(0 -> 255)
- uint16(0 -> 65,535)
- uint32(0 -> 4,294,967,295)
- uint64(0 -> 18,446,744,073,709,551,615)
- 浮点型(IEEE-754 标准):
- float32(+- 1e-45 -> +- 3.4 * 1e38)
- float64(+- 5 1e-324 -> 107 1e308)
添加前缀0表示8进制,如077,添加前缀0x表示16进制,如0x77。
Go有两个复数类型:complex64(32位实数和虚数),complex128(64位实数和虚数)。
var c1 complex = 1+2i
使用real(c1)可以获取实数部分,使用imag(c1)可以获取虚数部分,都是浮点型。
可以使用%v打印复数。
4.5.3 字符型
严格来说,这并不是Go的一个类型,字符只是整数的特殊用例。
byte是uint8的别名。
rune是int32的别名。
当我们定义一个单引号的字符时,实际上它时int32类型的:a := ‘1’ 。
var ch int = '\u0041'var ch2 int = '\u03B2'var ch3 int = '\U00101234'fmt.Printf("%d - %d - %d\n", ch, ch2, ch3) // integerfmt.Printf("%c - %c - %c\n", ch, ch2, ch3) // characterfmt.Printf("%X - %X - %X\n", ch, ch2, ch3) // UTF-8 bytesfmt.Printf("%U - %U - %U", ch, ch2, ch3) // UTF-8 code point
字符串是值类型,是不可变的,可以看作是一种定长数组,数组也是不可变的。
字符串是可以索引的,迭代的。
字符串分为解释字符串和非解释字符串。
- 解释字符串:斜杠\是转义字符串,能产生一些特殊的组合。- \n:换行符- \r:回车符- \t:tab 键- \u 或 \U:Unicode 字符- \\:反斜杠自身- 非解释字符串:反斜杠不起作用,所有字符都原样输出,同时支持多行字符。
字符串之间可以使用+号拼接,也可以使用strings.join()拼接,或者bytes.buffer。
4.5.4 指针型
程序在内存中存储值,每个内存块都有一个地址,成为内存地址,用十六进制表示。
变量直接指向值,而指针直接指向值的内存地址。
取值符用&,获取一个值的内存地址。
声明指针:var ptr *int,是一个int类型的指针。
package mainimport "fmt"func main() {i := 123var p *int //声明一个指针p = &i //取得i的地址fmt.Println("i的值", i)fmt.Println("p的值", p)fmt.Println("获取i的值", *p) //*p = *(&i) = i}
指针是指针地址,所以是引用类型,在传递给函数参数时,是引用传递。
Go的指针不能运算,即指针不能移动,p++,p+2等都是不允许的。
4.5.4 位运算
%b表示位的格式化标识符。
二元运算符:
- 按位与&:均相同为1,否则为0。- 按位或|:均为0,才为0,否则为1。- 按位异或^:不同为1,相同为0。- 按位清除:将指定位置上的值设置为0。
一元运算符:
- 按位补足^:和异或公用一个操作符。对于无符号值使用 “全部位设置为 1” 的规则,对于有符号 值,将该值与-1异或。^2 = 2^-1 = -3- 位左移<<n:相当于乘2的n次方。- 位右移>>n:相当于除2的n次方。
package mainimport "fmt"func main() {var a int = 1 //0000 0001var b int = 3 //0000 0011var c int = a & b //按位与var d int = a | b //按位或var e int = a ^ b //按位异或var f int = b &^ a //按位清除,把b在a的位置的数置0print(c) //0000 0001 = 1print(d) //0000 0011 = 3print(e) //0000 0010 = 2print(f) //0000 0010 = 2fmt.Printf("%08b", ^2) //-0000 0011print(1 << 10) //左移10位 = 1024print(1 >> 10) //右移10位 = 0}
格式化说明符 %c 用于表示字符;当和字符配合使用时,%v 或 %d 会输出用于表示该字符的整数;%U 输出格式为 U+hhhh 的字符串
包 unicode 包含了一些针对测试字符的非常有用的函数(其中 ch 代表字符):
- 判断是否为字母:unicode.IsLetter(ch)- 判断是否为数字:unicode.IsDigit(ch)- 判断是否为空白符号:unicode.IsSpace(ch)
4.5.5 随机数
内置rand包实现了伪随机数的生成。
package mainimport ("fmt""math/rand")func main() {a := rand.Int() //随机生成一个数,一般很大b := rand.Intn(100) //[0,100)内生成一个随机数fmt.Println(a, b)}
4.7 strings包和strconv包
4.7.1 strings包
一些常用的查找函数:
package mainimport ("fmt""strings")func main() {s := "I love China, i am wuyanzu"if strings.HasPrefix(s, "China") { //判断开头字符fmt.Println("字符串有China")}if strings.HasSuffix(s, "wuyanzu") { //判断结尾字符fmt.Println("字符串以wuyanzu结尾")}if strings.Contains(s, "am") { //判断包含字符fmt.Println("字符串包含了am")}k := strings.Index(s, "i") //返回字符所在第一个位置索引,LastIndex判断最后的位置索引if k != -1 {fmt.Println("i在字符串的位置:", k)}}
一些常用的替换函数:
package mainimport ("strings")func main() {s := "I love China, i am wuyanzu "news := strings.Replace(s, "China", "Chinese", -1) //-1代表替换所有cts := strings.Count(s, "i") //统计i在字符串中出现的次数rs := strings.Repeat("d", 5) //重复5次d字符,并返回ups := strings.ToUpper(s) //变大写lows := strings.ToLower(s) //变小写cuts := strings.TrimSpace(s) //剔除开头结尾的空格cutss := strings.Trim(s, "wuyanzu") //剔除开头结尾的wuyanzuslis := strings.Fields(s) //分割字符串,以空格分隔spls := strings.Split(s, ",") //以逗号来分割sli_str := strings.Join(slis, s) //切片slice和字符串拼接}
4.7.2 strconv包
整个包是用来将字符串和其他类型转换的。
package mainimport ("strconv")func main() {i := 123s := "123"var f float64 = 123.123toint, _ := strconv.Atoi(s) //字符串转inttos := strconv.Itoa(i) //int转字符串ftos := strconv.FormatFloat(f, 'f', 15, 64) //float64转字符串,精度15stof, _ := strconv.ParseFloat(s, 32) //字符串转float64}
