因为工作的关系要转而使用Go,对我来说这是一门新语言,只好临时抱一下佛脚。绝大部分内容来源于菜鸟教程。
一、环境安装
1. Go语言安装
下载地址:https://golang.org/dl/,包括Windows、Mac、Linux三种操作系统,默认安装位置是在C盘,如果自定义了,需要修改系统环境变量中的GOPATH以及Path中的相关值,让它保持跟自定义的路径一致。
2. 编译器
常用的Go编译器有三种:JetBrains系列的Goland(付费)、轻量级开源的LiteIDE以及Eclipse插件goclipse,当然,也可以自行用文本编辑器编写.go后缀的文件,然后用命令行:go run xxx.go来运行,或者给sublime、vscode之类的安装相应的插件。我自己图方便就用了LiteIDE。
二、基础语法
1. Hello World
// test project main.go
package main
import (
"fmt"
)
func main() {
fmt.Println("Hello World!")
}
第一行声明代码所在的包,包名和文件名、文件夹名没有必然联系,但同一文件夹内的包名必须一致。
第二行导包,fmt是Go的内置包,实现了一些IO操作。
剩余的就是声明了一个main函数,众所周知一般来讲,main函数是程序运行的入口,这里的代码也很简单,就是在控制台输出“Hello World!”的字符串。
第零行(不是)是典型的单行注释,多行注释使用/* */夹击。
Go中不允许"{"为单独一行
2. 数据类型
Go语言的数据类型分得比较细,数量也比较多,总体来讲分成了四个大类:
- 布尔型:bool,取值只有true或false
- 字符串:string,由单个字节连接起来,使用 UTF-8 编码标识 Unicode 文本,可用+号连接:"HelloWorld" = "Hello" + "World"
- 派生类型:指针(ptsd要犯了)、数组、结构、Channel、函数、切片、接口、Map,有两三个之前没接触过的概念
- 数字:这个类型最多了,选择哪个完全看内存需要
整型:uint8、uint16、uint32、uint64、int8、int16、int32、int64
浮点型:float32、float64、complex64、complex128(后两个分别指32位和64位的实数+虚数)
其他:byte(类似uint8)、rune(类似int32)、uint(32或64位)、int(=uint)、uintptr(用来放指针)
3. 变量与常量
3.1 变量声明
一般会使用var关键字。
指定类型
var name type
var name type = value
变量声明时可以不指定初始值,此时将被设置为零值。其中数字类型的零值为0,布尔型为false,字符串为"",其他的大致上都是nil。
不指定类型
var d = true // 自动判断为布尔型
省略var关键字
这种情况下必须声明新的变量,而不是仅对已有变量赋值。有多个变量的情况下,其中至少要有一个变量是新声明的。这种形式只能使用在函数体中,不可用于全局变量。
name := value // 必须是新变量
name1, name2 := value1, value2 // 其中之一是新变量即可
多变量声明
var name1, name2 type
name1, name2 = value1, value2
var name1, name2 = value1, value2
// 只能在函数体中出现
name1, name2 := value1, value2
// 常用于全局变量
var(
name1 type1
name2 type2
)
3.2 值类型和引用类型
- 值类型:int、float、bool、string,通过&i获取变量i的内存地址
- 复杂数据用引用类型,变量只存储内存地址(即指针)或地址中第一个字所在的位置
3.3 变量的其他说明
- 局部变量不允许只声明不使用(仅赋值也不行),也不允许未声明就使用
- 全局变量允许只声明不使用
- 同一类型变量可在同一行声明,不同类型变量可在同一行赋值
- 交换变量值
a, b = b, a
- 抛弃一个值
_, b = 5, 7
3.4 常量
Go语言的常量只能是布尔、数字、字符串之一,程序运行中不能修改,声明方法和变量类似,但是要把var改为const。
const name type = value
const name1, name2 = value1, value2
// 枚举
const (
name1 = value1
name2 = value2
name3 = value3
)
常量可以使用len()、cap()、unsafe.Sizeof()等内置函数。
使用最后一种方式时,如果某常量只声明未赋值,其赋值表达式将和它的上一个常量一样。但如果这是第一个常量,编译出错。
3.5 itoa
itoa是一个特殊常量,在const出现时重置为0,const中每新增一行常量声明使itoa加1。
const (
a = iota // a = 0
b // b = 1
c // c = 2
)
const (
a = iota // a = 0
b // b = 1
c = "ha" // c = "ha"
d // d = "ha"
e = 100 // e = 100
f // f = 100
g = iota // g = 6
h // h = 7
)
const (
a = 1 << iota // a = 1
b = 3 << iota // b = 6
c // c = 12
d // d = 24
)
4. 运算符
二元操作符需要二者类型相同。
4.1 算术运算符
+、-、*、/、%、++、--,其中++、--略有限制,不能用在赋值语句中。
var a = 10
a++ // a = 11
++a // 编译出错
var b = a++ // 编译出错
fmt.Print(a++) // 编译出错
4.2 关系运算符
==、!=、>、<、>=、<=。
4.3 逻辑运算符
&&、||、!。
4.4 位运算符
&、|、^、<<、>>,没有无符号位移<<<和>>>。
&^:按位置零,z = x &^ y,y = 0则z = x;y != 0则z = 0。
^也可作单目运算符,表示按位取反。
3 &^ 0 = 3
3 &^ 1 = 2
3 &^ 2 = 1
var a int8 = 3
^a = -4
var a uint8 = 3
^a = 252
a &^ b = a & (^b)
4.5 赋值运算符
=、+=、-=、*=、/=、%=、<<=、>>=、&=、^=、|=。
4.6 其他运算符
- &:取地址,&a为返回变量a的实际地址
- *:指针变量
var a int = 4
var ptr *int
ptr = &a
fmt.Printf(a) // a = 4
fmt.Printf(*ptr) // *ptr = 4
fmt.Println(ptr) // ptr = a的物理地址
4.7 运算符优先级
没有括号的情况下,优先级由高到低:
- *、/、%、<<、>>、&、&^
- +、-、|、^
- ==、!=、<、<=、>、>=
- &&
- ||
5. 条件语句
Go中没有三目运算符。
if 布尔表达式 {
}
if 布尔表达式 {
} else {
}
if 布尔表达式 {
} else if 布尔表达式 {
} ... else {
}
switch var1 { // case后默认自带break语句
case value1:
case value2, value3:
xxxx
fallthrough // 强制执行下一条case语句
...
default:
}
switch x.(type) {
case type1:
case type2, type3:
...
default:
}
select语句类似于switch,每个case必须是一个通信操作(发送或接受)。select随机执行一个可运行的case,若无则将阻塞。
select {
case communication clause1 :
statement(s);
case communication clause2 :
statement(s);
...
default :
statement(s);
}
6. 循环语句
for i := 0; i <= 10; i++ {
}
for [true] { // 无限循环
}
strings := []string{"google", "runoob"}
for i, s := range strings { // 对数组的foreach循环
}
循环控制语句包括break、continue和goto(跳至某个标签),前二者除了普通用法外,还可使用标签。
re:
for i := 1; i <= 3; i++ {
fmt.Printf("i: %d\n", i)
for i2 := 11; i2 <= 13; i2++ {
fmt.Printf("i2: %d\n", i2)
break re
}
}
// 输出:
// i: 1
// i2: 11
re:
for i := 1; i <= 3; i++ {
fmt.Printf("i: %d\n", i)
for i2 := 11; i2 <= 13; i2++ {
fmt.Printf("i2: %d\n", i2)
continue re
}
}
// 输出
// i: 1
// i2: 11
// i: 2
// i2: 11
// i: 3
// i2: 11
三、代码块
1. 函数
函数定义,函数可以返回多个值。
func functionName( [parameter list] ) [returnType] {
}
func functionName( [parameter list] ) (returnType list) {
}
函数可以作为另外一个函数的实参,将函数声明为函数变量。
/* 声明函数变量 */
getSquareRoot := func(x float64) float64 {
return math.Sqrt(x)
}
/* 使用函数 */
fmt.Println(getSquareRoot(9))
函数闭包,匿名函数,可以直接使用外函数内的变量。
func getSequence() func() int {
i := 0
return func() int {
i += 1
return i
}
}
func main() {
/* nextNumber 为一个函数,函数 i 为 0 */
nextNumber := getSequence()
/* 调用 nextNumber 函数,i 变量自增 1 并返回 */
fmt.Println(nextNumber())
fmt.Println(nextNumber())
fmt.Println(nextNumber())
}
// 输出结果
// 1
// 2
// 3
// 带参数的闭包
func add(x1, x2 int) func(x3 int,x4 int)(int,int,int) {
i := 0
return func(x3 int,x4 int) (int,int,int){
i++
return i,x1+x2,x3+x4
}
}
func main() {
add_func := add(1,2)
fmt.Println(add_func(1,1))
fmt.Println(add_func(0,0))
fmt.Println(add_func(2,2))
}
// 输出结果
// 1 3 2
// 2 3 0
// 3 3 4
2. 方法
类似于函数,但会绑定一个接受者。接受者可以使命名类型或结构体类型,相当于Java中的实例方法。但若要改变接受者中某部分的内容,需要传递指针。
// 声明格式
func (name type) functionName([parameter list]) [returnType] {
}
/* 定义结构体 */
type Circle struct {
radius float64
}
// 该 method 属于 Circle 类型对象中的方法
func (c Circle) getArea() float64 {
return 3.14 * c.radius * c.radius
}
// 方法的调用
func main() {
var c1 Circle
c1.radius = 10.00
fmt.Println("圆的面积 = ", c1.getArea())
}
3. 变量作用域
- 局部变量:函数体内声明,作用域也在函数体内。包括参数和返回值。
- 形式参数:作为函数的局部变量
- 全局变量:函数体外声明,可在整个包内或导出后在包外被使用。局部变量和全局变量名称相同时,局部变量会被优先考虑。
四、数据结构
1. 数组
var arrName [size] type // 仅声明
var arr = [5]int32{1, 2, 3, 4, 5} // 初始化,长度等于初始元素
var arr = [6]int32{1, 2, 3, 4, 5} // 初始化,长度大于初始元素,剩余位置置为0;长度不能小于初始元素
var arr = [...]int32{1, 2, 3, 4, 5} // 初始化,长度不指定,而是根据初始化值计算
多维数组
var arrName [size1][size2]...[sizeN] type
var arr = [2][3] int{
{1,2,3},
{4,5,6},
}
var arr = [2][3] int{
{1,2,3},
{4,5,6}}
向函数传递数组,必须制定数组长度。此时传递的是数组的拷贝,而不是指针。
func functionName(param [10]int) int { // 指定长度,此时传入的数组不能是其他长度
}
2.指针
// 获取变量内存地址
var a int = 1
fmt.Println(&a)
// 声明指针
var p *int
p = &a
// 用指针访问值
fmt.Println(*p)
指针定义后未分配变量,为空指针,值为nil。
指针也可以作为数组元素。
var arr []int = {1,2,3}
var pArr [3]*int
var i int
for i = 0; i < 3; i++ {
pArr[i] = &arr[i]
}
指针的指针(可以套娃到至少3层)
var a int = 1
var p *int = &a
var pp **int = &p
fmt.Println(a)
fmt.Println(*p)
fmt.Println(**pp)
指针作为函数参数,改变其所指向的变量的值
func swap(x *int, y *int) {
*x, *y = *y, *x
}
3.结构体
type Person struct {
name string
age int
}
var structVar1 Person = Person{"zhangsan", 20}
var structVar2 Person = Person{name: "zhangsan", age: 20}
var structVar3 Person = Person{name: "zhangsan"}
structVar1.age = 21
fmt.Println(structVar.age)
var sp *Person = &structVar1
sp.age = 22
fmt.Println(sp.age)
结构体作为函数参数时是值传递,若要改变成员的值,需要传递指针。
4.切片
数组的部分引用,长度可变。作为参数传递时是引用传递。
切片的初始化,既可以是数组,也可以是切片
// a为已存在数组,s为已存在切片
var s1 []int = a[startIndex : endIndex] // s[startIndex : endIndex]
var s1 []int = make([]int, len, cap)
其中startIndex,endIndex,cap均可省略。
len()和cap()方法可用于计算切片的长度和容量。切片只定义未初始化时为空切片,值为nil,长度为0。可以用append()追加切片内容,可同时追加多个元素,若长度超过容量,将自动扩容。可以用copy()拷贝切片内容,只拷贝和俩切片较小长度相同数量的内容。
如果切片来自于数组,对切片进行修改时,相应的数组元素也会被修改。
5.范围(Range关键字)
用于迭代,可用在数组、切片、集合、字符串等类型上,返回索引和对应值,或kv。
// 求和
nums := []int{2, 3, 4}
sum := 0
for _, num := range nums {
sum += num
}
kvs := map[string]string{"a": "apple", "b": "banana"}
for k, v := range kvs {
fmt.Printf("%s -> %s\n", k, v)
}
for i, c := range "go" {
fmt.Println(i, c) // 输出unicode值而不是字符
}
// 遍历命令行参数
for i, s := os.Args {
}
6.集合(Map)
var mapName map[keyType]valueType = make(map[keyType]valueType)
// 插入键值对
mapName["key1"] = "value1"
// 根据key读取value
value, exist := mapName["key"] // exist为布尔变量
// 删除一组kv
delete(mapName, "key1")
五、实用
1.数据类型转换
var sum int = 17
var count int = 5
var mean float32
mean = float32(sum) / float32(count)
2.接口
type Animal interface {
call(nums int) bool
}
type Cat struct {
name string
voice string
}
// 接口方法的实现函数有特殊写法
func (cat Cat) call(nums int) bool {
fmt.Print(cat.name + "叫:")
for i := 0; i < nums; i++ {
fmt.Print(cat.voice)
if i < nums - 1 {
fmt.Print(", ")
}
}
fmt.Println()
return true
}
var cat Animal = Cat{"Hua", "Miao"}
ok := cat.call(2)
fmt.Println(ok)
cat2 := new(Cat)
cat2.name = "Shabi"
cat2.voice = "Aba"
cat2.call(3)
3.错误处理
利用内置的error接口。
// 返回错误信息
func sqrt(f float) (float, error) {
if f < 0:
return -1, errors.New("Input a negative number")
...
}
//
panic、defer和recover
内置函数,用于异常处理。panic用于抛出错误,会终止其后面代码的运行。若panic函数中存在defer列表,会逆序执行。recover可用来获取panic传出的错误信息,执行一些应对措施。以下代码来源于这里。
func test() {
fmt.Println("c")
defer func() { // 必须要先声明defer,否则不能捕获到panic异常
fmt.Println("d")
if err := recover(); err != nil {
fmt.Println(err) // 这里的err其实就是panic传入的内容
}
fmt.Println("e")
}()
f() //开始调用f
fmt.Println("f") //这里开始下面代码不会再执行
}
func f() {
fmt.Println("a")
panic("异常信息")
fmt.Println("b") //这里开始下面代码不会再执行
}
func f1() {
defer func(){
if err := recover() ; err != nil {
fmt.Println(err)
}
}()
defer func(){
panic("three")
}()
defer func(){
panic("two")
}()
panic("one")
}
执行 test,先打印“c”,调用f,打印“a”,随后引发panic,进入defer,打印“d”和携带的错误信息“异常信息”,最后打印“e”,结束test函数。
执行f1,panic逆序触发另两个panic,但recover只捕获最后一次panic,因此只打印“three”。
函数结束时,即便未触发panic,也会逆序处理函数内声明的defer。
4.并发
使用go关键字开启协程(轻量级线程),主协程结束后,所有协程都会结束。
go f(x, y, z)
使用管道(Channel)在协程之间通信,管道相当于线程安全的FIFO队列。
ch := make(chan int32, 5) // 也可以make(chan int32)表示不带缓冲区
ch <- 1
ch <- 2
num := <-ch
fmt.Println(num) // 1
num = <-ch
fmt.Println(num) // 2
fmt.Println(len(ch)) // 0
close(ch)
无缓冲区的管道,发送方会阻塞直到接收方获取值,同步。若有缓冲区,接收方会阻塞直到管道中有值,异步。
管道关闭后数据不会丢失,但使用range遍历时将不会阻塞等待新数据进入,同时写入也会报错。
// 权限控制
go func(c chan int) { //读写均可的channel c } (a)
go func(c <- chan int) { //只读的Channel } (a)
go func(c chan <- int) { //只写的Channel } (a)