https://www.runoob.com/go/go-tutorial.html

GO的语言特性

  • 并发

    • Go语言在并发编程方面比绝大多数语言要简洁不少
    • Go语言的并发执行单元是一种称为 goroutine 的协程
  • 内存回收(GC)

    • 内存自动回收,再也不需要开发人员管理内存
    • 开发人员专注业务实现,降低了心智负担
    • 只需要new分配內存,不需要释放
  • 内存分配

    • 先分配一块大内存区域
    • 大内存被切分成各个大小等级的块,放入不同的空闲list中
    • 对象分配空间时从空闲list中取出大小合适的内存块
    • 内存回收时,会把不用的内存重放回空闲list
    • 空闲内存会按照一定策略合并,以减少碎片。
  • 编译

    目前 Galang具有两种编译器

    • 一种是建立在GC基础上的Gccgo
    • 另外—种是分别针对64位×64和32位86计算机的一套编译器(6g和8g)
  • 网络编程

    • socket用net.Dial(基于tcp/udp,封裝了传统的 connect、listen、accept等接口)
    • http用http Get/post()
    • rpc用client.Call(‘class_name.method_name’ , args, &reply)
  • 函数多返回值

    • 允许函数返回多个值,在某些场景下,可以有效的简化编程。
    • Go语言推荐的编程风格,是函数返回的最后一个参数为error类型(只要逻辑体中可能出现异常),这样,在语言级别支持多返回值,就很有必要了
  • 语言交互性

    • 语言交互性指的是本语言是否能和其他语言交互,比如可以调用其他语言编译的库。
    • Go可以和c程序交互

HelloWorld

GO自动找main包的main函数作为程序入口,然后进行编译

1
2
3
4
5
6
7
8
// 将文件声明为main包
package main

import "fmt"

func main(){
fmt.Println("helloGo")
}

代码解析

  • package

    • 在同一个包下面的文件属于同一个工程文件,不用import包,可以直接使用
    • 在同一个包下面的所有文件的package名,都是一样的
    • 在同一个包下面的文件package名都建议设为是该目录名,但也可以不是
  • import

  • import "fmt" : 告诉 Go 编译器这个程序需要使用 fmt 包的函数,fmt 包实现了格式化 IO(输入/输出)的函数可以是相对路径也可以是绝对路径,推荐使用绝对路径(起始于工程根目录)

  • 点操作 : 这个包导入之后在你调用这个包的函数时,你可以省略前缀的包名(类似于python的from import)

    也就是前面你调用的fmt.Println("hello world")可以省略的写成Println("hello world")

    1
    2
    3
    import(
    . "fmt"
    )
  • 别名操作 : 把包命名成另一个我们用起来容易记忆的名字

    别名操作的话调用包函数时前缀变成了我们的前缀,即f.Println("hello world")

    1
    2
    3
    import(
    f "fmt"
    )
  • _操作 : 引入该包,而不直接使用包里面的函数,而是调用了该包里面的init函数

    1
    2
    3
    4
    import (
    "database/sql"
    _ "github.com/ziutek/mymysql/godrv"
    )
  • main : 是程序运行的入口。

执行(go run)

  • python执行文件是python helloworld.py
  • go执行文件是go run helloworld.go

编译(go build , go install)

编译 :

1
2
3
4
5
6
7
8
# 在任意文件路径下,运行:
go build hello

# 进入项目(应用包)的路径,然后运行:
go build

# -o参数来指定编译后得到的可执行文件的名字
go build -o heiheihei.exe

在哪里执行build就会在哪里生成exe文件

install :

install= build + 移动exe文件 : 就是build之后 , 将exe文件移动到GOPATH目录下的bin目录

1
2
3
4
5
# 在任意文件路径下,运行:
go install hello

# 进入项目(应用包)的路径,然后运行:
go install

在编译生成go程序的时,go实际上会去两个地方找程序包 , 在程序包里,自动找main包的main函数作为程序入口,然后进行编译:

  • GOROOT下的src文件夹下,
  • GOPATH下的src文件夹下。

然后就能在GOPATH目录下的bin目录中找到hello.exe

交叉编译

想在windows下编译一个linux下可执行文件

只需要指定目标操作系统的平台和处理器架构即可:

1
2
3
SET CGO_ENABLED=0  // 禁用CGO
SET GOOS=linux // 目标平台是linux
SET GOARCH=amd64 // 目标处理器架构是amd64

然后再执行go build命令,得到的就是能够在Linux平台运行的可执行文件了。

变量

  • 静态强类型 : 变量需要声明类型 , 且变量的类型不经常改变
  • 局部变量在声明后必须使用 (全局变量可以不用 , 但是函数里面的变量声明后必须使用)
  • 推荐使用小驼峰变量(首字母不大写 , 后续单词大写)
  • 类型推导 : 省略变量类型声明 , 交给编译器去猜
  • 短变量声明 : 在函数内部,可以使用更简略的 := 方式声明并初始化变量。
  • 匿名变量 : _ 和python的_变量一样 , 匿名变量不占用命名空间,不会分配内存
  • 标识符(包括常量、变量、类型、函数名、结构字段等等)以一个大写字母开头,如:Group1,那么使用这种形式的标识符的对象就可以被外部包的代码所使用(客户端程序需要先导入这个包),这被称为导出(像面向对象语言中的 public);
  • 标识符如果以小写字母开头,则对包外是不可见的,但是他们在整个包的内部是可见并且可用的(像面向对象语言中的 protected )。
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 name string = "yui"

// 类型推导
var age = 21

// 批量声明
var (
school string = "gdgy"
isMarry bool
)

func foo() (a int, b string) {
return a, "Q1mi"
}

func main() {
// 短变量声明,只能在函数内使用
job := "student"
isMarry = false
fmt.Println(name, age, school, isMarry, job)
// 匿名变量
x, _ := foo()
fmt.Println("x=", x)
}

常量

  • const同时声明多个常量时,如果省略了值则表示和上面一行的表达式相同。 (注意是表达式相同 , 值不一定相同)
  • iota常量计数器,只能在常量的表达式中使用。**iota在const关键字出现时将被重置为0。const中每新增一行常量声明将使iota计数一次**(iota可理解为const语句块中的行索引)。
  • 定义数量级 : <<表示左移操作
  • 常量可以用len(), cap(), unsafe.Sizeof()函数计算表达式的值。不过函数必须是内置函数,否则编译不过
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
import "unsafe"

const pi = 3.1415
const (
forbiden = 403
notFound = 404
OK = 200
)

const (
a = "abc"
b = len(a)
c = unsafe.Sizeof(a)
)

// score2表示"同上(和socre1一样为98)"
const (
score1 = 98
score2
score3
)

const (
n1 = iota // 0
n2 // 1
n3 // 2
)

const (
n100 = 1 // 1
n200 = iota // 1
n300 = 288 // 288
n400 // 288
n500 = iota // 4
_
n600 // 6
)
const n700 = iota // 0

// d1, d2, d3, d4 : 1 2 101 201
const (
d1, d2 = iota + 1, iota + 2
d3, d4 = iota + 100, iota + 200
)

const (
_ = iota
KB = 1 << (10 * iota)
MB = 1 << (10 * iota)
GB = 1 << (10 * iota)
TB = 1 << (10 * iota)
PB = 1 << (10 * iota)
)

const (
i=1<<iota // 1
j=3<<iota // 6
k // 12
l // 24
)

func main() {
fmt.Println(pi,forbiden,notFound,OK)
fmt.Println(score1,score2,score3)
fmt.Println(n1,n2,n3)
}

数据类型

  • 整型、
  • 浮点型、
  • 布尔型、
  • 字符串
  • 数组、
  • 切片、
  • 结构体、
  • 函数、
  • map、
  • 通道(channel)

整型

  • 带符号整型 : int8、int16、int32、int64
  • 无符号整型 : uint8、uint16、uint32、uint64
  • 特殊整型 : uint、int

uint8就是我们熟知的byte型,int16对应C语言中的short型,int64对应C语言中的long型。

类型 描述
uint8 无符号 8位整型 (0 到 255)
uint16 无符号 16位整型 (0 到 65535)
uint32 无符号 32位整型 (0 到 4294967295)
uint64 无符号 64位整型 (0 到 18446744073709551615)
int8 有符号 8位整型 (-128 到 127)
int16 有符号 16位整型 (-32768 到 32767)
int32 有符号 32位整型 (-2147483648 到 2147483647)
int64 有符号 64位整型 (-9223372036854775808 到 9223372036854775807)
uint 32位操作系统上就是uint32,64位操作系统上就是uint64
int 32位操作系统上就是int32,64位操作系统上就是int64
uintptr 无符号整型,用于存放一个指针

Java一样 , 支持定义N进制

1
2
3
4
5
6
7
8
9
10
11
12
13
func main() {
// 十进制
decimal := 10
// 八进制
oct := 077
// 十六进制
ox := 0x1324567
// 二进制
ob := 0b1111
// 强制int8类型
interger8 := int8(123)
fmt.Println(decimal, oct, ox, ob, interger8)
}

浮点型

  • float32float64
  • 默认为float64
1
2
3
4
5
6
7
8
9
10
func main() {
num := 1.2354
num2 := float32(1.789)

fmt.Println(math.MaxFloat32)
fmt.Println(num,num2)

num = float64(num2)
fmt.Println(num,num2)
}

布尔型

  • 布尔类型变量的默认值为false
  • 不允许将整型强制转换为布尔型.
  • 布尔型无法参与数值运算,也无法与其他类型进行转换。

字符串

  • 字符串的值为双引号(")中的内容
  • 多行字符串为反引号

注意 : 反引号内的内容皆row格式

方法 介绍
len(str) 求长度
+或fmt.Sprintf 拼接字符串
strings.Split 分割
strings.contains 判断是否包含
strings.HasPrefix,strings.HasSuffix 前缀/后缀判断
strings.Index(),strings.LastIndex() 子串出现的位置
strings.Join(a[]string, sep string) join操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func main()  {
name := "heyingliang"
path := `C:\software\Anaconda3`
fmt.Println(name)
fmt.Println(path)

var1 := name + path
var2 := fmt.Sprintf("%s%s",name,path)
fmt.Println(var1==var2)

res := strings.Split(path,"\\")
fmt.Println(res) // [C: software Anaconda3]

fmt.Println(strings.Join(res,"+")) // C:+software+Anaconda3
fmt.Println(strings.Contains(path,"\\")) // true
fmt.Println(strings.HasPrefix(path,"C:"))// true
fmt.Println(strings.Index(path,"Ana")) // 12
}

byte和rune类型

  • 字符串中的每一个元素叫做“字符”,在遍历或者单个获取字符串元素时可以获得字符。
  • 字符串底层是一个byte数组
  • Go语言中为了处理非ASCI码类型的字符 , 定义了新的rune类型
  • byte 类型,代表了ASCII码的一个字符 。 实际是一个uint8
  • rune类型,代表一个 UTF-8字符实际是一个int32

实际上源码:

1
2
type byte = uint8
type rune = int32

byte 类型是 uint8 的别名,对于只占用 1 个字节的传统 ASCII 编码的字符来说,完全没有问题,例如 var ch byte = 'A',字符使用单引号括起来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func traversalString() {
s := "hello沙河"
for i := 0; i < len(s); i++ { //byte
fmt.Printf("%v(%c) ", s[i], s[i])
}
fmt.Println()
for _, r := range s { //rune
fmt.Printf("%v(%c) ", r, r)
}
fmt.Println()
}

func main() {
traversalString()
}
//104(h) 101(e) 108(l) 108(l) 111(o) 230(æ) 178(²) 153( ) 230(æ) 178(²) 179(³)
// 104(h) 101(e) 108(l) 108(l) 111(o) 27801(沙) 27827(河)
  • 因为UTF8编码下一个中文汉字由3~4个字节组成,所以我们不能简单的按照字节去遍历一个包含中文的字符串,否则就会出现上面输出中第一行的结果。
  • 字符串底层是一个byte数组,所以可以和[]byte类型相互转换。字符串是不能修改的,字符串是由byte字节组成,所以字符串的长度是byte字节的长度。 rune类型用来表示utf8字符,一个rune字符由一个或多个byte组成。

修改字符串

要修改字符串,需要先将其转换成[]rune[]byte,完成后再转换为string。无论哪种转换,都会重新分配内存,并复制字节数组。

1
2
3
4
5
6
7
8
9
10
11
12
func changeString() {
s1 := "big"
// 强制类型转换
byteS1 := []byte(s1)
byteS1[0] = 'p'
fmt.Println(string(byteS1)) // pig

s2 := "白萝卜"
runeS2 := []rune(s2)
runeS2[0] = '红'
fmt.Println(string(runeS2)) // 红萝卜
}

计算汉字长度

1
2
3
4
5
func main() {
s := "何应良"
chars := []rune(s)
fmt.Println(len(chars))
}

字符串的len函数

字符串的len函数 : 返回字节数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import (
"fmt"
"unicode/utf8"
)

func main() {
s1 := "jija"
s2 := "忍者"
fmt.Println(len(s1)) // 4
// 每个中文占用3个字节
fmt.Println(len(s2)) // 6
// 真正的字符数可以使用如下函数
fmt.Println(utf8.RuneCountInString(s2))
}

总结

  • 字符 : 使用byte存储 , 本质是一个byte整型
  • 字符串 : 使用[]int32存储 , 本质是一个byte整型 的 数组

值类型和引用类型

  • int、float、bool 和 string 这些基本类型都属于值类型,使用这些类型的变量直接指向存在内存中的值 , 当使用等号 = 将一个变量的值赋值给另一个变量时,如:j = i,实际上是在内存中将 i 的值进行了拷贝
  • 引用类型的变量 r1 存储的是 r1 的值所在的内存地址(数字),或内存地址中第一个字所在的位置。

流程控制

if

1
2
3
4
5
6
7
8
9
10
func main() {
age := 18
if age > 18 {
fmt.Println("old")
} else if age == 18 {
fmt.Println("18")
} else {
fmt.Println("young")
}
}

go判断前添加执行语句

1
2
3
4
5
6
7
8
9
10
11
func main() {
myAge := 17
// 在执行if语句前,还可以添加一个执行语句
if myAge++; myAge > 18 {
fmt.Println("old")
} else if myAge == 18 {
fmt.Println("18") // 执行此分支
} else {
fmt.Println("young")
}
}

注意 , 如果是定义变量的话 , 这个变量只存在于if语句中

1
2
3
4
5
6
7
8
9
10
11
func main() {
// myAge的作用域只有if语句中
if myAge := 17; myAge > 18 {
fmt.Println("old")
} else if myAge == 18 {
fmt.Println("18")
} else {
fmt.Println("young") // 执行此分支
}
//fmt.Println(myAge) myAge not define
}

switch

  • switch 语句执行的过程从上至下,直到找到匹配项,匹配项后面也不需要再加 break。
  • switch 默认情况下 case 最后自带 break 语句,匹配成功后就不会执行其他 case,如果我们需要执行后面的 case,可以使用 fallthrough
  • 使用 fallthrough 会强制执行后面的 case 语句,fallthrough 不会判断下一条 case 的表达式结果是否为 true。
1
2
3
4
5
6
7
8
9
10
11
12
var qq = 2

func main() {
switch qq {
case 1:
fmt.Println("first")
case 2:
fmt.Println("secend")
case 3:
fmt.Println("third")
}
}

for

1
2
3
4
5
func main() {
for s := 0; s < 10; s++ {
fmt.Println(s)
}
}

省略初始语句

1
2
3
4
5
6
7
var nu int = 0
func main() {
for ; nu < 10; nu++ {
fmt.Println(nu)
}
fmt.Println(nu) // 10
}

省略初始和结束语句

类似于python的while语句

1
2
3
4
5
6
7
func main() {
nu := 0
for nu < 10 {
fmt.Println(nu)
nu++
}
}

无限循环

1
2
3
4
5
6
7
8
9
10
func main() {
nu := 0
for {
nu++
fmt.Println(nu)
if nu == 10 {
break
}
}
}

for range(键值循环)

使用for range遍历数组、切片、字符串、map 及通道(channel)。

通过for range遍历的返回值有以下规律:

  1. 数组、切片、字符串返回索引
  2. map返回键和值。
  3. 通道(channel)只返回通道内的值。
1
2
3
4
5
6
7
8
9
10
11
func main() {
myStr := "aA何应良"
for idx, val := range myStr {
fmt.Printf("%v,%v--%c \n",idx, val, val)
}
}
//0,97--a
//1,65--A
//2,20309--何
//5,24212--应
//8,33391--良

goto

  • goto 语句可以无条件地转移到过程中指定的行。
  • goto 语句通常与条件语句配合使用。可用来实现条件转移, 构成循环,跳出循环体等功能。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 在变量a等于15的时候跳过本次循环并回到循环的开始语句LOOP处:
func main() {
var a int = 10
LOOP:
for a < 20 {
if a == 15 {
/* 跳过迭代 */
a = a + 1
goto LOOP
}
fmt.Printf("a的值为 : %d\n", a)
a++
}
}

函数

1
2
3
func function_name( [parameter list] ) [return_types] {
函数体
}
  • func:函数由 func 开始声明
  • function_name:函数名称,函数名和参数列表一起构成了函数签名。
  • parameter list:参数列表。
  • return_types:返回类型,函数返回一列值。return_types 是该列值的数据类型。有些功能不需要返回值,这种情况下 return_types 不是必须的。
  • 函数体:函数定义的代码集合。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func maxNum(num1 int, num2 int) int {
var res int
if num1 > num2 {
res = num1
} else {
res = num2
}
return res
}

func swap(x, y string) (string, string) {
return y, x
}
func main() {
a := "1"
b := "heyingliang"
fmt.Println(swap(a, b))
}

闭包

  • getSequence() ,返回另外一个函数。该函数的目的是在闭包中递增 i 变量,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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())

/* 创建新的函数 nextNumber1,并查看结果 */
nextNumber1 := getSequence()
fmt.Println(nextNumber1())
fmt.Println(nextNumber1())
}

方法

  • Go 语言中有函数和方法。
  • 一个方法就是一个包含了接受者的函数,接受者可以是命名类型或者结构体类型的一个值或者是一个指针。
  • 所有给定类型的方法属于该类型的方法集。
1
2
3
func (variable_name variable_data_type) function_name() [return_type]{
/* 函数体*/
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* 定义结构体 */
type Circle struct {
radius float64
}

//该 method 属于 Circle 类型对象中的方法
func (c Circle) getArea() float64 {
//c.radius 即为 Circle 类型对象中的属性
return 3.14 * c.radius * c.radius
}

func main() {
var c1 Circle
c1.radius = 10.00
fmt.Println("圆的面积 = ", c1.getArea())
}

变量作用域

Go 语言中变量可以在三个地方声明:

  • 函数内定义的变量称为局部变量
  • 函数外定义的变量称为全局变量
  • 函数定义中的变量称为形式参数
  • 局部变量 :

    在函数体内声明的变量称之为局部变量,它们的作用域只在函数体内,参数和返回值变量也是局部变量。

  • 全局变量 :

    在函数体外声明的变量称之为全局变量,全局变量可以在整个包甚至外部包(被导出后)使用

    全局变量与局部变量名称可以相同,但是函数内的局部变量会被优先考虑。

  • 形式参数 :

    形式参数会作为函数的局部变量来使用。

数组

  • 相同类型
  • 长度固定
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
// 声明数组 : 需要指定`元素类型`及`元素个数`
var variable_name [SIZE] variable_type
// 数组 balance 长度为 10 类型为 float32:
var balance [10] float32

// 初始化数组
var balance = [5]float32{1000.0, 2.0, 3.4, 7.0, 50.0}

// 如果忽略 [] 中的数字不设置数组大小,Go 语言会根据元素的个数来设置数组的大小:
var balance = [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0}

// 多维数组
var variable_name [SIZE1][SIZE2]...[SIZEN] variable_type
// 三维的整型数组
var threedim [5][10][4]int

// 初始化二维数组
a = [3][4]int{
{0, 1, 2, 3} , /* 第一行索引为 0 */
{4, 5, 6, 7} , /* 第二行索引为 1 */
{8, 9, 10, 11}, /* 第三行索引为 2 */
}

// 向函数传递数组 (未限定长度)
func getAverage(arr []int, size int) float32{}
// 限定长度
func getAverage(arr [5]int, size int) float32{}

指针

  • 一个指针变量指向了一个值的内存地址。
1
2
3
4
5
6
// 声明指针
// var-type 为指针类型,var_name 为指针变量名,* 号用于指定变量是作为一个指针。
var var_name *var-type

var ip *int /* 指向整型*/
var fp *float32 /* 指向浮点型 */

如何使用指针

指针使用流程:

  • 定义指针变量。
  • 为指针变量赋值。
  • 访问指针变量中指向地址的值。
1
2
3
4
5
6
7
8
9
10
11
12
13
func main() {
var a int = 20 /* 声明实际变量 */
var ip *int /* 声明指针变量 */

ip = &a /* 指针变量的存储地址 */
fmt.Printf("a 变量的地址是: %x\n", &a) //a 变量的地址是: c0000a0068

/* 指针变量的存储地址 */
fmt.Printf("ip 变量储存的指针地址: %x\n", ip) // ip 变量储存的指针地址: c0000a0068

/* 使用指针访问值 */
fmt.Printf("*ip 变量的值: %d\n", *ip) // *ip 变量的值: 20
}

空指针

  • 当一个指针被定义后没有分配到任何变量时,它的值为 nil。
  • nil 指针也称为空指针。
  • nil在概念上和其它语言的null、None、nil、NULL一样,都指代零值或空值。
  • 一个指针变量通常缩写为 ptr。
1
2
3
4
func main() {
var ptr *int
fmt.Printf("ptr 的值为 : %x\n", ptr) // ptr 的值为 : 0
}
1
2
if(ptr != nil)     /* ptr 不是空指针 */
if(ptr == nil) /* ptr 是空指针 */

指针数组

1
2
// 声明指针数组
var ptr [MAX]*int;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const MAX int = 3

func main() {
a := []int{10, 100, 200}
var i int
var ptr [MAX]*int // 声明一个长度为3的指针数组

for i = 0; i < MAX; i++ {
ptr[i] = &a[i] /* 整数地址赋值给指针数组 */
}

for i = 0; i < MAX; i++ {
fmt.Printf("a[%d] = %d\n", i, *ptr[i])
}
}
//a[0] = 10
//a[1] = 100
//a[2] = 200

指向指针的指针

  • 如果一个指针变量存放的又是另一个指针变量的地址,则称这个指针变量为指向指针的指针变量。
  • 当定义一个指向指针的指针变量时,第一个指针存放第二个指针的地址,第二个指针存放变量的地址:

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 指向指针的指针变量声明格式
var ptr **int;

// 访问指向指针的指针变量值需要使用两个 * 号
func main() {
var a int
var ptr *int // 指针
var pptr **int // 指向指针的指针

a = 3000
ptr = &a /* 指针 ptr 地址 */
pptr = &ptr /* 指向指针 ptr 地址 */

/* 获取 pptr 的值 */
fmt.Printf("变量 a = %d\n", a)
fmt.Printf("指针变量 *ptr = %d\n", *ptr)
fmt.Printf("指向指针的指针变量 **pptr = %d\n", **pptr)
}
//变量 a = 3000
//指针变量 *ptr = 3000
//指向指针的指针变量 **pptr = 3000

指针作为函数参数

  • 向函数传递指针,只需要在函数定义的参数上设置为指针类型即可
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func main() {
/* 定义局部变量 */
var a int = 100
var b int= 200

fmt.Printf("交换前 a 的值 : %d\n", a )
fmt.Printf("交换前 b 的值 : %d\n", b )

/* 调用函数用于交换值
* &a 指向 a 变量的地址
* &b 指向 b 变量的地址
*/
swap(&a, &b);

fmt.Printf("交换后 a 的值 : %d\n", a )
fmt.Printf("交换后 b 的值 : %d\n", b )
}

func swap(x *int, y *int){
*x, *y = *y, *x
}

结构体

  • 数组可以存储同一类型的数据,但在结构体中我们可以为不同项定义不同的数据类型。
  • 结构体是由一系列具有相同类型或不同类型的数据构成的数据集合。
  • 结构体表示一项记录

类似于python中的具名元组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 结构体定义需要使用 type 和 struct 语句。
// struct 语句定义一个新的数据类型,结构体中有一个或多个成员。
// type 语句设定了结构体的名称。结构体的格式如下:
type struct_variable_type struct {
member definition
...
member definition
}

// 一旦定义了结构体类型,它就能用于变量的声明,语法格式如下:
variable_name := structure_variable_type {value1, value2...valuen}
// 或
variable_name := structure_variable_type { key1: value1, key2: value2..., keyn: valuen}

// 访问结构体成员
结构体.成员名
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
type Books struct {
title string
author string
subject string
book_id int
}

func main() {
// 创建一个新的结构体
fmt.Println(Books{"Go 语言", "www.runoob.com", "Go 语言教程", 6495407})
// 也可以使用 key => value 格式
fmt.Println(Books{title: "Go 语言", author: "www.runoob.com", subject: "Go 语言教程", book_id: 6495407})
// 忽略的字段为 0 或 空
fmt.Println(Books{title: "Go 语言", author: "www.runoob.com"})

var Book1 Books /* 声明 Book1 为 Books 类型 */
/* book 1 描述 */
Book1.title = "Go 语言"
Book1.book_id = 6495407
fmt.Printf("Book 1 title : %s\n", Book1.title)
}

结构体作为函数参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
type Books struct {
title string
author string
subject string
book_id int
}

func printBook(book Books) {
fmt.Printf("Book title : %s\n", book.title)
}

func main() {
var Book1 Books /* 声明 Book1 为 Books 类型 */
/* book 1 描述 */
Book1.title = "Go 语言"
Book1.author = "www.runoob.com"
Book1.subject = "Go 语言教程"
Book1.book_id = 6495407
/* 打印 Book1 信息 */
printBook(Book1)
}

结构体指针

  • 定义指向结构体的指针
1
2
3
4
5
6
// 定义指向结构体的指针
var struct_pointer *Books
// 查看结构体变量地址,可以将 & 符号放置于结构体变量前
struct_pointer = &Book1
// 使用结构体指针访问结构体成员,使用 "." 操作符
struct_pointer.title
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type Books struct {
title string
author string
subject string
book_id int
}

func printBook(book *Books) {
fmt.Printf("Book title : %s\n", book.title)
}

func main() {
var Book1 Books
Book1.title = "Go 语言"
printBook(&Book1)
}

切片(Slice)

  • 切片 就是 长度可变的数组。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//定义切片
// 定义切片不需要说明长度。模式如下 :
var identifier []type
// 或使用make()函数来创建切片:
var slice1 []type = make([]type, len)
// 也可以简写为
slice1 := make([]type, len)
// 也可以指定容量,其中capacity为可选参数
make([]T, length, capacity)

// 切片初始化
s :=[] int {1,2,3 }
// 直接初始化切片,[]表示是切片类型
s := arr[:]
s := arr[startIndex:]

初始化数组和切片

1
2
3
4
// 数组
var arr = [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
// 切片
var slice = []float32{1000.0, 2.0, 3.4, 7.0, 50.0}

空(nil)切片

  • 一个切片在未初始化之前默认为 nil,长度为 0
1
2
3
4
5
6
7
8
9
10
11
func printSlice(x []int){
fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}

func main() {
var numbers []int
printSlice(numbers) // len=0 cap=0 slice=[]
if(numbers == nil){
fmt.Printf("切片是空的")
}
}

len() 和 cap() 函数

  • 切片是可索引的,并且可以由 len() 方法获取长度。
  • 切片提供了计算容量的方法 cap() 可以测量切片最长可以达到多少
1
2
3
4
5
6
7
8
9
10
func main() {
var numbers = make([]int,3,5)

printSlice(numbers)
}

func printSlice(x []int){
fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}
// len=3 cap=5 slice=[0 0 0]

append() 和 copy() 函数

  • 如果想增加切片的容量,我们必须创建一个新的更大的切片并把原分片的内容都拷贝过来。
  • 下面的代码描述了从拷贝切片的 copy 方法和向切片追加新元素的 append 方法。
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
func main() {
var numbers []int
printSlice(numbers)

/* 允许追加空切片 */
numbers = append(numbers, 0)
printSlice(numbers)

/* 向切片添加一个元素 */
numbers = append(numbers, 1)
printSlice(numbers)

/* 同时添加多个元素 */
numbers = append(numbers, 2,3,4)
printSlice(numbers)

/* 创建切片 numbers1 是之前切片的两倍容量*/
numbers1 := make([]int, len(numbers), (cap(numbers))*2)

/* 拷贝 numbers 的内容到 numbers1 */
copy(numbers1,numbers)
printSlice(numbers1)
}

func printSlice(x []int){
fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}
//len=0 cap=0 slice=[]
//len=1 cap=1 slice=[0]
//len=2 cap=2 slice=[0 1]
//len=5 cap=6 slice=[0 1 2 3 4]
//len=5 cap=12 slice=[0 1 2 3 4]

范围(Range)

  • range 关键字用于 for 循环中迭代数组(array)、切片(slice)、通道(channel)或集合(map)的元素。
  • 在数组和切片中它返回元素的索引和索引对应的值,在集合中返回 key-value 对。
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
func main() {
//这是我们使用range去求一个slice的和。使用数组跟这个很类似
nums := []int{2, 3, 4}
sum := 0
for _, num := range nums {
sum += num
}
fmt.Println("sum:", sum)

for i, num := range nums {
if num == 3 {
fmt.Println("index:", i)
}
}

//range也可以用在map的键值对上。
kvs := map[string]string{"a": "apple", "b": "banana"}
for k, v := range kvs {
fmt.Printf("%s -> %s\n", k, v)
}

//range也可以用来枚举Unicode字符串。第一个参数是字符的索引,第二个是字符(Unicode的值)本身。
for i, c := range "go" {
fmt.Println(i, c)
}
}

Map(集合)

定义 Map , 有两种形式

  • 两步创建
  • 一步创建
1
2
3
4
5
6
7
8
9
10
11
12
// 一步创建
m2 := make(map[string]string)

// 两步创建
var map_variable map[key_data_type]value_data_type // 先声明map
map_variable := make(map[key_data_type]value_data_type) // 再使用make函数创建一个非nil的map,nil map不能赋值

// 初始化 + 赋值一体化
m3 := map[string]string{
"a": "aa",
"b": "bb",
}

delete() 函数

1
2
3
4
5
6
7
8
9
10
11
12
func main() {
/* 创建map */
countryCapitalMap := map[string]string{
"France": "Paris",
"Italy": "Rome",
"Japan": "Tokyo",
"India": "New delhi",
}
/*删除元素*/
delete(countryCapitalMap, "France")
fmt.Println("法国条目被删除")
}

递归函数

1
2
3
4
5
6
7
func recursion() {
recursion() /* 函数调用自身 */
}

func main() {
recursion()
}
1
2
3
4
5
6
7
8
9
10
11
12
func fibo(count int) int {
if count <= 2 {
return 1
} else {
return fibo(count-1) + fibo(count-2)
}
}

func main() {
count := 8
fmt.Println(fibo(count))
}

类型转换

1
type_name(expression)
1
2
3
4
5
6
7
8
func main() {
var sum int = 17
var count int = 5
var mean float32

mean = float32(sum) / float32(count)
fmt.Printf("mean 的值为: %f\n", mean)
}

接口

接口,它把所有的具有共性的方法定义在一起,任何其他类型只要实现了这些方法就是实现了这个接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* 定义接口 */
type interface_name interface {
method_name1 [return_type]
method_name2 [return_type]
method_name3 [return_type]
...
method_namen [return_type]
}

/* 定义结构体 */
type struct_name struct {
/* variables */
}

/* 实现接口方法 */
func (struct_name_variable struct_name) method_name1() [return_type] {
/* 方法实现 */
}
...
func (struct_name_variable struct_name) method_namen() [return_type] {
/* 方法实现*/
}
  • 我们定义了一个接口Phone,接口里面有一个方法call()。
  • 然后我们在main函数里面定义了一个Phone类型变量,并分别为之赋值为NokiaPhone和IPhone。然后调用call()方法,输出结果如下:
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
type Phone interface {
call()
}

type NokiaPhone struct {
}

type IPhone struct {
}

func (nokiaPhone NokiaPhone) call() {
fmt.Println("I am Nokia, I can call you!")
}

func (iPhone IPhone) call() {
fmt.Println("I am iPhone, I can call you!")
}

func main() {
var phone Phone

phone = new(NokiaPhone)
phone.call()

phone = new(IPhone)
phone.call()
}

错误处理

  • 通过内置的错误接口提供了非常简单的错误处理机制。
1
2
3
4
5
6
7
8
9
10
11
12
13
// error类型是一个接口类型,这是它的定义:
type error interface {
Error() string
}

// 我们可以在编码中通过实现 error 接口类型来生成错误信息。
// 函数通常在最后的返回值中返回错误信息。使用errors.New 可返回一个错误信息:
func Sqrt(f float64) (float64, error) {
if f < 0 {
return 0, errors.New("math: square root of negative number")
}
// 实现
}
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
// 定义一个 DivideError 结构
type DivideError struct {
dividee int
divider int
}

// 实现 `error` 接口
func (de *DivideError) Error() string {
strFormat := `
Cannot proceed, the divider is zero.
dividee: %d
divider: 0
`
return fmt.Sprintf(strFormat, de.dividee)
}

// 定义 `int` 类型除法运算的函数
func Divide(varDividee int, varDivider int) (result int, errorMsg string) {
if varDivider == 0 {
dData := DivideError{
dividee: varDividee,
divider: varDivider,
}
errorMsg = dData.Error()
return
} else {
return varDividee / varDivider, ""
}
}

func main() {
// 正常情况
if result, errorMsg := Divide(100, 10); errorMsg == "" {
fmt.Println("100/10 = ", result)
}
// 当除数为零的时候会返回错误信息
if _, errorMsg := Divide(100, 0); errorMsg != "" {
fmt.Println("errorMsg is: ", errorMsg)
}
}

并发

  • Go 语言支持并发,我们只需要通过 go 关键字来开启 goroutine 即可。
  • goroutine 是轻量级线程,goroutine 的调度是由 Golang 运行时进行管理的。
1
2
3
4
5
6
7
// goroutine 语法格式:
go 函数名( 参数列表 )
// eg:
go f(x, y, z)

//开启一个新的 goroutine:
f(x, y, z)

Go 允许使用 go 语句开启一个新的运行期线程, 即 goroutine,以一个不同的、新创建的 goroutine 来执行一个函数。 同一个程序中的所有 goroutine 共享同一个地址空间。

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
// 输出的 hello 和 world 是没有固定先后顺序。因为它们是两个 goroutine 在执行:
import (
"fmt"
"time"
)

func say(s string) {
for i := 0; i < 5; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Println(s)
}
}

func main() {
go say("world")
say("hello")
}
//world
//hello
//hello
//world
//world
//hello
//hello
//world
//world
//hello

通道(channel)

  • 通道(channel)是用来传递数据的一个数据结构。
  • 通道可用于两个 goroutine 之间通过传递一个指定类型的值来同步运行和通讯。
  • 操作符 <- 用于指定通道的方向,发送或接收。如果未指定方向,则为双向通道。
1
2
ch <- v    // 把 v 发送到通道 ch
v := <-ch // 从 ch 接收数据 , 并把值赋给 v
1
2
// 声明一个通道使用chan关键字即可,通道在使用前必须先创建:
ch := make(chan int)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func sum(s []int, c chan int) {
sum := 0
for _, v := range s {
sum += v
}
c <- sum // 把 sum 发送到通道 c
}

func main() {
s := []int{7, 2, 8, -9, 4, 0}
c := make(chan int)
go sum(s[:len(s)/2], c)
go sum(s[len(s)/2:], c)
x, y := <-c, <-c // 从通道 c 中接收
fmt.Println(x, y, x+y) // -5 17 12
}

通道缓冲区

通道可以设置缓冲区,通过 make 的第二个参数指定缓冲区大小:

1
ch := make(chan int, 100)
  • 带缓冲区的通道允许发送端的数据发送和接收端的数据获取处于异步状态,就是说发送端发送的数据可以放在缓冲区里面,可以等待接收端去获取数据,而不是立刻需要接收端去获取数据。
  • 不过由于缓冲区的大小是有限的,所以还是必须有接收端来接收数据的,否则缓冲区一满,数据发送端就无法再发送数据了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
func main() {
// 这里我们定义了一个可以存储整数类型的带缓冲通道
// 缓冲区大小为2
ch := make(chan int, 2)

// 因为 ch 是带缓冲的通道,我们可以同时发送两个数据
// 而不用立刻需要去同步读取数据
ch <- 1
ch <- 2

// 获取这两个数据
fmt.Println(<-ch) // 1
fmt.Println(<-ch) // 2
}

遍历通道与关闭通道

  • 通过 range 关键字来实现遍历读取到的数据,类似于与数组或切片。
  • 如果通道接收不到数据后 ok 就为 false,这时通道就可以使用 close() 函数来关闭。
1
v, ok := <-ch
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func fibonacci(n int, c chan int) {
x, y := 0, 1
for i := 0; i < n; i++ {
c <- x
x, y = y, x+y
}
close(c)
}

func main() {
c := make(chan int, 10)
go fibonacci(cap(c), c)
// range 函数遍历每个从通道接收到的数据,因为 c 在发送完 10 个
// 数据之后就关闭了通道,所以这里我们 range 函数在接收到 10 个数据
// 之后就结束了。如果上面的 c 通道不关闭,那么 range 函数就不
// 会结束,从而在接收第 11 个数据的时候就阻塞了。
for i := range c {
fmt.Println(i)
}
}