实习了一段时间深感java已经不太行了,转go才是出路

go

变量,结构与函数

变量

与java不同,go可以不显示声明变量数据类型。一般来说有:

  • 整数型int,int8,int16,int32,int64,无符号整型uint,uint8,uint16,uint32,uint64
  • 浮点型:float32,float64,相当于单精度浮点型float和双精度浮点型double
  • 布尔:true和false,值得一提的是bool赋初始值的时候默认false
  • 字符串:string直接就是一个数据类型了,还有一个rune,后续再去了解

很重要的一点是go语言的变量声明了就必须得用,而且最后不用加分号。下面是声明数据类型的例子,有:=可以代替var进行类型推断,可以同时推断多个类型(但我觉得还是显示声明类型比较好,否则一个函数返回来怎么判断类型)

1
2
3
4
5
6
7
8
9
10
11
12
13
var age_1 uint8 = 31
var age_2 = 32
age_3 := 33
fmt.Println(age_1, age_2, age_3)

var age_4, age_5, age_6 int = 31, 32, 33
fmt.Println(age_4, age_5, age_6)

var name_1, age_7 = "Tom", 30
fmt.Println(name_1, age_7)

name_2, is_boy, height := "Jay", true, 180.66
fmt.Println(name_2, is_boy, height)

常量也是类似,可以进行类型推断,但是必须赋初始值,且一旦定义了就不能改变了,类似java中的private static final int = 1;这种

函数与判断结构

go的函数与主流编程语言类似,但是估计不分static和非静态,也是给出参数列表和返回值。但是这里可以返回多个变量,这一点应该会比java好

1
2
3
4
5
6
7
8
9
10
11
12
func main() {
var numerator int = 11
var denominator int = 2
var result, remainder int = intDivision(numerator, denominator)
fmt.Println(result, remainder)

}
func intDivision(num1, num2 int) (int, int) {
var result int = num1 / num2
var remainder int = num1 % num2
return result, remainder
}

注意这里的异常处理,与java中try-catch的思想不同,函数在返回的时候也会给一个error返回值,外部调用通过error是否为nil来判断函数执行是否出错。这是一个广泛使用的设计思想,后续可能需要遵守。

例如这里的除数为0的例子,当除数为0相当于要抛出异常,用errors包下的一个函数throw new RunTimeException(),再在主函数去判断这个error是否为空,为空说明没有抛出异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package main

import (
"errors"
"fmt"
)

func main() {
var numerator int = 11
var denominator int = 0
result, remainder, err := intDivision(numerator, denominator)
//执行正常
if err == nil {
fmt.Println(result, remainder)
} else {
fmt.Println(err)
}

}
func intDivision(num1, num2 int) (int, int, error) {
var err error
if num2 == 0 {
err = errors.New("num1 is zero")
return 0, 0, err
}
var result int = num1 / num2
var remainder int = num1 % num2
return result, remainder, err
}

最后提一下go的判断结构,if后面的括号必须贴着同一行,else的哪一行必须写成”} else {“,否则编译器会报错,此外switch语句不需要写break了

数组,切片和哈希表

数组

go中的数组跟java的数组很像,但是go的数组可以操作指针。数组的大小在声明的时候就已经固定,如果想用跟ArrayList那样的动态数组,请使用slice切片。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func main() {
var intArr [10]int32
for i := 0; i < len(intArr); i++ {
intArr[i] = int32(i + 1)
}
//左闭右开,这个就是下标为456的三个元素
fmt.Println(intArr[4:7])

//地址
for i := 0; i < len(intArr); i++ {
fmt.Println(&intArr[i])
}
//这里输出结果是连续的4B,说明经典数组是连续分布的
}

值得注意的是这里数组如果传入的是形式变量,需要传地址,跟c语言一样,否则就只会改变形参

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func main() {
var arr = [5]int{1, 2, 3, 4, 5}
withAddress(&arr)
fmt.Println(arr)//[1 20 3 4 5]
noAddress(arr)
fmt.Println(arr)//[1 20 3 4 5]
}

func withAddress(a *[5]int) {
a[1] = 20
}
func noAddress(a [5]int) {
a[3] = 20
fmt.Println(a)//[1 20 3 20 5]
}

切片slice

基本上跟ArrayList的机制一样,长度和容量,如果到了设定阈值就会动态扩容。如果能够预估业务数据量,在构造slice的时候直接指定容量可以免去动态扩容的开销。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package main

import "fmt"

func main() {
sliceDynamic()
}

// 静态方法创建
func sliceStatic() {

var slice []int32 = []int32{1, 2, 3}
//length is 3, with capacity is 3
fmt.Printf("length is %v, with capacity is %v\n", len(slice), cap(slice))
slice = append(slice, 4)
//length is 4, with capacity is 6
fmt.Printf("length is %v, with capacity is %v\n", len(slice), cap(slice))
fmt.Println(slice)
}

// 动态方法创建
func sliceDynamic() {
//可以指定构造长度和容量,这里构造了一个长度为3,容量为20的slice
var intSlice []int32 = make([]int32, 3, 20)
fmt.Printf("length is %v, with capacity is %v\n", len(intSlice), cap(intSlice))
//前三个元素是初始化了的,后面没有
for i := 0; i < len(intSlice); i++ {
fmt.Println(intSlice[i])
}
//就跟ArrayList一样,如果业务能够预估动态数组的长度,最好还是构造的时候就提前设定好
//否则会频繁进行扩容,影响效率
}
//取数逻辑。左闭右开,跟java中subString类似
func slicePartition() {
sli := []int{1, 2, 3, 4, 5, 6}
fmt.Printf("len=%d cap=%d slice=%v\n", len(sli), cap(sli), sli)

fmt.Println("sli[1] ==", sli[1])
fmt.Println("sli[:] ==", sli[:])
//sli[1]->sli[len-1]
fmt.Println("sli[1:] ==", sli[1:])
//sli[0]->sli[4-1]
fmt.Println("sli[:4] ==", sli[:4])
//sli[0]->sli[3-1]
fmt.Println("sli[0:3] ==", sli[0:3])
fmt.Printf("len=%d cap=%d slice=%v\n", len(sli[0:3]), cap(sli[0:3]), sli[0:3])
}

循环

go语言中没有while循环(反正我也没经常用),对于slice和map需要特别注意,range关键字会在遍历这两个数据结构的时候进行处理。例如slice通过range关键字的时候会有index和value两个值,不需要index则直接”_”,跟python相似;同理map会遍历出key和value

这里就顺带把map提一下,map[key]value,这样的结构,查询一个元素直接括号里面找,注意找不到也会返回0这个默认值,所以以后用到的时候可能需要判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func sliceWithClass() {
var teachers []Teacher = make([]Teacher, 0)
teachers = append(teachers, Teacher{"yangyifan", 12})
fmt.Println(teachers)

var teacherMap map[string]Teacher = make(map[string]Teacher)
teacherMap["yangyifan"] = Teacher{"yangyifan", 12}
fmt.Println(teacherMap["yangyifan"])
teacherMap["xuxuanyan"] = Teacher{"xuxuanyan", 12}
fmt.Println(teacherMap["xuxuanyan"])
delete(teacherMap, "xuxuanyan")
fmt.Println(teacherMap["xuxuanyan"])

//遍历数组的时候返回两个值,一个是index一个是值
//不需要的就直接_
for _, teacher := range teachers {
fmt.Println(teacher)
}
//遍历map的时候每次都会得到两个值,一个是key一个是value
//这里只希望返回所有的value前面的key就用_代替
for _, teacher := range teacherMap {
fmt.Println(teacher)
}
}

string与rune

在go中string的底层是一个字节数组,采用utf8编码,由于utf8是不固定长度的,一般来说汉字都会占3B。所以直接去用len一个string数组长度返回的是字节数量,有两种遍历方式,一种是直接遍历len,这样会返回每一个未解码的utf8字节,比如一个汉字“大”,占三位,用普通遍历就会返回这三个字节的初始值;但是如果用range关键字,他会帮我们做一些处理,把这三个字节解码拼成一块,就会返回真实的字符,但这样前面的index仍然不准确。

如果采用rune就是我们直觉上的遍历字符数组了,例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
package main

import "fmt"
import "strings"

func main() {
//stringByte()
runeSlice()
stringBuilder()
}
func stringByte() {
var str1 = "大连理工大学"
char := str1[0]
fmt.Println(char)
//大连理工大学 has 18 character
fmt.Printf("%v has %v character\n", str1, len(str1))

//index: 0, char:å
//index: 1, char:¤
//index: 2, char:§
//index: 3, char:è
//index: 4, char:¿
//index: 5, char:ž
//index: 6, char:ç
//index: 7, char:
//index: 8, char:†
//index: 9, char:å
//index: 10, char:·
//index: 11, char:¥
//index: 12, char:å
//index: 13, char:¤
//index: 14, char:§
//index: 15, char:å
//index: 16, char:­
//index: 17, char:¦
//这里出现乱码的原因是字符串底层是一个字节数组结构,
//而一个汉字在utf8中占3个字节,他把每一个字节的内容都输出,就不会组成一个完整的汉字
for i := 0; i < len(str1); i++ {
fmt.Printf("index: %d, char:%c\n", i, str1[i])
}

//如果用range关键字就会帮我们把字符串的utf8解码
//index: 0, char:大
//index: 3, char:连
//index: 6, char:理
//index: 9, char:工
//index: 12, char:大
//index: 15, char:学
//这里也可以看到跳过了一些index
for index, v := range str1 {
fmt.Printf("index: %d, char:%c\n", index, v)
}

}
func runeSlice() {
var runeSlice = []rune("大连理工大学")
//index: 0, char:大
//index: 1, char:连
//index: 2, char:理
//index: 3, char:工
//index: 4, char:大
//index: 5, char:学
//这里的index就是顺序的了
for i := 0; i < len(runeSlice); i++ {
fmt.Printf("index: %d, char:%c\n", i, runeSlice[i])
}

}

此外一些string操作都在strings这个包下面,例如stringBuilder和其他的一些字符串操作,需要的时候导入。

结构体与接口

go应该是一个面向过程的语言,这里采用的还是结构体,但是类似的也有接口和类方法,在实现go中的接口时,不需要有java那种implements,编译器搜索所有有该方法签名的结构体自动绑定;在实现类方法时,需要在方法名前绑定结构体。例子如下所示,有一个engine接口,两个实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package main

import "fmt"

// Engine 接口
type Engine interface {
milesLeft() uint8
}

type GasEngine struct {
mpg uint8
gallons uint8
}

func (ge GasEngine) milesLeft() uint8 {
return ge.mpg * ge.gallons
}

type ElectricEngine struct {
mpkwh uint8
kwh uint8
}

func (ee ElectricEngine) milesLeft() uint8 {
return ee.mpkwh * ee.kwh
}

func canMakeIt(engine Engine, remainMiles uint8) bool {
if engine.milesLeft() < remainMiles {
return false
} else {
return true
}
}
func main() {
var milesLeft uint8 = 60
var ge GasEngine = GasEngine{10, 20}
fmt.Println(canMakeIt(ge, milesLeft))
var ee ElectricEngine = ElectricEngine{10, 5}
fmt.Println(canMakeIt(ee, milesLeft))
}