Go

概述:Go

[TOC]

1.Go语言

Go语言也称 Golang,兼具效率、性能、安全、健壮等特性,部署简单,Go编译生成静态可执行文件,除glibc外无其他外部依赖,可直接编译成机器码,语言层面支持并发执行,内置runtime,支持垃圾回收,含丰富的标准库,可跨平台编译。

Go语言适合用于服务器编程,处理日志、数据打包、虚拟机处理、文件系统,网络编程,云平台开发等,另外可基于Go实现Web开发、内网穿透、高性能爬虫等等。

2.Go语法示范网站

(1)Example:https://gobyexample.com/

(2)Video-尚硅谷韩顺平Go语言核心编程:https://www.bilibili.com/video/av35928275

3.Hello World

1
2
3
4
5
6
7
8
package main
//package main表示一个可独立执行的程序,每个 Go 应用程序都包含一个名为 main 的包。
import "fmt"
//fmt 包实现了格式化 IO(输入/输出)的函数
func main() {
//main 函数是每一个可执行程序所必须包含的,一般来说都是在启动后第一个执行的函数(如果有 init() 函数则会先执行该函数)。
fmt.Println("hello world")
}

4.变量和静态变量、变量类型

4.1 使用var关键字声明变量,可不设置变量类型,标准格式var 变量名 变量类型 = 变量值,可同时声明多个变量,无须分号结尾,且变量必须使用/输出,在函数外声明变量为全局变量,另外Golang严格区分大小写,“_”为特殊标识符,成为空标识符,它对应值通常会被忽略,通常被用于占位符使用,特例:变量名可以用int、float32,开发不建议使用,程序可编译通过

Tip:全局变量不能在函数外赋值(不等同于初始化)

1
2
3
4
var a = 100 // 可执行
var b string // 可执行
b = "111" // × 语法错误
name := "jun" // ×语法错误(左侧变量不应该是已声明过的)

4.2 静态变量可以不使用/输出,使用关键字const声明静态变量

4.3 变量类型含基本数据类型:

数值型:(int,int{x},unint,uint{x},byte,float32,float64)、字符型、布尔型、字符串型;

类型 有无符号 占用存储空间 数范围
int{x} {x}*1字节|或理解为x位 -2^{x}~(2^{x}-1)
uint{x} {x}*1字节 0~(2^{x+1}-1)
单精度float32 4字节 -3.403E38~3.403E38
双精度float64 8字节 -1.798E308~1.798E308

Go语言字符使用UTF-8编码,英文字母为1个字节,中文为3个字节

派送/复杂数据类型:指针、数组、结构体、管道、函数、切片、接口、集合

另外一种划分方式:值类型和引用类型

值类型:基本数据类型int系列、float系列、bool、string、数组、结构体。|| 变量直接存储值,内存通常在栈中分配;

引用类型:指针、slice切片、map集合、管道、interface。|| 变量存储地址,该地址对应空间为真正存储数据值,内存通常在堆分配,当没有任何变量引用该地址,则地址对应的数据空间为垃圾地址,需要进行垃圾回收

通常约定:变量名、函数名、常量名的首字母大写则可以被其他包访问,若首字母小写则在本包使用,即只有首字母大写的变量才可以被其他包使用

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
var a = "example"
var b,c int = 123,222
o := 1
var d,e,f = 100,"eee",888 //一次性声明多个变量
g,h,j := -11,true,"aaa" //一次性声明多个变量
var(
n1 = 100
n2 = 200
)//一次性声明多个变量
const s = "ssssss"

var sss = "hello"
sss[0] = "a"//在Go中string不可变,该语句ERROR
aaa :=`
func zeroptr(iptr *int) {
*iptr = 0
}`
fmt.Println(aaa)//字符串中包含关键字符时可使用反引号\`

fmt.Printf("n1类型%T\n",n1)//查看数据类型(注意是Printf而不是Println)
fmt.Printf("n1占用字节数:%d",unsafe.Sizeof(n1))//查看变量占用的字节大小(需导入unsafe包)

var c1 byte = "a"
var c2 byte = "0"
fmt.Println("c1=",c1)#直接输出byte即输出ASCII值 a=>97
fmt.Println("c2=",c2)#0=>48
fmt.Printf("c1=%c,c2=%c",c1,c2)#格式化输出
var c3 byte = "好"
fmt.Println("c3=",c3)#ERROR:overflow byte
var c4 int = "好"
fmt.Println("c4=",c4)#输出ASCII值

var a int
var b float32
var c float64
var d bool
var name string
fmt.Println("a=%d,b=%f,c=%f,d=%v,name=%v") //格式化输出

//在工作目录调用其他go文件中的变量
//起初已经在另外一个文件夹model中新建util.go并定义变量HeroName
(util.go)var HeroName string = "aaaaaa"

(hello.go)import ("awesomeProject/model")
(hello.go)fmt.Println(model.HeroName)

5.批量导入函数

1
2
3
4
5
6
7
8
import (
"fmt"
"math"
)
func main(){
const n = 6.28
fmt.Println(math.Cos(n))
}

6.强制转换类型

任何类型间转换均需要强制转换

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
var a = 6.66
fmt.Println(int64(a))//int、float之间均参考该格式

//其他基本数据类型转string
//方法一:使用fmt.Sprintf("%参数",表达式),生成格式化的字符串
var num1 int = 99
var num2 float32 = 2.45
var b bool = true
var str string

str = fmt.Sprintf("%d",num1)
fmt.Println(str)

str = fmt.Sprintf("%f",num2)
fmt.Println(str)
fmt.Printf("%T\n",str)//==>string类型

str = fmt.Sprintf("%t",b)
fmt.Println(str)
//方法二:使用strconv包的函数
//strconv的Format*方法有:FormatBool、FormatFloat(float64类型形参)、FormatInt(int64类型形参)
var num3 int= 99
var str string
str = strconv.FormatInt(int64(num3),10)//第2个参数base\=10表示10进制,其范围在2-36之间
fmt.Printf("str's type is %T, str's value is %s\n",str,str)
var num4 float64 = 9.22
str = strconv.FormatFloat(num4,10,64)//第2个'f'表示格式,第3个参数表示小数10保留10,第4个64表示float64
//strconv包有一个函数Itoa也可以转换int到string,处理int类型而不是int64
var num5 int = 4567
str = strconv.Itoa(num5)

//string转其他基本数据类型
//使用strconv中的Parse*方法
//ParseBool、ParseFloat、ParseInt、ParseUint
//String转Bool
var str string = "true"
var b bool
b,_ = strconv.ParseBool(str)//返回两个值,第一个为返回值,第二个为错误ERROR
fmt.Printf("b's type is':%T,b's value is'%t",b,b)
//Output:b's type is':bool,b's value is'true

//String转Int
var str1 string = "11"
var n1 int64
n1,_ = strconv.ParseInt(str1,10,64)//第2个参数为10进制,第3个参数为int64
fmt.Printf("n1's type is':%T,n1's value is'%d",n1,n1)
//Output:n1's type is':int64,n1's value is'11

//String转Float
var str2 string = "22.22"
var n2 float64
n2,_ = strconv.ParseFloat(str2,64)//第2个参数为float64
fmt.Printf("n2's type is':%T,n2's value is'%f",n2,n2)
//Output:n2's type is':float64,n2's value is'22.220000

7.For循环

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
//在for中使用之前定义/声明的变量
i := 1// 循环变量初始化
for i <= 3{
fmt.Println(i)
i = i+1//使用i++也可以代替该语句
}

//在for中定义/声明变量
for j:=0;j<2;j++{
fmt.Println(j)
}

//for;;循环(通常配合break)
var k = 1
for {
if k >5{
fmt.Println("k>5")
break
}
k++
fmt.Println(k)
}

//字符串遍历-for(For循环是以字节读取,而对于字符串中含中文时会出现乱码问题,原因在于中文字符在utf-8编码中对应3个字节),如果强制使用for循环,可使用切片将string类型转成[]rune切片即可,但是转换后需要定义新变量

var str string = "helloWorld"
for i:=0;i<len(str);i++{
fmt.Printf("%c\t",str[i])
}

var str2 string = "helloWorld哈喽"
str3 := []rune(str2)//转成[]rune切片
for i:=0;i<len(str3);i++{
fmt.Printf("%c\t",str3[i])
}

//字符串遍历-range,该方法返回index和value,不使用index则用"_"存储,该方法使用字符遍历,因此遇到中文字符不会出现乱码
for _,value := range str{
fmt.Printf("%c\t",value)
}

Tip1:可使用break永久跳出循环

Tip2:可使用continue跳跃该次循环

8.判断IF/ELSE

1
2
3
4
5
6
var a,b = 100,10
if a > b{
fmt.Println("a > b")
} else {
fmt.Println("a <= b")
}

9.Switch

Golang中可在一个case后可有多个表达式,使用”,”间隔

Tip1:表达式可以是常量值、变量、一个有返回值的函数之一均可

Tip2:case后的表达式和switch后接表达式的数据类型必须一致

Tip3:case后的表达式不能重复

Tip4:default不是必填项

Tip5:switch后可定义/声明变量,但需要后接“;”(不推荐)

Tip6:switch穿透-fallthrough,如果在case语句后增加fallthrough则会继续执行下一个case,称为switch穿透(默认穿透1层)

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
a := 10
switch a {
case 10,40:
fmt.Println("i = 10 or 40")
case 20:
fmt.Println("i = 20")
case 30:
fmt.Println("i = 30")
default:
fmt.Println("i not in (10,20,30,40)")
}

//switch有返回值的函数
func test(key int) int{
return key
}
switch test(10) {
case 10,40:
fmt.Println("i = 10 or 40")
default:
fmt.Println("i not in (10,40)")
}

//switch和case后接变量
var n1 int32 = 3
var n2 int32 = 5
switch n1 {
case n2:
fmt.Println("匹配n2")
default:
fmt.Println("不匹配n2")
}

//switch后声明变量
switch n1 := 100; {
case n1>90:
fmt.Println(">90")
default:
fmt.Println("<=90")
}

//switch穿透
var n1 int = 10
switch n1{
case 10:
fmt.Println("10")
fallthrough//(穿透一次执行下一个case的输出)
case 20:
fmt.Println("20")
default:
fmt.Println(">20 or <10")
}
//输出:10 \n 20

10.输出今日星期/小时

1
2
3
4
5
6
7
8
9
a := time.Now().Weekday()//星期
fmt.Println(a)
if a == time.Saturday{
fmt.Print("今天是周六")
}else{
fmt.Print("今天不是周六")
}
b := time.Now()//小时
fmt.Println(b.Hour())

11.函数

Go语言中函数分为自定义函数,系统函数

在Go使用interface()可用于向函数传递任意类型的变量,在函数内部,该变量仍为interface{}类型,注意函数的输入参数通常需要判断类型后再强制转换,不然易报错

函数格式:func 函数名(形参名 形参类型) 函数返回类型{…}

规范:通常不会将项目所需要所有函数写入mian文件,一般写入utils.go,该go文件专门用于定义函数,由main或者其他文件调用,其中会被其他文件引用的函数名必须首字母大写,或理解为该函数可导出

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
// 使用interface不定义形参类型
myfunc := func(i interface{}) {
switch i.(type) {
case string:
fmt.Println("string",i.(string))
case int:
fmt.Println("int",i.(int))
default:
fmt.Printf("others")
}
}
myfunc(100)
myfunc("abc")

// 明确函数的形参类型
myfunc2 := func(a int,b int){
fmt.Println(a+b)
}
myfunc2(1,2)

// 定义多个形参类型
myfunc3 := func(a, b, c int) int {
return a + b + c
}

// 在外部定义函数(必须在main函数外定义)
func plus1(a, b, c int) int {
return a + b + c
}

// 在外部定义函数,多个返回值(必须在main函数外定义)
func plus2(a, b, c int) (int,string) {
return a + b + c,"sum"
}

关于上述中调用utils文件函数截图举例:

Tip1:注意通常导入时文件包名和文件夹名一致,即utils.go所在的文件夹名为utils,则文件中第一行规范书写为packge utils,一般为小写,package后的包名不是必须与包名一致,不过建议一致,不然影响开发效率

Tip2:导入时语法为包名.函数名

Tip3:给包名其别名,将utils更改为util,其原名则失效,使用方法则为别名.函数名

1
import util "awesomeProject/utils"

Tip3:将go文件编译成可执行文件,需要将包声明为main,即package main,在$GOPATH执行

举例:个人GOPATH在C:\Users\Junming\go下,则在该目录下启动CMD,而目标文件在C:\Users\Junming\go\src\awesomeProject\main\helloworld.go,在执行命令时直接以src下定位,命令如下

1
C:\Users\Junming\go>go build awesomeProject\main

编译后生成一个有默认名的可执行文件,在$GOPATH目录下,也可以指定名字和目录,以上命令执行后会在GOPATH下生成一个main.exe,同时可自定义设置存储名字与路径:

1
C:\Users\Junming\go>go build -o bin/my.exe awesomeProject\main

Tip:以上执行后其实会在pkg文件夹中...\下生成utils.a,该文件为库文件(二进制文件)

函数调用机制底层解析:

栈区:基本数据类型

堆区:引用数据类型

(1)调用函数时会分配一个新的空间,编译器会将该函数对应的栈与其他栈进行区分

(2)当一个函数被调用完后,程序会自动销毁掉该函数对应的栈空间

函数形参调用

(1)值传递(n1 int)

(2)引用传递(n1 *int)(地址拷贝,效率更高,其值传递数据越大,消耗越大)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func test1(n1 int){
n1 = n1 + 10
fmt.Println(n1)
}
func test2(n1 *int){
*n1 = *n1 + 10
}
func main(){
num := 20
test1(num)
fmt.Println(num)
test2(&num)
fmt.Println(num)
}

(3)函数作为形参

以下abc函数作为myFun函数的形参传入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func getSum(n1 int,n2 int)int{
return n1+n2
}
// 当多个函数形参类型一致时可以修改(函数功能同上)
//func getSum(n1,n2 int)int{
// return n1+n2
//}

func myFun(abc func(int,int) int,num1 int, num2 int)int{
return abc(num1,num2)
}
func main(){
res := myFun(getSum,50,60)
fmt.Println(res)
}

可变函数:形参数量不预先预定,需要注意的是可变形参可以和固定形参一起使用,不过可变参数必须为形参列表的最后一项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func sum(nums ...int) int{
res := 0
for _,v :=range nums{
res+=v
}
return res
}

func sum2(nums ...int)int{
sum := 0
for i:=0;i<len(nums);i++{
sum+=nums[i]
}
return sum
}

func main(){
res := sum(1,2,3,4)
fmt.Println(res)
res2 := sum2(1,2,3,4)
fmt.Println(res2)
}

12.数组Array

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
70
71
72
// 数组的地址可以通过数组名获取,数组的地址也是第一个元素的地址,第二个元素的地址是第一个元素的地址+数组类型所占字节数(int占8字节)
var a [3] int
fmt.Printf("a的地址=%p,a[0]的地址=%p",&a,&a[0]) //%p取变量地址

var a [5]int
fmt.Println(a)
//声明int类型数组初始化为全0,Output:[0 0 0 0 0]

// 赋值方法一
b := [5]int{1,2,3,4,5}
// 赋值方法二(不定义具体数量,固定三个点...)
b := [...]int{1,2,3,4,5}
// 赋值方法三(下标1值为800,下标0值为900)
b := [...]int{1:800,0:900}
// 赋值方法四
a[0] = 10
a[1] = 20

// 定义/声明二维数组
var tD [2][3]int
for i:=0;i<2;i++{
for j:=0;j<3;j++{
tD[i][j] = i+j
}
fmt.Println(tD)
}
//二维数组直接初始化
var tD [2][3]int = [2][3]int{{1,2,3},{2,3,4}}
// 或者
var tD [2][3]int = [...][3]int{{1,2,3},{2,3,4}}
// 或者
var tD = [2][3]int{{1,2,3},{2,3,4}}
// 或者
var tD = [...][3]int{{1,2,3},{2,3,4}}
fmt.Println(tD)

// 方式1 二维数组的遍历
for i:=0;i<len(tD);i++{
for j:=0;j<len(tD[i]);j++{
fmt.Print(tD[i][j],"\t")
}
fmt.Println()
}
// 方式2 二维数组的遍历
for _,v1:=range tD{
for _,v2:=range v1{
fmt.Print(v2,"\t")
}
fmt.Println()
}

a := [4]int{1,2,3,4}
// 数组遍历一(注意格式化输出使用Printf)
for _,value := range a{
fmt.Printf("%d\t",value)
}

// 数组遍历二
for i:=0;i<len(a);i++{
fmt.Printf("%d\t",a[i])
}

// 引用传递,修改数组的值
func change(a *[4]int){//注意数组长度必须一致,也是数据类型的一部分
(*a)[0] = 88
}

func main(){
a := [4]int{1,2,3,4}
change(&a)
fmt.Printf("%v",a)
}

13.分片Slices

关键字make() 只用于映射、切片和程道,不返回指针,即可以不初始化具体元素值

1
2
3
4
5
6
a := make([]string, 3)
b := make([]string,len(a))
copy(b,a)//复制数组
// 数组分片
c := [3]int{1,2,3}
fmt.Println(c[1:])

14.集合Map

关键字map实现键值对

1
2
3
4
5
6
7
8
9
m := make(map[string]int)
m["k1"] = 7
m["k2"] = 11
fmt.Println(m)
delete(m,"k1")//删除某键值对
_, prs := m["k2"]//第二个返回值返回该键值是否存在,而第一个返回值不是关注内容,可用_代替
fmt.Println("prs:", prs)
n := map[string]int{"foo": 1, "bar": 2}
//定义map格式:map[key-type]value-type{key-real:value-real}

15.范围Range

1
2
3
4
5
6
7
8
9
10
//int数组
nums := []int{2,3,4}
for index,num := range nums{
fmt.Println(index,num)
}
//map集合(键值对)
kvs := map[string]string{"a": "apple", "b": "banana"}
for k, v := range kvs {
fmt.Printf("%s -> %s\n", k, v)
}

16.生成随机数

1
2
3
4
//表示生成0-99之间的数,但是每次生成为伪随机数,即多次重复运行均为同一值,因此需要提前设置一个种子,即Seed方法,返回1970.01.01 /00:00:00到此时的秒数/纳秒
rand.Seed(time.Now().Unix())//秒数
rand.Seed(time.Now().UnixNano())//秒数
rand.Intn(100)

17.匿名函数

包含匿名函数的函数返回类型为func(),格式:func 函数名() func() 匿名函数返回类型{…}

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
// 方式1:在main中定义匿名函数
func main(){
res1 := func(n1 int,n2 int)int{
return n1+n2
}(10,20)
fmt.Println(res1)
}

// 方式2:把匿名函数赋给一个变量(函数变量),再通过变量调用匿名函数
func main(){
a:= func (n1 int,n2 int)int{
return n1 - n2
}
res1 := a(10,30)
fmt.Println("res1=",res1)
}

// 方式3:全局匿名函数:将匿名函数
var (
fun1 = func(n1 int,n2 int)int{
return n1 + n2
}
)
func main(){
res := fun1(1,2)
fmt.Println("res=",res)
}

18.指针

指针类型:指针变量存的是一个地址,该地址指向的空间才是值,指针本身也有地址

int类型:int变量存的是一个int值(与指针类型作对比)

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var num int = 10
fmt.Println("num的地址为:",&num)

var ptr *int = &num
fmt.Println("ptr为:",ptr)
fmt.Println("ptr指向的值:",*ptr)

//解释var ptr *int = &num
//解释1:ptr是一个指针类型的变量
//解释2:ptr类型是*int
//解释3:ptr本身值维&num
//num = 10
//ptr = num的存储地址

//num的地址为: 0xc04203c1d0
//ptr为: 0xc04203c1d0
//ptr指向的值: 10

*变量名表示该变量的指针,&变量名读取变量的内存地址(即地址),例如示范代码中int类型变量i值为1,&i表示变量i的内存地址,在zeroptr函数中形参为指针类型,

1
2
3
4
5
6
7
func zeroptr(iptr *int) {
*iptr = 0
}
func main(){
i := 1
zeroptr(&i)
}

练习1:获取一个int变量num的地址并显示

练习2:将num地址赋给ptr并通过ptr修改num值

1
2
3
4
5
6
var num int = 20
var ptr1 *int = &num
fmt.Println(ptr1)

*ptr1 = 30
fmt.Println(num)

19.运算符%

a % b = a - a / b * b

1
2
3
4
10 % 3 = 1
-10 % 3 = -10 - (-10) / 3 * 3 = -10 +9 = -1
10 % -3 = 1
-10 % -3 = -1

20.逻辑与/或

不仅可以针对变量值判断之外,还可以对函数返回值作为逻辑与/或的判断

1
2
3
4
5
6
7
8
func test() bool{
return true
}
func main(){
if test() {
fmt.Println("true")
}
}

21.结构体及方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type person struct {
name string
age int
sex string
}

func newPerson(name string,age int) person{
p:= person{name:name,age:age}
p.sex = "male"
return p
}

func main() {
fmt.Println(newPerson("John",18))
}

结构体方法格式:func (结构体名 结构体类型) 方法名() 返回类型{…}

Tips:结构体方法中对结构体类型前是否添加*对结果不影响

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
type rect struct {
width, height int
}

func (r rect) area() int {
return r.width * r.height
}

// 使用*结构体类型效果一致
//func (r *rect) area() int {
// return r.width * r.height
//}

func (r rect) perim() int {
return 2*r.width + 2*r.height
}

func main() {
r := rect{width: 10, height: 5}
fmt.Println("area: ", r.area())
fmt.Println("perim:", r.perim())
}

22.错误Error

在Go中,通过显式的、单独的返回值来传递错误是常见的现象,首先需要导入”error”,在函数返回类型添加error,

补充:nil在Go中表示空值

1
2
3
4
5
6
7
8
9
10
11
12
func f1(arg int) (int, error) {
if arg > 100 {
return -1, errors.New("more than 100")
}
return arg, nil
}
func main() {
fmt.Println(f1(120))
fmt.Println(f1(90))
}
//Output: -1 more than 100
//Output: 90 <nil>

23.获取用户终端输入

调用fmt包中的fmt.Scanf和fmt.Scanln,其中fmt.Scanln表示读取一行数据

1
2
3
4
5
6
7
8
9
10
11
12
13
// 方式一:使用Scanln
var name string
var age int
fmt.Println("input your name:")
fmt.Scanln(&name)//传入变量的地址(引用传递),则函数内的值会影响函数外的变量值,使用name无法执行该语句
fmt.Println("input your age:")
fmt.Scanln(&age)//传入变量的地址(引用传递),则函数内的值会影响函数外的变量值
fmt.Println(name,age)

// 方式二:使用Scanf
fmt.Println("Input your name,age by spacing off")
fmt.Scanf("%s %d",&name,&age)
fmt.Println(name,age)

24.进制输出

八进制:以数字0开头表示;

十六进制:以数字0x或者0X开头表示

1
2
3
4
5
6
7
8
9
// 二进制(使用格式化输出用Printf)
var i int = 5
fmt.Printf("%b\n",i)
// 八进制
var j int = 011
fmt.Println("j=",j)
// 十六进制
var m int = 0x11
fmt.Println("m=",m)

25.原码、反码、补码

1)二进制最高位是符号位:0表示正数,1表示负数

例如:1=>[0000 0001] -1=>[1000 0001]

2)正数的原码、反码、补码相同

3)负数的反码 = 原码符号位不变,其他位数取反

例如:1=>原码、反码、补码 [0000 0001]

-1=>原码[1000 0001],反码[1111 1110],补码[1111 1111](下一性质)

4)负数的补码 = 反码 + 1

5)0的反码、补码 = 0

6)计算机运算时以补码方式运算

26.移位运算

右移运算符>>:低位溢出,符号位不变,使用符号位补溢的高位

左移运算符<<:符号位不变,低位补0

例如:1>>2 // 0000 0001 => 0000 0000 = 0

例如:1<<2 // 0000 0001 => 0000 0100 = 4(左移2位)

27.自定义数据类型

基本语法:type 自定义数据类型 数据类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
type myInt int //myInt等价于int

// 自定义函数类型

// 1.定义函数
func getSum(n1 int,n2 int)int{
return n1+n2
}

// 2.自定义函数类型
type myFunType func(int,int)int

// 3.将定义函数作为另一个函数的形参
func myFun(abc myFunType,num1 int, num2 int)int{
return abc(num1,num2)
}

// 4.主函数:调用函数
func main(){
res := myFun(getSum,50,60)
fmt.Println(res)
}

28.init函数

每一个源文件都有一个init函数,当声明init函数和main函数时,init优先执行,通常在init完成初始化工作,如果文件中包含全局变量定义、init函数、main函数时,执行流程:变量定义->init函数->main函数

29.闭包

闭包:是一个函数与其相关的引用环境组合的一个整体(实体)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import "fmt"
func myfunc() func(int) int {
var n int = 10
return func(i int) int {
n = n + i
return n
}
}

func main(){
nextInt := myfunc()
fmt.Println(nextInt(1))// 10 + 1
fmt.Println(nextInt(2))// 11 + 2(主要原因在于myfunc中的变量n并不是每一次均被初始化,而是原基础变化)
fmt.Println(nextInt(3))// 13 + 3
}

//Output:
//11
//13
//16

其中myfunc返回的是fun (int) int,返回的是一个匿名函数,匿名函数引用到函数外的变量n,共同构成了整体

练习:

(1)编写一个函数makeSuffix(suffix string)可接收一个文件后缀名(比如.jpg),并返回一个闭包

(2)调用闭包,可传入一个文件名,如果该文件名没有指定的后缀(比如.jpg),则返回文件名.jpg,如果有.jpg后缀,则返回原文件名

(3)要求使用闭包方式完成

(4)strings.HasSuffix可以判断某个字符串是否有指定的后缀

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func makeSuffix(suffix string) func(string) string{
return func (name string) string{
// 如果name没有指定后缀,则加上,否则返回原来的名字
if strings.HasSuffix(name,suffix){
return name
}
return name + suffix
}
}
func main(){
f := makeSuffix(".jpg")
fmt.Println("文件名处理后=",f("winter"))
fmt.Println("文件名处理后=",f("bird.jpg"))
}
//返回函数与变量suffix组合成一个闭包,返回函数引用变量suffix
//闭包优势在于仅需要输入一次suffix(后缀)即可

30.函数关键字-defer

通常需要创建资源(例如:数据库连接、文件句柄、锁等),为了函数执行后及时释放资源,Go中提供了defer(延时机制)

(1)当执行defer时,暂时不执行,且会将defer后语句压入独立的栈(defer栈),当函数执行后才按照先入后出的方式执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func sum(n1 int,n2 int)int{
defer fmt.Println("n1=",n1)
defer fmt.Println("n2=",n2)
n1++
n2++
res := n1 + n2
fmt.Println("res=",res)
return res
}

func main() {
sum(10,20)
}

// 返回结果: res=32、n2=20、n1=10
// 原因在于在n1++、n2++前已经压入defer栈

(2)及时释放函数创建的资源(模拟代码)

1
2
3
4
5
6
7
8
9
10
11
func test(){
file = openfile("文件名")
defer file.close()
//其他代码
}

func test(){
connect = openDatabase()
defer connect.close()
//其他代码
}

31.系统常用函数——字符串

序号 函数 说明
1 len(str) 统计字符串长度
2 r :=[]rune(str) 转切片,字符串遍历,同时解决中文问题,需要重新定义变量
3 n,err := strconv.Atoi[“12”] 字符串转整数
4 str := strconv.Itoa(1234) 整数转字符串
5 var bytes = []byte(“hello go”) 字符串转[]byte
6 str := string([]byte{1,2,3}) []byte转字符串
7 str := strconv.FormatInt(123,2) 10进制转2,8,16进制,返回对应的字符串
8 strings.Contains(“sea”,”food”) 查找子串是否在指定的字符串
9 strings.Count(“cheese”,”e”) 统计一个字符串有几个指定的不重复的子串
10 fmt.Println(strings.EqualFold(“abc”,”ABC”)) 不区分大小写的字符串比较(==区分大小写)
11 strings.Index(“NLT_ABC”,”abc”) 返回子串在字符串第一次出现的index值
12 strings.LastIndex(“go golang”,”go”) 返回子串在字符串最后一次index
13 strings.Replace(“go go hello”,”go”,”go语言”,n) n可以指定你希望替换几个,如果n=-1全部替换,注意不会替换原字符串,而是返回一个新的字符串
14 strings.Split(“hello,world,ok”,”,”) 将一个字符串拆分成字符串数组,返回新数组
15 strings.ToLower(“Go”) 将字符串的字母进行大小写转换
16 strings.TrimSpace(“ abc aa “) 将字符串左右两边的空格去掉
17 strings.TrimLeft(“! hello!”,”!”) 将字符串左边指定的字符去掉//左边!和“ ”去掉
18 strings.TrimRight(“! hello!”,”!”) 将字符串右边指定的字符去掉//右边!和“ ”去掉
19 strings.HasPrefix(“ftp://“,”ftp”) 判断字符串是否以指定字符串开头
20 strings.HasSuffix(“ABac”,”AB”) 判断字符串是否以指定字符串结尾
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
// 3. 字符串转整数
n,err := strconv.Atoi("123")
if err!= nil{
fmt.Println("转换错误",err)
}else{
fmt.Println(n)
}
//如果非整数则返回:转换错误 strconv.Atoi: parsing "z123": invalid syntax

// 4. 整数转字符串
str := strconv.Itoa(12345)
fmt.Printf("str=%v,type=%T",str,str)

// 补充格式化输出
%v 表示按照相应的默认格式
%T 相应值的类型
%t 布尔值 truefalse
%b 二进制表示
%c 相应 Unicode 码点所表示的字符
%d 十进制表示
%o 八进制表示
%x 十六进制表示,字母形式为小写 a-f
%X 十六进制表示,字母形式为大写 A-F
%e 科学计数法,例如 -1234.456e+78
%E 科学计数法,例如 -1234.456E+78
%f 有小数点而无指数,例如 123.456
%s 字符串或切片的无解译字节

// 5.字符串转[]byte(byte的%v输出是对应的ASCII码)
var bytes = []byte("hello go")

// 13.n可以指定你希望替换几个,如果n=-1全部替换
strings.Replace("go go hello","go","go语言",n)

32.时间和日期相关函数

需要提前导入time包

时间的常量:

时间常量 定义
Nanosecond 纳秒
Microsecond 微秒
Millisecond 毫秒
Second
Minute 分钟
Hour 小时
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
//获取当前时间,分别获得年月日时分秒:now.Year()/Month()/Day()/Hour()/Minute()/Second()
now := time.Now()
fmt.Println(now)//2019-11-12 18:38:40.4767544 +0800 CST
fmt.Println(now.Year())//2019
fmt.Println(now.Month())//Novemeber
fmt.Println(int(now.Month()))//11

// 每隔0.1秒打印一个数字
i := 0
for {
i++
fmt.Println(i)
time.Sleep(time.Millisecond*100)
if i == 100{//10秒后退出
break
}
}

//Unix和UnixNano的使用
fmt.Printf("unix时间戳=%v,unixnano时间戳=%v",now.Unix(),now.UnixNano())

// 统计函数执行时间
func test(){
str := ""
for i:=0;i<100000;i++ {
str += "hello" + strconv.Itoa(i) //备注:strconv.Itoa()表示将整数转为字符串
}
}

func main(){
start := time.Now().Unix()
test()
end := time.Now().Unix()
fmt.Printf("执行test()耗费时间%v",(end-start))
}

33.内置函数

函数名 功能
new 分配内存,主要分配值类型,int,float32,struct,返回指针
make 分配内存,主要分配引用类型,char,map,slice
1
2
3
4
5
6
7
func main(){
num1 := 100
fmt.Printf("num1类型:%T,num1值:%v,num1的地址:%v\n",num1,num1,&num1)
num2 := new(int)
fmt.Printf("num2类型:%T,num2值:%v,num2的地址:%v,num2指向的值:%v",num2,num2,&num2,*num2)
}
//因此new有2个子过程:其一分配指针内存空间,其二再指向一个默认值

34.错误/异常的捕获和处理

Go中错误(panic)程序会退出(奔溃)
Go引用的处理方式:defer,panic,recover
Go可以抛出一个panic的异常,在defer中通过recover捕获这个异常,再正常处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func test(){
// 使用defer + recover捕获和处理异常
defer func(){//匿名函数
err := recover() // recover()内置函数,可以捕获异常
if err != nil{
fmt.Println("err=",err)
// 通常该函数的负责员(保证了主函数的执行)
}
}()
num1 := 10
num2 := 0
res := num1 / num2
fmt.Println(res)
}

func main(){
test()
fmt.Println("执行test()")//使用了defer+recover后仍可以执行该句
}

Go也支持自定义错误,使用errors.New和panic内置函数
1)errors.New(“错误说明”),会返回一个error类型的值,表示一个错误
2)panic内置函数,接收一个interface{}类型的值作为参数,可以接收error类型的变量,输出错误信息,并退出程序

1
2
3
4
5
6
7
8
func test2(){
err := readConf("config.ini")
if err !=nil{
//发生错误并输出错误,终止程序!
panic(err)
}
fmt.Println("执行完test2()")
}

35.四舍五入

1
2
3
4
h1 := 27.0
h2 := 5.0
h3 := fmt.Sprintf("%.2f",h1/h2)//保留2位小数
fmt.Printf("h3 = %v",h3)

36.切片Slice

(1)切片是数组的引用,切片是引用类型,在进行传递时,遵守引用传递的机制

(2)切片的使用和数组类似,遍历切片和访问切片的元素、求切片长度都是一样

(3)切片长度可以变换,即动态变换数组

(4)定义基本语法:var 变量名 [] 类型

(5)切片结构:

指向数组的地址 长度 容量
address(eg.0x0011) len cap

声明/定义切片三个方式:

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
// 方式一:定义一个切片,让切片引用已经创建好的数组
var myarr [5] int = [...]int{1,2,3,4,5}
myslice := myarr[1:3] // 引用myarr数组的第2个元素到第3个元素(左闭右开),即myarr[1]和myarr[2]
fmt.Println(myslice)
fmt.Println("切片容量:",cap(myslice))//切片容量是动态变化,cap全称为capability,cap容量取决于具体类型,其中切片容量一般是底层数组长度的两倍
// 切片由3部分组成:(1)指向数组的指针(2)切片长度(3)切片容量

// 方式二:通过make创建切片
// 基本语法:var 切片名 [] type = make([]type,len,[cap])
// 其中type数据类型 len大小 cap指定切片容量
func main(){
var myslice [] int = make([]int,4)// cap>len
fmt.Println(myslice)// 默认值为[0 0 0 0]
fmt.Println("slice len=",len(myslice),"slice cap=",cap(myslice))// slice len= 4 slice cap= 4
myslice[0] = 100
myslice[2] = 200
fmt.Println(myslice)// [100 0 200 0]
}

// 方式三:定义一个切片,直接指定具体数组,使用原理类似make
func main(){
var myslice [] int = []int {1,3,5}
fmt.Println(myslice)
}

区分:

方式1:是直接引用数组,该数组是预先存在的,程序员可见,数组和切片均可访问
方式2:通过make创建切片,make会创建一个数组,由切片在底层进行维护,程序员不可见,切片存放的指针指向内部创建的数组空间的第一个元素,只能通过切片访问元素

切片的遍历

1
2
3
4
5
6
7
8
9
10
11
var myarr [5]int = [...]int{1,2,3,4,5}
var myslice = myarr[1:3]
// 方式1 for循环
for i:=0;i<len(myslice);i++{
fmt.Println(myslice[i])
}

//方式2 for-range
for _,value:=range myslice{
fmt.Println(value)
}

切片简写:

1
2
3
var myslice = myarr[0:end] => var myslice = myarr[:end]
var myslice = myarr[start:len(arr)] => var myslice = myarr[start:?]
var myslice = myarr[0:len(arr)] => var myslice = myarr[:]

(1)cap是一个内置函数,用于统计切片容量,即最大可存放多少个元素
(2)切片定义完后不能使用,因为本身为空的,需要让其引用一个数组,或者make一个空间供切片使用
(3)切片可继续切片
(4)用append内置函数可以对切片动态追加,若切片有足够的容量,其目标会重新切片以容纳新的元素,否则会分配一个新的基本数组,append返回更新后的切片,因此必须存储追加后的结果,注意需要重新赋给切片

在切片上追加元素/切片:

1
2
3
4
5
var myslice [] int = []int{1,2,3}//定义切片
fmt.Println(myslice)
myslice = append(myslice,4,5,6) // 追加元素
myslice = append(myslice,myslice...) // 追加切片(注意要加...,另外仅能添加切片,不能添加数组)
fmt.Println(myslice)

切片append操作的底层原理分析:
(1)切片append操作的本质是对数组扩容
(2)go底层会创建新的数组newArr
(3)将slice原来包含的元素拷贝到新的数组newArr
(4)slice重新引用到newArr
(5)newArr是在底层维护,程序员不可见

切片的拷贝操作:

1
2
3
4
5
6
var myslice1 []int = []int{1,2,3,4,5}
var myslice2 = make([]int,10)//定义一个长度为10的切片
copy(myslice2,myslice1)//把myslice1拷贝给myslice2
fmt.Println(myslice1)//[1 2 3 4 5]
fmt.Println(myslice2)//[1 2 3 4 5 0 0 0 0 0]
// 注意myslice1和myslice2的空间是独立的,即修改myslice1的元素不会影响myslice2

在拷贝(copy)过程中,如果slice的长度不够时,会依据slice的真实长度拷贝对应长度,不会因为长度不够而报错

例如:

1
2
3
4
5
var a[]int = []int{1,2,3,4,5}
var myslice = make([]int,1)
fmt.Println(a)//[1 2 3 4 5]
copy(myslice,a)
fmt.Println(myslice)//[1]

切片是引用类型,因此在传递过程中,遵守引用机制

1
2
3
4
5
6
7
8
9
func test(myslice []int){
myslice[0] = 100
}
func main(){
var myslice[]int = []int{1,2,3,4,5}
fmt.Println(myslice)//[1 2 3 4 5]
test(myslice)
fmt.Println(myslice)//[100 2 3 4 5]
}

string底层是byte数组,因此string可进行切片处理

1
2
3
mystr := "helloworld"
myslice := mystr[5:]
fmt.Println(myslice)//world

string是不可变数据结构,不能赋值具体元素

1
mystr[0] = 'a'// × 编译失败 error 无法赋值

需要修改string可以通过先将string -> [] byte 或者 []rune -> 修改 -> 重转成string

1
2
3
4
5
mystr := "helloworld"
arr := []byte(mystr)
arr[0] = 'a'
mystr = string(arr)
fmt.Println(mystr)//aelloworld

但是以上方法仅能处理英语和数字,不能处理中文,1个byte按照1个字节处理,但一个汉字占3个字节,解决办法将string转成[]rune,按照字符处理,兼容汉字

1
2
3
4
5
mystr := "helloworld"
arr := []rune(mystr)
arr[0] = '汉'
mystr = string(arr)
fmt.Println(mystr)

斐波拉契数列(每个元素为前两个元素之和,第一个和第二个元素均为1)

1
2
3
4
5
6
7
8
9
10
11
12
func fbn(n int)([]int){
myslice := make([]int,n)
myslice[0],myslice[1] = 1,1
for i:=2;i<n;i++{
myslice[i] = myslice[i-1] + myslice[i-2]
}
return myslice
}
func main(){
myfbn := fbn(10)
fmt.Println(myfbn)
}

37.排序

排序分类:

(1)内部排序:将需要处理的所有数据加载在内存存储器进行排序(交换式排序、选择式排序、插入式排序)

交换式排序:冒泡排序、快速排序

(2)外部排序:数据量过大时无法全部加载到内存,需要借助外部存储进行排序(合并排序、直接合并排序)

冒泡排序实战:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func BubbleSort(arr *[5]int){
for i:=0;i<len(arr)-1;i++{
for j:=i+1;j<len(arr);j++{
if (*arr)[i]>(*arr)[j]{
arr[i],arr[j] = arr[j],arr[i]
}
}
}
}
func main(){
arr := [5]int{2,3,1,5,4}// 定义数组
BubbleSort(&arr)
fmt.Println(arr)//[1 2 3 4 5]
}

二分查找实战(必须是有序数组):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func binaryfind(arr *[7]int,leftindex int, rightindex int,target int){
// 判断leftindex是否大于rightindx
if leftindex > rightindex{
fmt.Println("找不到该元素")
return
}
mid := (leftindex+rightindex)/2
if (*arr)[mid] > target{
binaryfind(arr,leftindex,mid-1,target)
}else if (*arr)[mid] < target{
binaryfind(arr,mid+1,rightindex,target)
}else if (*arr)[mid] == target{
fmt.Printf("找到该元素,下标为%v",mid)
}
}

func main(){
arr := [7]int{1,2,3,4,5,6,7}
binaryfind(&arr,0,6,14)
}

38.map字段

map是key-value数据结构,又称为字段或者关联数组,类似于其他编程语言的集合/字典,

基本语法:var map 变量名 map[keytype] valuetype

key类型:int 、bool、string、指针、channel、接口、结构体、数组

key不支持的类型:切片、map、函数(无法使用==判断)

声明map不会分配内存,初始化需要value,分配内存后才可以赋值和使用

Tip1:key值不可以重复

Tip2:value值可重复

Tip3:make-value是无序的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//方式一:声明 + 分配
var a map[string]string// 声明
a = make(map[string]string,10)// 分配数据空间(其中第2个参数表示该map可以存放10个key-value)
//方式二:声明分配
a := make(map[string]string)
a["num1"] = "张三"//初始化
a["num2"] = "李四"
fmt.Println(a)// map[num1:张三 num2:李四]
//方式三:声明分配初始化
var cities map[string]string = map[string]string{
"num1":"北京",
"num2":"深圳",//注意最后一项也要添加","
}
fmt.Println(cities)

练习题:建立一个学生信息Map,存放3个学生信息,每个学生信息有name和sex信息,即嵌套map

1
2
3
4
5
6
7
8
9
10
11
12
studentMap := make(map[string]map[string]string)//key类型为string,value类型为map
studentMap["stu01"] = make(map[string]string,2)
studentMap["stu01"]["name"] = "Jack"
studentMap["stu01"]["sex"] = "Male"

studentMap["stu02"] = make(map[string]string,2)
studentMap["stu02"]["name"] = "Mary"
studentMap["stu02"]["sex"] = "Female"

studentMap["stu03"] = make(map[string]string,2)
studentMap["stu03"]["name"] = "Lawrence"
studentMap["stu03"]["sex"] = "Male"

map删除操作:

1
delete(map,"key")//key不存在不会报错

删除所有的key:

1
cities = make(map[string]string)//重新分配一个新空间即全部删除key

map查找(判断map中是否存在指定的key):

1
2
3
4
5
6
7
8
//方式一
val,findRes = cities["num1"]// 如果cities存在"num1",则findRes返回true,否则返回false
//方式二
if cities["num1"] != nil{
//存在该用户
}else{
//不存在该用户
}

map遍历(for-range):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var cities map[string]string = map[string]string{
"num1":"北京",
"num2":"深圳",
}
for k,v := range cities{
fmt.Println(k,v)
}

// 嵌套map遍历(复杂的双重for-range)
studentMap := make(map[string]map[string]string)//key类型为string,value类型为map
studentMap["stu01"] = make(map[string]string,2)
studentMap["stu01"]["name"] = "Jack"
studentMap["stu01"]["sex"] = "Male"
for k1,v1 := range studentMap{
fmt.Println(k1)
for k2,v2 := range v1{
fmt.Println(k2,":",v2,"\t")
}
}

获取map长度:

1
fmt.Println(len(studentMap))

map切片:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var monsters []map[string]string // 声明一个切片
monsters = make([]map[string]string,2)//初始化切片
if monsters[0] == nil{
monsters[0] = make(map[string]string,2)
monsters[0]["name"] = "PersonA"
monsters[0]["age"] = "18"
}
// 定义一个monster信息
newMonster := map[string]string{
"name" : "新英雄",
"age" : "22",
}
monsters = append(monsters,newMonster)
fmt.Println(monsters)

map排序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
map1 := make(map[int]int,3)
map1[10] = 100
map1[1] = 11
map1[2] = 999
fmt.Println(map1)
var keys []int
for k,_ := range map1{
keys = append(keys,k)
}
// 排序
sort.Ints(keys)
fmt.Println(keys)
// 根据keys遍历切片
for _,k := range keys{
fmt.Print(k,":",map1[k],"\t")
}

map也是引用类型,遵守引用类型的传递机制,在函数接受map后会直接修改原map

复习:切片也是引用类型,切片和map的使用均需要先make分配空间,其中切片的make需要指定长度,map不需要

1
2
3
4
5
6
7
8
9
10
11
func modify(mymap map[int]int){
mymap[10] = 100
}
func main(){
mymap := make(map[int]int)
mymap[10] = 1
mymap[20] = 2
fmt.Println(mymap)
modify(mymap)
fmt.Println(mymap)
}

map容量达到后会自动扩容,而不会发生panic,即动态增长键值对

map的value也经常用struct类型,更适合于管理复杂的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
type Stu struct{
Name string
Age int
Address string
}
func main(){
students := make(map[string]Stu,10)
Stu1 := Stu{Name:"Tom",Age:18,Address:"Beijing"}
Stu2 := Stu{Name:"Mary",Age:18,Address:"Hongkong"}
Stu3 := Stu{Name:"Jack",Age:18,Address:"Shenzhen"}
students["No1"] =Stu1
students["No2"] =Stu2
students["No3"] =Stu3
fmt.Println(students)
for k,v := range students{
fmt.Println("学生编号:",k)
fmt.Println("学生姓名:",v.Name)
fmt.Println("学生年龄:",v.Age)
fmt.Println("学生地址:",v.Address)
}
}

39.结构体

面向对象编程(OOP, Object Oriented Programming)特性——结构体

(1)Golang和传统的面向对象编程有区别,并不是纯粹的面向对象语言,即通常说Golang支持面向对象编程特性

(2)在Golang中没有类,而结构体与其他编程语言的类有同等地位,基于struct实现OOP,无OOP的继承,方法重载,构造函数,析构函数,隐藏的this指针等

(3)Golang仍然有面向对象编程的继承,封装和多态的特性,只是实现的方式与其他OOP语言不一样,例如继承:Golang没有extend关键字,而是通过匿名字段实现

结构体:声明后,字段/属性拥有默认初始值,注意当字段为:指针/切片/map均为nil,即还没有分配空间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type Stu struct{
Name string
Age int
Address string
ptr *int//指针
slice []int//切片
mymap map[string]string//map
}
p1 := Stu{"Yoyo",15,"shenzhen"}
// 使用slice和map均提前make
p1.slice = make([]int,10)
p1.slice[0] = 100

p1.mymap = make(map[string]string)
p1.mymap["keya"] = "valuea"
fmt.Println(p1)

不同结构体变量的字段是独立的,互不影响,即一个结构体变量字段更改不影响另外一个

1
2
3
4
5
6
7
8
9
10
//声明1
var p2 Stu
//声明2
//var p2 Stu = Stu{}
p2.Name = "KK"
p2.Age = 23
p3 := p2 // 默认值拷贝
p3.Age = 24
fmt.Println(p2)//{KK 23 <nil> [] map[]}
fmt.Println(p3)//{KK 24 <nil> [] map[]}

创建结构体变量:以下两种方式返回结构体指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type Person struct{
Name string
Age int
}
// 声明3
var p3 *Person = new (Person)
(*p3).Name = "Jack~"//注意:"."的运算符优先级高于"*",因此必须加上()
(*p3).Age = 28
// 以上可等价于(设计目的:方便使用,在go编译器底层对其转换,默认添加*)
p3.Name = "Pearson"
fmt.Println(p3)
// 声明4
var p4 *Person = &Person{}
(*p4).Name = "Alex"
//等价于
p4.Name = "Fisher"
fmt.Println(p4)

结构体的所有字段在内存中是连续分布的(可熟练通过地址获取内容),但是其指向的地址不一定连续(可能连续)

1
2
// 打印结构体字段的地址
fmt.Printf("Name的地址:%p,Age的地址:%p",&p3.Name,&p3.Age)

结构体在与其他类型转换时需要有相同的字段(名字、个数、类型)

1
2
3
4
5
6
7
8
9
10
11
12
type A struct{
Num int
}
type B struct{
Num int
}
func main(){
var a A
var b B
a = A(b)
fmt.Println(a)//强制转换可通过(A和B结构体的字段名字、个数、类型一致)
}

结构体进行type重新定义(重命名),Golang认为该新名为新的数据类型,不可以直接赋值,但是可以互相强制转换

1
2
3
4
5
6
7
8
9
10
11
12
type Student struct{
Name string
Age int
}
type Stu Student//重命名
func main(){
var stu1 Student
var stu2 Stu
//stu2 = stu1//ERROR,不可以直接赋值,数据类型不匹配
stu2 = Stu(stu1)//需要使用强制转换
fmt.Println(stu2)
}

struct的每个字段上,可以写上一个tag,该tag可以通过反射机制(后续介绍)获取,常见的使用场景是序列化和反序列化

序列化:变量转成字符串(Json)

问题:在结构体中字段名通常首字母为大写,不过在返回给其他程序使用时,例如jquery.php不习惯首字母大写的方式,由于其他包访问结构体字段时如果遇到小写则无法共享,因此必须使用大写

解决:使用tag标签解决(import “encoding/json”)

tag格式:json:"别名"

json.Marshal函数使用反射(后续介绍)

1
2
3
4
5
6
7
8
9
10
11
type Person struct{
Name string `json:"name"`//使用反引号,`json:"别名"`
Age int `json:"age"`
}
func main(){
p1 := Person{"TT",25}//创建一个Person变量
jsonPerson,error := json.Marshal(p1)//返回2个值([]byte , error),注意如果结构体字段首字母为小写,该返回为空(即不显示首字母小写的字段信息),由于json在另一个包调用该包
if error == nil{
fmt.Println(string(jsonPerson))//使用反引号后首字母小写
}
}

总结创建结构体变量:

1
2
3
4
5
6
//方式1(可指定字段值名,也可忽略)
var stu1 Student = Student("Tom",18)
//方式2
stu2 := Student{"JoJo",19}
//方式3
var stu3 *Student = &Student{"Smith",30}

40.方法

Golang的方法是作用在指定的数据类型,自定义类型都可以有方法,不仅仅struct

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
type A struct{
Num int
}
func (a A)test(n int)int{//表示A结构体有test()方法
fmt.Println(a.Num)
fmt.Println("received n: ",n)
return n+10
}
func main(){
var a A
res := a.test(11)
fmt.Println(res)
}

type integer int
func (i interger) print(){//值传递
fmt.Println("i=",i)
}
func main(){
var i integer = 10
i.print()
}

type integer int
func (i *integer) print(){//引用传递
*i = *i + 1
fmt.Printf("i=%v\n",*i)
}
func main(){
var i integer = 10
(&i).print()
fmt.Printf("i=%v",i)
}

如果一个类型实现了String()方法,则fmt.Println默认调用String()进行输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type Student struct {
Name string
Age int
}
func (stu *Student)String()string{
//该出输入参数为指针类型,是为了main()中fmt.Println(stu)正常输出stu,仅(&stu)才使用改写的String()
str := fmt.Sprintf("Name=[%v],Age=[%v]",stu.Name,stu.Age)//底层优化可省略"*"
return str
}
func main(){
stu := Student{"Chen",25}
fmt.Println(stu)//{Chen 25}
// 实现了*Student类型的String方法,则在执行fmt.Println()自动调用
fmt.Println(&stu)//Name=[Chen],Age=[25]
}

方法和函数的区别 - 1:

函数调用方式:函数名(实参列表)

方法调用方式:变量.方法名(实参列表)

方法和函数的区别 - 2:

普通函数:接受者为值类型时,不能将指针类型的数据直接传递,即形参类型必须与定义的的类型一致

方法:接收者为值类型时,可以直接使用指针类型,但仍然为方法的传递机制(基于方法,默认底层优化的机制)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
type Person struct{
Name string
Age int
}

func(p Person) print() {//值传递(不管是结构体变量还是其地址)
p.Name = "Jack"
//fmt.Println("print() Name = ",p.Name)
}
func (p *Person) print2(){//引用传递(不管是结构体变量还是其地址)
p.Name = "Jack"
//fmt.Println("print2() Name = ",p.Name)
}

func main(){
p := Person{"ABC",22}
p.print()//结构体变量
(&p).print()//结构体变量地址(本质仍然是值传递)
fmt.Println("print()->main() Name=",p.Name)//print()->main() Name= ABC(没有变化)
p.print2()//结构体变量
fmt.Println("print2()->main() Name=",p.Name)//print2()->main() Name= Jack(变化了)
}

41.工厂模式

Golang的结构体没有构造函数,通常使用工厂模式来解决问题

需要:结构体名首字母小写且可以被其他包所调用

不使用工厂模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 文件1 awesomeProject/utils/utils.go
package utils

type Student struct {//必须大写才可以被其他包调用
Name string
Age int
}

// 文件2 awesomeProject/main/helloworld.go
package main

import (
"awesomeProject/utils"
"fmt"
)
func main(){
stu := utils.Student{"Chen",25}
fmt.Println(stu)
}

使用工厂模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 文件1 awesomeProject/utils/utils.go
package utils

type student struct {
Name string
Age int
}

// 由于student结构体首字母小写,因此只能在utils.go中使用,可以通过工厂模式解决

func NewStudent(n string, a int) *student{
return &student{n,a}
}
// 文件2 awesomeProject/main/helloworld.go
package main

import (
"awesomeProject/utils"
"fmt"
)
func main(){
stu := utils.NewStudent("Chen",25)
fmt.Println(stu)
}

遇到结构体字段的首字母小写时,可使用Get..()方法解决

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
// 文件1 awesomeProject/utils/utils.go
package utils

type student struct {
Name string
age int
}

// 由于student结构体首字母小写,因此只能在utils.go中使用,可以通过工厂模式解决

func NewStudent(n string, a int) *student{
return &student{n,a}
}

func (s *student) GetAge() int {
return s.age
}

// 文件2 awesomeProject/main/helloworld.go
package main

import (
"awesomeProject/utils"
"fmt"
)
func main(){
stu := utils.NewStudent("Chen",25)
fmt.Println(stu.GetAge())
}

42.继承

使用嵌套匿名结构体实现继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type Goods struct{
Name string
Price int
}

type Book struct{
Goods//嵌套匿名结构体Goods
Writer string
}

func main(){
book1 := utils.Book{utils.Goods{Name:"书籍1",Price:18},"Tomcat"}
fmt.Println(book1)
}

结构体可用嵌套匿名结构体所有字段和方法,即首字母大小写的字段、方法均可使用,调用方法时会先寻找本身结构体的方法,若找不到则向上寻找方法

1
2
3
4
5
6
7
8
9
10
func (good Goods) Print(){
fmt.Println(good)
}

func (book Book) Print(){
fmt.Println("abc")
}

book1.Goods.Name = "bbb" //修改Goods的字段值
book1.Print() //调用Book/Goods的方法,当找不到Book类型的方法则自动向上寻找

如果struct嵌套了一个有名结构体,该模式为组合,如果是组合关系,那么在访问组合的结构体字段/方法时,必须带上结构体名字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
type A struct{
Name string
Age int
}

type B struct{
nickname A //有名结构体,格式:字段名 字段类型
}
type C struct{
*A // 嵌套结构体指针其效率更高,注意声明结构体变量的操作【遇到指针就给地址,也返回地址】
}

func main(){
var b B
b.nickname.Name = "aaa"
b.nickname.Age = 11
fmt.Println(b)

c := C{&A{"aaa",11}} //需要添加&
fmt.Println(*c.A)//返回地址,使用*取值
}

结构体还可以定义基本数据类型直接为字段

1
2
3
4
5
6
7
8
9
type Person struct{
Name string
int//匿名字段:基本数据类型
n int//有相同类型则需重命名
}
func main(){
p := Person{"aa",11}
fmt.Println(p)//使用方法相同
}

43.接口Interface

接口(interface)类型可以定义一组方法,不需要实现,interface不能包含任何变量,在某个自定义类型需要使用时才根据具体情况实现所有方法

例子1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 定义2个方法的接口
type geometry interface { //声明2个没有实现的方法
area() float64
perim() float64
}
// 定义结构体
type rect struct {
width, height float64
}
// 让rect结构体实现geometry接口的定义方法area()
func (r rect) area() float64 {
return r.width * r.height
}
// 让rect结构体实现geometry接口的定义方法perim()
func (r rect) perim() float64 {
return 2*r.width + 2*r.height
}

例子2:(接口——“松耦合高类聚”的思想)

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
type Usb interface{
Start()
Stop()
}
// 定义Phone结构体
type Phone struct{

}
// 让Phone结构体实现Usb的接口方法(不需要显式定义,即未知是Usb接口所声明的方法,与java中的implement关键字的继承截然不同)
func (phone Phone)Start(){
fmt.Println("phone is started")
}
func (phone Phone)Stop(){
fmt.Println("phone is stoped")
}
// 定义Camera结构体
type Camera struct{

}
// 让Camera结构体实现Usb的接口方法
func (camera Camera)Start(){
fmt.Println("camera is started")
}
func (camera Camera)Stop(){
fmt.Println("camera is stoped")
}
// 定义Computer结构体
type Computer struct{

}
//接收一个Usb接口类型变量,实现了Usb接口,即实现Usb接口所声明的所有方法
func (c Computer) Working(usb Usb){
usb.Start()//通过usb接口变量调用start()方法
usb.Stop()//通过usb接口变量调用stop()方法
}
func main(){
computer := Computer{}
phone := Phone{}
camera := Camera{}

computer.Working(phone)//传入的Phone类型变量,即调用Phone.start()和Phone.stop()
computer.Working(camera)//调用Camera.start()和Camera.stop()
// 以上传入Phone和Camera类似于多态,呈现不同的状态
}

接口相关知识:

(1)接口本身不能创建实例,但是可以指向一个实现了该接口的自定义类型的变量(实例)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type A interface{
Say()
}

//定义一个结构体并实现A接口的方法
type Stu struct{
Name string
}
func (student Stu)Say(){
fmt.Println("student is saying")
}

func main(){
var student Stu
var a A = student//若不实现Say()方法,即Student必须实现A接口声明的方法,则尽可以声明a,但无法实例化a,即无法将一个其他类型赋给接口类型变量a
a.Say()//
}

(2)接口中的所有方法都没有方法体,即没有实现的方法

(3)需要将接口作为形参传入时,即实现该接口,即需要实现该接口所声明的所有方法

(4)一个自定义类型只有实现了某接口,才能将该自定义类型的实例赋给接口类型

1
例子如(1)

(5)所有自定义数据类型均可以实现接口

(6)一个自定义类型可以实现多个接口

(7)接口中不允许有变量

1
2
3
type A interface{
Name string//×不允许定义
}

(8)一个接口可以继承多个接口,需实现继承的全部接口所声明的方法

(9)接口类型默认是指针(引用类型),没有对其初始化时会输出

1
2
3
4
5
6
7
type A interface{
start()
}
func main(){
var a A
fmt.Println(a)
}

(10)空接口没有任何方法,即所有类型均实现了空接口

1
2
3
4
5
6
7
8
type T interface{
}

func main(){
var student Stu
var t interface{} = student //空接口
var t T = student //空接口
}

(11)接口继承多接口时,不允许有重复方法名

1
2
3
4
5
6
7
8
9
10
11
12
type A interface{
Test1()
}
type B interface{
Test1()
}
type C interface{//继承多接口出现了重复方法名
A
B
}
func main(){
}

(12)变量指针类型和变量类型是有区别的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type A interface{
Say()
}
type Stu struct{
}
func (stu *Stu) Say(){
fmt.Println("Stu Say()")
}
func main(){
var stu Stu = Stu{}
var a A = stu//编译不通过,由于实现的是指针类型
var a A = &stu//编译通过
a.Say()
}

(13)实现结构体切片的排序:sort.Sort(data Interface)

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
import (
"fmt"
"math/rand"
"sort"
)

//1.声明Person结构体
type Person struct{
Name string
Age int
}
//2.声明一个Person结构体切片类型
type PersonSlice []Person

//3.实现接口方法(实现Sort需要实现Len()int,Less(i,j int)bool,Swap(i,j int)方法)
func (p PersonSlice) Len() int{
return len(p)
}
func (p PersonSlice) Less(i,j int) bool{//决定使用什么标准排序(按照age从小到大排序)
return p[i].Age < p[j].Age
}
func (p PersonSlice) Swap(i,j int) {
temp := p[i]
p[i] = p[j]
p[j] = temp
}

//4.测试 结构体切片排序
func main(){
// 定义一个数组/切片
var intSlice = []int{0,-1,10,7,90}
sort.Ints(intSlice)
fmt.Println(intSlice)
// 定义结构体切片
var ps PersonSlice
for i:=0;i<10;i++{
p := Person{
Name:fmt.Sprintf("人物%d",rand.Intn(100)),
Age:rand.Intn(100),
}
ps = append(ps,p)
}
for _,v := range ps{
fmt.Println(v)
}
//5.调用sort.Sort
sort.Sort(ps)
fmt.Println("=======Sorted=======")
//6.检查结果
for _,v := range ps{
fmt.Println(v)
}
}

继承:解决代码的复用性和可维护性

接口:设计和规范方法,让自定义类型实现方法

多态参数:接口既可以允许A结构体实现方法,也可以允许B结构体实现方法,即接口会依据具体实现方法的类型变量而定

多态数组:接口类型切片变量既可以存放A结构体也可以存放B结构体

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
type Usb interface {
start()
stop()
}
type Phone struct {
Name string
}
type Camera struct {
Name string
}
func (p Phone)start(){//多态参数 1
fmt.Println("phone start")
}
func (p Phone)stop(){
fmt.Println("phone stop")
}
func (p Camera)start(){//多态参数 2
fmt.Println("camera start")
}
func (p Camera)stop(){
fmt.Println("camera stop")
}
func main(){
var usbs [3]Usb//多态数组
usbs[0] = Phone{"vivo"}
usbs[1] = Phone{"oppo"}
usbs[2] = Camera{"索尼"}
fmt.Println(usbs)
}

44.类型断言assert

类型断言:由于接口是一般类型,不知道具体类型,如果要转成具体类型,需要使用类型断言

例子 1 :

1
2
3
4
var t float32
var x interface{}
x = t
y := x

例子 2 :

1
2
3
4
5
6
7
8
9
10
11
12
type Point struct{
x,y int
}
func main(){
var a interface{}
var point Point = Point{1,2}
a = point // 空接口可接收任何一个变量
var b Point
//b = a//ERROR,不允许将空接口(类型不明确)赋给Point类型
b = a.(Point) //类型断言,即尝试将a变量转换为Point类型变量,成功即通过,不成功即报错
fmt.Println(b) //{1 2}
}

例子 3:

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
package main

import "fmt"

type Usb interface {
start()
stop()
}
type Phone struct {
Name string
}
type Camera struct {
Name string
}
type Computer struct{//不同之处

}
func (c Computer)Working(usb Usb){//不同之处
usb.start()
//如果为Phone类型(类型断言)则调用Call()方法
if phone,ok := usb.(Phone);ok{
phone.Call()
}
usb.stop()
fmt.Println("phone start")
}
func (p Phone)start(){//多态参数 1
fmt.Println("phone start")
}
func (p Phone)Call(){//多态参数 1
fmt.Println("phone is calling")
}
func (p Phone)stop(){
fmt.Println("phone stop")
}
func (p Camera)start(){//多态参数 2
fmt.Println("camera start")
}
func (p Camera)stop(){
fmt.Println("camera stop")
}

func main(){
var usbs [3]Usb//多态数组
usbs[0] = Phone{"vivo"}
usbs[1] = Phone{"oppo"}
usbs[2] = Camera{"索尼"}
//fmt.Println(usbs)
var computer Computer
for _,v := range(usbs){
computer.Working(v)//基于v的类型调用usb.start()和usb.stop()因此,前2组为手机,第3组为相机
}
}

例子 4:

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
func Typejudge(items ...interface{}){
for i,x := range items{
switch x.(type) {
case bool:
fmt.Printf("第%v个参数为布尔类型\n",i)
case int,int32,int64:
fmt.Printf("第%v个参数为整数类型\n",i)
case float32,float64:
fmt.Printf("第%v个参数为浮点数类型\n",i)
case string:
fmt.Printf("第%v个参数为字符串类型\n",i)
case *Student:
fmt.Printf("第%v个参数为*Student类型\n",i)
case Student:
fmt.Printf("第%v个参数为Student类型\n",i)
default:
fmt.Printf("第%v个参数为其他类型\n",i)
}
}
}
func main(){
a := 1
b := "ss"
c := 5.5
d := Student{"Jomin"}
Typejudge(a,b,c,d,&d)
}

TODO 尚硅谷韩顺平Go语言核心编程 P229 226家庭收支记账 / P391 387结束语