基础语法
- 包名:package = namespace,每个文件都必须有一个包名,每个目录下必须有个main包
- 引入:import = use
- 入口:func main() {},同一级目录只能有一个main()函数
- GOROOT go程序的执行文件位置,go安装好之后会自动生成该环境变量
- 配置 GOPATH 环境变量 会影响构建、执行,GOPATH可以配置多个目录,编译的时候会自动在所有的GOPATH下寻找需要的包
- GOPATH目录下,通常会创建3个目录,src(项目源码),pkg(第三方包),bin(go install产生的文件)
- golang会自动去在GOPATH和GOROOT下的src目录和pkg目录寻找import的包
构建、执行、格式化
go build main.go
go run main.go
go test xxx (xxx为package名)
gofmt -w xxx.go 不加-w则只是格式化输出,加了之后会格式化改写代码文件
定义变量
var x = 1,或var()集中定义多个变量
var a,b int = 3,4 如果定义了类型,那么一行的所有变量都需要是同一类型
a,b,c,d := 1,2,true,“abc” # 如果不定义变量类型,在一行里可以多重赋值不同类型的变量
x := 1 (仅支持在func里这样写,func外只能用var来定义)
a, b int := 3, 4 这样是错误的 冒号+等号 不可以指定类型
var d 纯粹的变量声明时可不能省略类型,那样会编译器会报错
匿名变量 _
i,j = j,i // go支持多重赋值
func GetName() (userName, nickName string) {
return "nonfu", "学院君"
}
_,nickname := getName() // 只返回nickname,前一个_(匿名变量)被废弃
定义常量
基本用法同var
如果两个 const
的赋值语句的表达式是一样的,那么还可以省略后一个赋值表达式
iota 是一个在编译期间可变的常量,每定义一次iota时,iota的值自增1(初始值为0)
const (
Sunday = iota
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
numberOfDays
)
// 对应赋值分别是0,1,2,3,4,5,6,7
单元测试
func_test.go
变量类型
在 Go 语言中,引用类型包括切片(slice)、字典(map)和管道(channel),其它都是值类型。
结构体是值类型,不是引用类型
浮点型
查看数据类型和占用字节大小
fmt.Printf("n1类型是 %T, 占用字节数 %s", n1, unsafe.Sizeof(n1))
- 浮点数都是有符号位的
- float64比float32精度高
- 浮点型默认声明为float64类型,通常应该用float64
- 十进制的浮点数可以声明成3.12和.12(表示0.12)
- 科学计数法
num1 := 2.1234e2
num2 := 2.1234E2
num3 := 2.1234e-2
字符
byte直接用fmt输出时,会输出其对于的Unicode码值,需要用fmt.Printf(“%c”, c1)才能格式化输出字符串
var c1 byte = 'a'
var c2 = '-'
fmt.Println(c1, c2) // 97 45
fmt.Printf("%c %c", c1, c2) // a -
汉字的字符Unicode码超出ASCII码范围,应该用int,用法同上
var c2 string = "abc"
c2[0] = "c" // 这样是不允许的,go不支持单独改一个字符串的单个值
c2 = "www" // 这样是重新赋值,是支持的
- 对于单个字符,双引号的字符存储的是字符本身,单引号的字符存储的是该字符对应的Unicode码值
- 字符串必须用双引号包裹
fmt.Println("D",'D') // 会输出 D 68
// 'DEF'会直接报错invalid character literal (more than one character)
数组、切片
var a [8]byte // 长度为8的数组,每个元素为一个字节
var b [3][3]int // 二维数组(9宫格)
var c [3][3][3]float64 // 三维数组(立体的9宫格)
var d = [3]int{1, 2, 3} // 声明时初始化
var e = new([3]string) // 通过 new 初始化
a := [...]int{1, 2, 3}
这种情况下,Go 会在编译期自动计算出数组长度(3
)
- 数组在初始化的时候,如果没有填满,则空位会通过对应的元素类型空值填充
- 如果初始化的数组元素个数超过声明的数组长度,则无法编译
- []int 和 […]int{x,x,x…x} 不是同一种类型,前者是一个切片类型,后者在编译期间可以推算出长度,即指定长度的数组
- 数组切片底层引用了一个数组,由三个部分构成:指针、长度和容量,指针指向数组起始下标,长度对应切片中元素的个数,容量则是切片起始位置到底层数组结尾的位置,切片长度不能超过容量
nums := [...]int{0,1,2,3,4,5,6,7,8,9,10,11,12}
q3 := nums[6:9]
fmt.Println("q3 = ", q3)
fmt.Println(q3[2:4])
// 以上输出
q3 = [6 7 8] // 左边包含,右边不包含
[8 9] // q3[0] = nums[6],则q3[2:4] = nums[8:10]
- 切片自动扩容,一般会将容量扩大到原来的2倍,当容量扩大至1024时,容量扩大的比例由1/8逐渐增长至4/3,经测试,分别是2,1.25,1.325,1.3584905660377358,1.3333333333333333
- 切片是对一个数组的视图,是对原数组的地址引用,此时,执行append(slice1, xxx),若新增后的切片容量不超过原数组的容量,则会直接体现到原数组该位置的值发生变更;而若新增后的切片容量超过了原数组的容量,则新的切片不再是之前原数组的视图,也不会影响原数组的值,如下:
arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7}
s1 := arr[2:6] // [2,3,4,5]
s2 := s1[3:4] // [5]
s3 := append(s2, 10) // [5,10],此时s3对应的arr的最大索引<cap(arr),arr的相应索引位置的值会被替换,arr = [0,1,2,3,4,5,10,7]
s4 := append(s3, 11) // [5,10,11],此时s4对应的arr的最大索引=cap(arr),未超过,arr的相应索引位置的值会被替换,arr = [0,1,2,3,4,5,10,11]
s5 := append(s4, 12) // [5,10,11,12],此时s5对应的arr的最大索引>cap(arr),已超过,arr的值不再变化,arr = [0,1,2,3,4,5,10,11],arr的类型是[8]int,不会发生变化
// 此时s5实际已经不再是arr的视图了,系统底层会自动分配一个更大的数组给其当做原数组
- make创建切片必须要指定长度
make([]interface{}, len)
字典
字典是一个无序集合,不会按存储的顺序打印内容,而是按照键名的ASCII码值顺序排列的
定义字典类型的2种方式:
testMap := map[string]int{
"one":1,
"two":2,
"three":3,
}
tm := make(map[string]int)
tm["a"] = 23
tm["c"] = 34
以上两种方式都支持像php的关联数组那样追加键值对,如:
testMap["four"] = 4
testMap["five"] = 5
testMap["zero"] = 0
tm["b"] = 3
tm["z"] = 35
下面3中声明方式有差异:
var invMap = make(map[int]string) // ok
invMap := map[int]string{} // ok
var invMap map[int]string // 编译期间会报panic,不可用
指针
如上,ptr
就是一个指针类型,表示指向存储 int
类型值的指针。我们可以通过 *ptr
获取指针指向内存地址存储的变量值(我们通常将这种引用称作「间接引用」),ptr
本身是一个内存地址值(通过 &a
可以获取变量 a
所在的内存地址)
package main
import "fmt"
func main() {
a := 100
var ptr *int
ptr = &a
fmt.Println(ptr) // 输出0xc000062080
fmt.Println(*ptr) // 输出100
*ptr = 20
fmt.Println(*ptr) // 输出20
fmt.Println(a) // 输出20
}
上面例子,由于*ptr
是指向变量a的内存地址,*ptr
改变时,也就是变量a的值被改变了
type关键字
package main
type NewInt int // 定义了一种新的类型,fmt.Printf("%T")会输出main.NewInt,
type IntAlias = int // 给int类型起别名为IntAlias,fmt.Printf("%T")会输出int,IntAlias 类型只会在代码中存在,编译完成时,不会有 IntAlias 类型。
非本地类型不能定义方法
package main
import (
"time"
)
// 定义time.Duration的别名为MyDuration
type MyDuration = time.Duration // 会编译出错
编译器提示:不能在一个非本地的类型 time.Duration 上定义新方法。非本地方法指的就是使用 time.Duration 的代码所在的包,也就是 main 包。因为 time.Duration 是在 time 包中定义的,在 main 包中使用。time.Duration 包与 main 包不在同一个包中,因此不能为不在一个包中的类型定义方法。
解决这个问题有下面两种方法:
- 将
type MyDuration = time.Duration
修改为type MyDuration time.Duration
,也就是将 MyDuration 从别名改为类型。 - 将 MyDuration 的别名定义放在 time 包中。
条件控制
-
在
if
之后,条件语句之前,可以添加变量初始化语句,使用;
间隔,比如可以这么写if score := 100; score >= 90 {
-
在 Go 语言中使用
switch...case...
分支语句时,需要注意以下几点:- 和条件语句一样,左花括号
{
必须与switch
处于同一行; - 单个
case
中,可以出现多个结果选项(通过逗号分隔); - 与其它语言不同,Go 语言不需要用
break
来明确退出一个case
; - 只有在
case
中明确添加fallthrough
关键字,才会继续执行紧跟的下一个case
; - 可以不设定
switch
之后的条件表达式,在这种情况下,整个 switch 结构与多个if...else...
的逻辑作用等同。
- 和条件语句一样,左花括号
函数、闭包
Go里面定义的闭包,可以直接调用闭包外定义的变量,而不用传入(php需要显示的use)
var j int = 1
f := func() {
var i int = 1
fmt.Printf("i, j: %d, %d\n", i, j)
}
f() // 输出i, j: 1, 1
j += 2
f() // 输出i, j: 1, 3
同时,可以在闭包里重新声明一个外部已经声明过的变量,且不会影响外部定义的变量,如:
var j int = 1
f := func() {
var i int = 1
j := 5
fmt.Printf("i, j: %d, %d\n", i, j)
}
f() // 输出i, j: 1, 5
j += 2
f() // 输出i, j: 1, 5
fmt.Println(j) // 输出3
如果在闭包里引入了一个闭包外部声明的变量,然后离开这个外部变量的作用域去使用该闭包,仍然可以使用该闭包引入的外部变量。如我们可以先声明一个外部函数的参数为函数类型,然后定义一个闭包并赋值给指定变量,再将这个变量传递到外部函数中:
import "fmt"
func main() {
i := 10
add := func (a, b int) {
fmt.Printf("Variable i from main func: %d\n", i)
fmt.Printf("The sum of %d and %d is: %d\n", a, b, a+b)
}
callback(1, add);
}
func callback(x int, f func(int, int)) {
f(x, 2)
}
上述代码的打印结果是:
Variable i from main func: 10
The sum of 1 and 2 is: 3
通过这个示例,我们还验证了虽然 i
变量声明在 main
函数中,在调用 callback
外部函数时传入了匿名函数 add
作为参数,add
函数在外部函数中执行,虽然作用域离开了 main
函数,但是还是可以访问到变量 i
。
面向对象
归属同一个包的代码具备以下特性:
- 归属于同一个包的代码包声明语句要一致,即同一级目录的源文件必须属于同一个包;
- 在同一个包下不同的不同文件中不能重复声明同一个变量、函数和类;