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 testdir
import "fmt"
func Test() {
fmt.Println("这里是testdir包")
}
main.go:
package main
import (
"test/testdir" //这里需要连父目录test也导入,而不是testdir
)
func main() {
testdir.Test()
}
dd.go属于testdir包,也是目录名,但是这不是强制的,dd.go可以改包名为任意的,如test123包,那么在mian.go导入的时候需要这样:
//dd.go
package test123
import "fmt"
func Test() {
fmt.Println("这里是testdir包")
}
//main.go
package main
import (
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 main
import "fmt"
func add(a int, b int) (int, int) { //加法函数,多参数,多返回值
return 1, a + b
}
func main() {
_, sum := add(5, 6) //舍弃返回值1
fmt.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 main
import (
"fmt"
"reflect"
)
func main() {
var a string
a = "1"
fmt.Println(a)
fmt.Println([]byte(a))
fmt.Println(reflect.TypeOf([]byte(a))) //利用发射来查看变量的类型
}
但是,如果是int和string之间的转换,需要用到strcov内置函数。
package main
import (
"fmt"
"reflect"
"strconv"
)
func main() {
var a int
var b string
a = 1
b = "2"
c := strconv.Itoa(a) //int转string
d, _ := strconv.Atoi(b) //string转int
fmt.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 main
func main() {
const (
a = iota //a = 0
b //b = 1
c = iota //c = 2
d = 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 main
import "fmt"
func main() {
a := 1
if 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) // integer
fmt.Printf("%c - %c - %c\n", ch, ch2, ch3) // character
fmt.Printf("%X - %X - %X\n", ch, ch2, ch3) // UTF-8 bytes
fmt.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 main
import "fmt"
func main() {
i := 123
var 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 main
import "fmt"
func main() {
var a int = 1 //0000 0001
var b int = 3 //0000 0011
var c int = a & b //按位与
var d int = a | b //按位或
var e int = a ^ b //按位异或
var f int = b &^ a //按位清除,把b在a的位置的数置0
print(c) //0000 0001 = 1
print(d) //0000 0011 = 3
print(e) //0000 0010 = 2
print(f) //0000 0010 = 2
fmt.Printf("%08b", ^2) //-0000 0011
print(1 << 10) //左移10位 = 1024
print(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 main
import (
"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 main
import (
"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 main
import (
"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") //剔除开头结尾的wuyanzu
slis := strings.Fields(s) //分割字符串,以空格分隔
spls := strings.Split(s, ",") //以逗号来分割
sli_str := strings.Join(slis, s) //切片slice和字符串拼接
}
4.7.2 strconv包
整个包是用来将字符串和其他类型转换的。
package main
import (
"strconv"
)
func main() {
i := 123
s := "123"
var f float64 = 123.123
toint, _ := strconv.Atoi(s) //字符串转int
tos := strconv.Itoa(i) //int转字符串
ftos := strconv.FormatFloat(f, 'f', 15, 64) //float64转字符串,精度15
stof, _ := strconv.ParseFloat(s, 32) //字符串转float64
}