GO基础
Go是一种并发的、带垃圾回收的、快速编译的语言。
一个例子:
//当前程序的包名 package main //导入其它的包 import ( "flag" "fmt" "os" ) //常量定义 const PI = 3.14 //全局变量的声明与赋值 var name = "go" //一般类型的声明 type intType int //结构声明 type newType struct { } //接口声明 type newInterface interface { } //main函数,程序的入口 func main() { fmt.Println(os.Args) flag.Parse() fmt.Println(flag.Args()) }
说明:
1、每个go源代码文件的开头都是一个package声明,表示该go代码所属的包;包是go语言中最基本的分发单位,也是工程管理中依赖关系的体现。要生成go可执行程序,必须建立一个名为main的package,并且在该package中包含一个叫main()的函数;
2、Go语言的main()函数不能带参数,也不能定义返回值,传入的命令行参数在os.Args变量中保存;
3、在包声明之后,是一系列的import语句,用于导入改程序所依赖的包,但不能导入在源代码文件中没有用到的包,否则Go编译器会报错;
4、Go语言中,语句不用以分号结束;
编译、运行:
go run test.go
使用这个命令,会将编译、链接和运行3个步骤合并为一步,运行完成后在当前目录下看不到任何中间文件和最终的可执行文件。
如果只要生成编译结果,而不自动运行,可以使用:
go build test.go
变量
变量声明语句:
var 变量名 变量类型
例子:
var v1 int var v2 string // 字符串 var v3 [10]int // 数组 var v4 []int // 数组切片 var v5 struct { // 接口 f int } var v6 *int // 指针 var v7 map[string]int // map,key为string类型,value为int类型 var v8 func(a int) int
可以使用一个var关键字同时声明多个变量:
var ( v1 int v2 string )
如果在声明变量的同时还进行了初始化,则var关键字可以省略,并且Go编译器可以从初始化表达式的右值推导出该变量应该声明为哪种类型,这有点类似于动态类型,但Go实际上是强类型的语言(静态类型语言)。
如下所示三种用法的效果是一样的:
var v1 int = 10 v2 int = 10 // 编译器自动推导出v2的类型 v3 := 10 // 编译器自动推导出v3的类型
说明:
":="用于明确表达同时进行变量声明和初始化工作,但要注意出现在:=左侧的变量不应该是已经被声明过的,否则会导致编译器报错,如下:
var i int i := 2 //error
常量
在Go语言中,常量是指编译期间就已知且不可改变的值。
1、字面常量
-12 // 整型常量 3.14 // 浮点型常量 3.2+12i // 复数类型的常量 true // 布尔型常量 "foo" // 字符串常量
在C语言中,常量通常有类型,比如-12在C语言中会认为是一个int类型,如果要指定它为long类型,需要写成-12L。
在Go语言中,字面常量没有类型,只要这个常量在相应类型的值域范围内,就可以作为该类型的常量,比如上面的常量-12,可以赋值给int、uint、int32、int64、float32、float64、complex64、complex128等类型的变量。
2、const常量
通过const常量,可以给字面常量指定一个友好的名字:
const Pi float64 = 3.1415 const zero = 0.0 //无类型浮点常量 cosnt ( size int64 = 1024 eof = -1 //无类型整型常量 ) const u,v float32 = 0, 3 const a,b,c = 3,4,"foo"
const mask = 1<<3
常量的赋值是一个编译期行为,所以右值不能出现任何需要运行期才能得出结果的表达式,例如:
const HOME = os.GetEnv("HOME") // error
3、预定义常量
Go语言预定义常量有:true、false、iota,前两个为bool常量;
iota是一个可被编译器修改的常量,在每一个const关键字出现时被重置为0,然后在下一个const出现之前,每出现一次iota,其所代表的数字会自动增1,例如:
const ( // rest iota c0 = iota // c0=0 c1 = iota // c1=1 c2 = iota // c2=2 )
4、枚举
在const后跟一对园括号的方式定义一组常量,例如:
const ( Sunday = iota Monday Tuesday Wednesday Thursday Friday Saturday numberofDays // 这个常量没有导出 )
同Go语言的其他符号一样,以大写字母开头的常量在包外可见;
以上例子中,numberofDays为包内私有,其它符号则可被其它包访问。
类型
Go语言中使用的类型包括:
基础类型 |
||
布尔类型(bool) |
var b1 bool = true |
|
整型 |
var v1 int = 12 |
|
浮点类型(float32、float64) |
var f1 float32 = 12.0 |
|
复数类型(complex64、complex128) |
var c1 complex64 = 3.2 + 12i |
|
字符串(string) |
var s string = “sina” |
|
字符类型(rune) |
代表单个的unicode字符 |
|
错误类型(error) |
|
|
|
|
|
复合类型 |
||
指针(pointer) |
|
|
数组(array) |
var array [32] byte |
|
切片(slice) |
var slice [] int |
|
字典(map) |
var word_count map[string] int |
|
通道(chan) |
var ch chan int |
|
结构体(struct) |
var s struct {} |
|
接口(interface) |
|
1、布尔类型
布尔类型不能接受其它类型的赋值,不支持自动或强制的类型转换,以下的示例是一些错误的用法:
var b bool b = 1 // error b = bool(1) // error
以下的用法是正确的:
var b bool b = (1!=0)
2、整型
类型 |
长度 |
值范围 |
int8 |
1 |
-128 ~ 127 |
uint8(即byte) |
1 |
0 ~ 255 |
int16 |
2 |
-32768 ~ 32767 |
uint16 |
2 |
0 ~ 65535 |
int32 |
4 |
-2147483648 ~ 2147483647 |
uint32 |
4 |
0 ~ 4294967295 |
int64 |
8 |
(-2^63) ~ (2^63-1) |
uint64 |
8 |
0 ~ (2^64-1) |
int |
平台相关 |
平台相关 |
uint |
平台相关 |
平台相关 |
uintptr |
同指针 |
32位平台下为4字节,64位平台下为8字节 |
需要注意的是,int和int32是不同的类型, 不能相互赋值,例如:
var val2 int32 val1 := 64 // val1会被自动推导为int类型 var2 = val1 // error
var2 = int32(val1) // ok
此外,不同类型的整型数不能直接比较,比如int8类型的数和int类型的数不能直接比较,但各种类型的整型变量都可以直接与字面常量(literal)进行比较,比如:
var i int32 var j int64 i,j = 1,2 if i==j { // error fmt.Println("i and j are equal.") } if i==1 || j==2 { // ok fmt.Println("i and j are equal.") }
3、浮点型
Go语言中的float32和float64分别等价于C语言的float、double类型;
var i float32 = 12.1 j := 64.0 // 自动推导为float64类型 j = i // error j = float64(i) // ok
判断两个浮点数是否相等,是根据不同精度来的:
import "math" func IsEqual(f1, f2, p float64) bool { return math.Fdim(f1, f2) < p }
其中,p是用户自定义的比较精度,比如p=0.00001。
4、字符类型
在Go语言中支持两个字符类型,一个是byte(实际上是uint8的别名),代表UTF-8字符串的单个字节的值;另一个是rune,代表单个Unicode字符。
关于rune相关的操作,可查阅Go标准库的unicode包;另外unicode/utf8包也提供了UTF8和Unicode之间的转换。
5、字符串
字符串支持下标读取操作:
str := "Hello world" ch := str[0] fmt.Printf("The length of \"%s\" is %d\n", str, len(str)) fmt.Printf("The 1st character of \"%s\" is '%c'\n", str, ch)
但字符串的内容在初始化后不能被修改,例如:
str := "Hello world" str[0] = 'X' // error
常用的字符串操作:
操作 |
含义 |
s1 + s2 |
字符串连接 |
len(s) |
字符串长度 |
s[i] |
取字符 |
字符串遍历有两种方式:
str := "Hello,世界"
// 以字节数组的方式遍历
for i := 0; i<len(str); i++ {
ch := str[i]
fmt.Println(i, ch)
}
// 以unicode字符方式遍历,每个字符的类型是rune
for i, ch := range str {
fmt.Println(i, ch)
}
6、数组
数组的声明方法比较多,比如:
[32] byte // 字节数组 [2*N] struct {x, y int 32} // 结构体数组 [1000] *float64 // 指针数组 [3][5] int // 二维数组 [2][2][2] float64
[...]int{1,2,3} // 省略数组长度,Go会根据元素个数来计算长度
在声明数组时长度可以为一个常量或一个常量表达式,数组长度在定义以后就不可以再改变。
数组支持按下标读写元素,也支持range关键字的遍历,例如:
var array = [5] int {10,20,30,40,50} for i, v := range array { array[i] = v*2; } for i, v := range array { fmt.Println(i, v) }
另外,数组是值类型,如果将数组作为函数参数传递,则在函数调用的时候该参数将发生数据复制,因此,在函数体中无法修改传入的数组的内容。
7、slice
数组切片类似于C++中STL的std::vector<>,支持动态扩展数组,并且可以被作为函数参数传递而不会导致元素被复制。
数组切片的数据结构可以抽象为以下3个变量:
- 一个指向原生数组的指针;
- 数组切片中的元素个数;
- 数组切片已分配的存储空间;
其结构大致如下:
type slice struct { first *T len int cap int }
创建数组切片有下面多种方式:
1、基于数组创建的方式
var myArray [10] int = [10] int {1,2,3,4,5,6,7,8,9,10} var s1 = myArray[:] // 基于myArray的所有元素创建数组切片 var s2 = myArray[:5] // 基于myArray的前5个元素创建数组切片 var s3 = myArray[5:] // 基于myArray从第5个元素开始的所有元素创建数组切片
2、直接创建数组切片的方式
s1 := make([] int,5) // 创建一个初始元素个数为5的数组切片,元素初始值为0 s2 := make([] int,5, 10) // 创建一个初始元素个数为5的数组切片,元素初始值为0,并预留10个元素的存储空间 s3 := []int{1,2,3,4,5} // 创建并初始化包含5个指定元素的数组切片
3、基于数组切片创建的方式
oldSlice := []int{1,2,3,4,5} newSlice := oldSlice[:3]
操作数组元素的所有方法都适用于数组切片,比如数组切片也可以按下标读写元素,用len()获取元素个数,并支持使用range关键字来快速遍历所有元素。
数组切片支持可动态增减元素,内置的cap()和len()函数,分别返回数组切片分配的空间大小、当前存储的元素个数。
s := make([] int,5, 10) fmt.Println("len(s)=",len(s)) // 5 fmt.Println("cap(s)=",cap(s)) // 10
使用append函数可以在数组切片尾端添加新元素:
s = append(s, 1,2,3)
如果追加的内容长度超过当前已分配的存储空间(即cap()返回值),数组切片会自动分配一块足够大的内存。
还可以将另一个数组切片追加到一个数组切片末端:
s2 := []int{8,9,10} s = append(s, s2...) // s2后面的省略号必须要有
前面提到Slice内部是一个指针,所以修改Slice的元素,会影响其所指向的数组,例如:
var myArray [5] int = [5] int {1,2,3,4,5} var s1 = myArray[:] var s2 = s1 s1[3] = 90 fmt.Println(myArray) // [1 2 3 90 5] fmt.Println(s1) // [1 2 3 90 5] fmt.Println(s2) // [1 2 3 90 5]
数组切片的复制,如果两个slice不一样大,就会按其中较小的slice的元素个数进行复制,例如:
s1 := []int {1,2,3,4,5} s2 := []int {5,4,7} copy(s1, s2) //只复制s2的3个元素到s1的前3个位置 copy(s2, s1) //只复制s1的前3个元素到s2中
8、map
map是key-value结构的一个字典,类似于C++中STL的std::map<>。
例子:
type PersonInfo struct { ID string Name string Address string } func main() { var personDB map[string] PersonInfo // 变量声明 personDB = make(map[string] PersonInfo) // 变量创建 personDB["1"] = PersonInfo{"12345","Tom","Room 203"} // 增加了一个键 person, ok := personDB["1"] // 查找 if ok { fmt.Println("found person", person.Name, "with ID 1") } else { fmt.Println("Did not find person with ID 1") }
delete(personDB, "1") // 删除一个键 }
遍历map的例子:
m1 := map[string]int{"one": 1, "two": 2, "three": 3} fmt.Println(m1) for key, val := range m1{ fmt.Printf("%s => %d \n", key, val) }
注意:range支持对 string, array, slice, map, buffered chan的遍历;
9、结构体
定义一个struct:
type Rect struct { x, y float64 width, height float64 }
初始化的几种方式:
rect1 := new(Rect) rect2 := &Rect{} rect3 := &Rect{0, 0, 100, 200} rect4 := &Rect{width:100, height:200}
在Go语言中,未进行显式初始化的变量都会被初始化为该类型的零值,例如bool类型的零值为false,int类型的零值为0,string类型的零值为空字符串。
10、指针
Go支持指针,例如:
var i int = 1 var pInt *int = &i //输出:i=1 pInt=0xf8400371b0 *pInt=1 fmt.Printf("i=%d\tpInt=%p\t*pInt=%d\n", i, pInt, *pInt) *pInt = 2 //输出:i=2 pInt=0xf8400371b0 *pInt=2 fmt.Printf("i=%d\tpInt=%p\t*pInt=%d\n", i, pInt, *pInt) i = 3 //输出:i=3 pInt=0xf8400371b0 *pInt=3 fmt.Printf("i=%d\tpInt=%p\t*pInt=%d\n", i, pInt, *pInt)
package
- 一个可执行程序有且仅有一个main包;
- 只有package名称为main的包可以包含main函数;
- 每个package都可以定义自己的init()函数,每个包被导入时,会执行其init()函数;
- import导入包时,是在$GOPATH变量定义的路径里面搜索;
- 通过import关键字来导入其它非main包,有两种方式:
-
import "flag" import "fmt" import "os"
import ( "flag" "fmt" "os" )
-
- 导入包之后,使用<PackageName>.<FuncName>来对包中的函数进行调用;
- 导入的包可以重命名,例如 import alias "PackageName",然后使用<alias>.<FuncName>来对包中的函数进行调用;
- 将包名重命名为 ".",可以省略调用,例如 import . "PackageName",然后可以直接使用<FuncName>来对包中的函数进行调用;
- import _ "packageName" 操作不直接使用包里的函数,而是调用了该包的init函数;
- 使用大小写来决定该常量、变量、类型、接口、结构或函数是否可以被外部包所调用——函数名首字母小写即为private,函数名首字母大写即为public;
内存分配
Go具有两个分配内存的机制,分别是内建函数new()和make()。
new 是一个分配内存的内建函数,new(T)为一个类型为T的新对象分配了值为零的存储空间并返回其地址,也就是一个类型为*T的值。用Go的术语来说,就是它返回了一个指向新分配的类型为T的零值的指针。
make(T, args)仅用于创建slice、map和chan,并返回类型T(不是*T)的一个被初始化了的(不是零)实例。这三种类型实质上是对在使用前必须进行初始化的数据结构的引用:
slice := make([] type, len, cap) map := make(map[key_type] value_type) chan := make(chan type, len)
以下示例说明了new和make的不同:
var p *[]int = new([]int) // 为切片结构分配内存,*p == nil var v []int = make([]int, 10) // 切片v现在是对一个新的有10个整数的数组的引用 fmt.Println(*p) // [] fmt.Println(v) // [0 0 0 0 0 0 0 0 0 0]
流程控制
1、选择语句
if 条件表达式 { ... } else if{ ... } else {
...
}
注意:
- 条件表达式不需要使用括号();
- 条件语句体必须使用花括号,且"{"必须与 if 或者 else处于同一行;
- 在if 和条件表达式之间,可以添加变量初始化语句,使用";"间隔;
- 在有返回值的函数中,不允许将最终的return语句包含在if...else...结构中;
switch语句:
switch i { // 左花括号"{"必须与switch处于同一行 case 0: fmt.Printf("0") case 1: fmt.Printf("0") case 2: fallthrough case 3: fmt.Printf("3") case 4,5,6: fmt.Printf("multi") default: fmt.Printf("default") }
- i=0时,输出0;
- i=1时,输出1;
- i=2时,输出3,fallthrough关键字表示继续执行下一个case;
- i=3时,输出3;
- i=4/5/6时,输出"multi",单个case后面可以出现多个结果项;
- i=其它任意值时,输出"default"。
注意:
- Go语言不需要用break来退出一个case,只有在case中添加fallthrough关键字,才会继续执行紧跟的下一个case;
- 条件表达式不限制为常量或者整数,条件表达式甚至也可以没有,例如:
Num := 8 switch { case 0<= Num && Num <=3: fmt.Println("0-3") case 4<= Num && Num <=6: fmt.Println("4-6") case 7<= Num && Num <=9: fmt.Println("7-9") }
当缺失条件表达式时,整个switch结构与多个if...else...的逻辑作用相同。
2、循环语句
Go语言的循环控制只支持for语句,不支持while结构。
for循环语句的循环表达式也不需要使用()括起来,例如:
sum := 0 for i:=0; i<10; i++ { sum += i }
精简的for循环语句:
i := 1 for i<10 { fmt.Println(i) i++ }
for循环也支持continue和break语句,例如:
sum := 0 for { // 死循环 sum ++ if sum>100 { break } }
对于嵌套循环,break还可以选择中断哪个循环,例如:
var i int var j int JLoop: for j=0; j<5; j++ { for i=0; i<10; i++ { if i>5 { break JLoop } } }
3、跳转语句
Go语言仍支持使用goto关键字在函数体内进行跳转,例如:
func foo() { i := 0 HERE: i++ if i<10 { goto HERE } }
函数
函数定义
函数声明语句:
func 函数名(参数列表) (返回值列表) { // 函数体 }
注意:
1、 参数列表和返回值列表都是变量名在前,变量类型在后;
2、 Go函数支持多返回值,但并不是所有返回值都必须赋值,在函数返回时没有被明确赋值的返回值都会被设置为默认值。
3、 函数左起的花括号”{”不能另起一行,否则会报错;
以一个简单的计算加法的函数为例:
func add(a int, b int) (ret int, err error) { if a<0 || b<0 { // 假设这个函数只支持两个非负数的加法 err = errors.New("Should be non-negative numbers!") return } return a+b, nil }
如果参数列表中若干个相邻的参数类型相同,则可以在参数列表中省略前面变量的类型声明,例如:
func add(a, b int) (ret int, err error) { ... }
如果返回值列表中多个返回值的类型相同,也可以用同样的方式合并;另外,如果函数只有一个返回值,可以这样写:
func Add(a, b int) int { ... }
Go语言支持多重赋值,比如:
i, j = j, i
用于交换两个变量的值,在不支持多重赋值的语言中,交换两个变量的内容需要引入一个临时变量:
t = i; i = j; j = t
Go函数带回多个返回值时,可以使用多重赋值语句,将不同的返回值赋值给不同的变量,并且允许使用匿名变量("_")接受不需要使用的返回值,例如:
func GetName() (firstName, lastName, nickName string) { return "May", "Chan", "Chibi Maruko" } _, _, nickName := GetName
函数调用
函数调用非常方便,只要事先导入该函数所在的包,就可以调用了:
import "mymath" c := mymath.Add(1,2)
注意:小写字母开头的函数只在本包内可见,大写字母开头的函数才能被其它包使用。
不定参数
例如:
func foo(args ...int) { // 接受不定数量的参数,这些参数都是int类型 for _, arg := range args { fmt.Println(arg) } } foo(2,3,4) foo(1,3,7,13)
形如"...type"格式的类型只能作为函数的参数类型存在,并且必须是最后一个参数。
"...type"本质上是一个数组切片,也就是[]type,这也是为什么上面的参数args可以用for循环来获得每个传入的参数。
如果希望不定参数传任意类型,可以指定类型为interface{},如标准库中的fmt.Printf()的函数原型:
func Printf(format string, args ...interface{}) { ... }
例如:
func foo(args ...interface{}) { for _, arg := range args { switch arg.(type) { case int: fmt.Println(arg, "is an int value.") case string: fmt.Println(arg, "is a string value.") case float32: fmt.Println(arg, "is a float32 value.") default: fmt.Println(arg, "is an unknown type.") } } }
匿名函数与闭包
匿名函数可以直接赋值给一个变量,例如:
f := func(x, y int) int { return x+y }
或者直接执行一个匿名函数:
func(ch chan int) { ch <- ACK } (reply_chan) // 花括号后面直接跟参数列表表示函数调用
闭包:当一个函数内部嵌套另一个函数定义时,内部的函数体可以访问外部函数的局部变量。
a := func() (func()) { var i int = 10 return func(){ fmt.Printf("i=%d\n", i) i++ } } c1 := a() c2 := a() c1() // 10 c1() // 11 c1() // 12 c2() // 10
c1和c2是建立在同一个函数上,但作用在同一个局部变量的不同实例上的两个不同的闭包。