# Go学习资料

# 一、Go环境搭建

  1. 安装go环境

    • 官方下载环境包直接next走就行

    • 查看go的安装目录

      # 其中GOROOT即为安装环境,GOPATH即GO项目工作目录
      go env
      
      1
      2
  2. 下载Go开发工具

    我才用的是利用已有的Java Idea安装go插件,出现无法配置Go SDK的情况

    • 解决(注意标点格式):https://www.jianshu.com/p/4dd25a1744aa
  3. 选择SDK-新建项目-新建main包 image-20220731235322242

# 二、Go基础知识

# 25个关键字

break    default      func    interface    select
case     defer        go      map          struct
chan     else         goto    package      switch
const    fallthrough  if      range        type
continue for          import  return       var
1
2
3
4
5

type:参考 (opens new window)

  • 定义接口
  • 定义结构体
  • 定义新类型
  • 定义类型的别名
  • 定义函数的类型:函数式编程,感觉有点像接口
func main() {
	 // 将返回值直接作为函数调用
	 res1 := fun1()
	 fmt.Println(res1(10,20))
}

type my_fun func (int,int)(string)

//fun1()函数的返回值是my_func类型
func fun1 () my_fun{
	fun := func(a,b int) string {
		s := strconv.Itoa(a) + strconv.Itoa(b)
		return s
	}
	return fun
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 内置函数

  • len:获取长度
  • make:用于内建类型(map、slice 和 channel)的内存分配
  • new:用于各种类型的内存分配

# 变量声明

变量声明

  • 普通变量:var 关键字是 Go 最基本的定义变量方式

// 普通声明(包括类型及赋值)
var vname1, vname2, vname3 type= v1, v2, v3

// 省略类型的声明
var vname1, vname2, vname3 = v1, v2, v3

// 简短声明:省略var的声明(只能用在局部变量)
vname1, vname2, vname3 := v1, v2, v3

// 下划线是个特殊的变量,任何赋予的值都会被丢弃,下面是b赋值了35
_, b := 34, 35

// 字符串(可以通过“”和``声明)
s := "hello"

// 换行字符串
m := `hello
    world`

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
  • 常量
const constantName = value
// 如果需要,也可以明确指定常量的类型:
const Pi float32 = 3.1415926
1
2
3
  • 数组变量(array、slice)

一维数组

var arr [3]int = {1, 2, 3}
a := [3]int{1, 2, 3} // 声明了一个长度为3的int数组
b := [10]int{1, 2, 3} // 声明了一个长度为 10 的 int 数组,其中前三个元素初始化为 1、2、3,其它默认为 0
c := [...]int{4, 5, 6} // 可以省略长度而采用 `...` 的方式,Go 会自动根据元素个数来计算长度
1
2
3
4

动态数组(slice):无需定义长度

var fslice []int
slice := []byte {'a', 'b', 'c', 'd'}

// 声明一个数组
var array = [10]byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'}
// 声明两个 slice
var aSlice, bSlice []byte

// 演示一些简便操作
aSlice = array[:3] // 等价于 aSlice = array[0:3] aSlice 包含元素: a,b,c
aSlice = array[5:] // 等价于 aSlice = array[5:10] aSlice 包含元素: f,g,h,i,j
aSlice = array[:]  // 等价于 aSlice = array[0:10] 这样 aSlice 包含了全部的元素
1
2
3
4
5
6
7
8
9
10
11
12

二维数组

// 声明了一个二维数组,该数组以两个数组作为元素,其中每个数组中又有 4 个 int 类型的元素
doubleArray := [2][4]int{[4]int{1, 2, 3, 4}, [4]int{5, 6, 7, 8}}

// 上面的声明可以简化,直接忽略内部的类型
easyArray := [2][4]int{{1, 2, 3, 4}, {5, 6, 7, 8}}
1
2
3
4
5
  • map变量
// 声明一个 key 是字符串,值为 int 的字典, 这种方式的声明需要在使用之前使用 make 初始化
var numbers map[string]int

// 另一种 map 的声明方式
numbers := make(map[string]int)
numbers["one"] = 1  // 赋值
numbers["ten"] = 10 // 赋值
numbers["three"] = 3
fmt.Println("第三个数字是: ", numbers["three"]) // 读取数据

// 初始化map
rating := map[string]float32{"C":5, "Go":4.5, "Python":4.5, "C++":2 }
// 删除key为C的元素
delete(rating, "C") 

// 遍历map的key,
for k,v:=range rating {
    fmt.Println("map's key:",k)
    fmt.Println("map's val:",v)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

分组声明

// 多个声明合为一组

import(
    "fmt"
    "os"
)

const(
    i = 100
    pi = 3.1415
    prefix = "Go_"
)

var(
    i int
    pi float32
    prefix string
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 流程和函数

流程控制

If

// 计算获取值 x, 然后根据 x 返回的大小,判断是否大于 10。
if x := computedValue(); x > 10 {
    fmt.Println("x is greater than 10")
} else {
    fmt.Println("x is less than 10")
}
1
2
3
4
5
6

for

普通遍历

for index := 10; index>0; index-- {
    if index == 5{
        break // 或者continue
    }
    fmt.Println(index)
}
1
2
3
4
5
6

while方式遍历

sum := 1
for sum < 1000 {
    sum += sum
}
1
2
3
4

map遍历:for+range

for k,v:=range map {
    fmt.Println("map's key:",k)
    fmt.Println("map's val:",v)
}

// go中声明而未被调用的变量会报错,可以同_取代
for _, v := range map{
    fmt.Println("map's val:", v)
}
1
2
3
4
5
6
7
8
9

switch

// go中的case默认带break,若想强制下面的继续执行,可以使用fallthrough
i := 10
switch i {
case 1:
    fmt.Println("i is equal to 1")
  	fallthrough
case 2, 3, 4:
    fmt.Println("i is equal to 2, 3 or 4")
case 10:
    fmt.Println("i is equal to 10")
default:
    fmt.Println("All I know is that i is an integer")
}
1
2
3
4
5
6
7
8
9
10
11
12
13

函数

普通函数

func funcName(input1 type1, input2 type2) (output1 type1, output2 type2) {
    // 这里是处理逻辑代码
    // 返回多个值
    return value1, value2
}

// 返回 A+B 和 A*B
func SumAndProduct(A, B int) (int, int) {
    return A+B, A*B
}

// 官方推荐的写法,声明返回的变量名,这样可读性更强
func SumAndProduct(A, B int) (add int, Multiplied int) {
    add = A+B
    Multiplied = A*B
    return
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

指针函数

func add1(a *int) int { // 请注意,
    *a = *a+1 // 修改了a的值
    return *a // 返回新值
}

func main() {
    x := 3

    fmt.Println("x = ", x)  // 应该输出 "x = 3"

    x1 := add1(&x)  // 调用 add1(&x) 传x的地址

    fmt.Println("x+1 = ", x1) // 应该输出 "x+1 = 4"
    fmt.Println("x = ", x)    // 应该输出 "x = 4"
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

延迟语句(defer):defer修饰的语句会在函数返回前调用,多个defer按照后进先出的顺序

func ReadWrite() bool {
    file.Open("file")
    defer file.Close()
    if failureX {
        return false
    }
    if failureY {
        return false
    }
    return true
}
1
2
3
4
5
6
7
8
9
10
11

类异常机制:Panic 和 Recover

// todo

保留函数:main和init

main和init是Go中的保留函数,main方法必须存在于main package中,init方法可以在package中声明,并且Go程序会自动调用该函数,通常只在package中定义一个init函数。

img

# struct类型

Struct

type Skills []string

type Human struct {
    name string
    age int
    weight int
}

type Student struct {
    Human  // 匿名字段,struct
    Skills // 匿名字段,自定义的类型 string slice
    int    // 内置类型作为匿名字段
    speciality string
}

// 访问对应的字段
fmt.Println("His name is ", mark.name)
// 修改对应的字段
mark.name = "Tom"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 面向对象

Go语言

  • 没有封装的特性,所有的方法和属性都是分散的,但是可以进行绑定

  • 有继承的特性,可以以匿名struct的方式继承其他struct及其method

  • 多态的特性,父类struct可以接收子类struct的值,并且通过父类或接口调用子类(继承父类重写)或(实现接口)的方法。

method定义

除了上述的函数之外,Go还可以创建自定义变量的方法,结构体内部不能拥有方法,方法通过以下格式与结构体绑定。这样相同方法名不同参数的写法类似于Java中的方法重载。

// funcName方法属于ReceiverType结构体,可以通过结构体.funcName调用该方法
func (r ReceiverType) funcName(parameters) (results)

type Rectangle struct {
    width, height float64
}

func (r Rectangle) area() float64 {
    return r.width*r.height
}

func main() {
    r1 := Rectangle{12, 2}

    fmt.Println("Area of r1 is: ", r1.area())
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

method继承

如果一个structA中有一个匿名structB,即A继承了B的字段+method,所以通过A也可以调用B的method

type Human struct {
    name string
    age int
    phone string
}

type Student struct {
    Human // 匿名字段
    school string
}

func (h *Human) SayHi() {
    fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)
}

func main() {
    mark := Student{Human{"Mark", 25, "222-222-YYYY"}, "MIT"}
    mark.SayHi()
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# interface

interface是一组 method 签名的组合,我们通过 interface 来定义对象的一组行为。如果某个对象实现了某个接口的所有方法,则此对象就实现了此接口。

  • interface多态:interface变量可以存放(赋值)任意类型实现了该interface的对象,类似Java中的多态
  • 空 interface: 可以存储任意类型的数值,方法返回interface{},说明该方法可以返回任意值
// 定义 a 为空接口
var a interface{}
var i int = 5
s := "Hello world"
// a 可以存储任意类型的数值
a = i
a = s
1
2
3
4
5
6
7
  • 判断interface变量存储的类型:todo

# 反射

  • todo:深入学习

Go 语言实现了反射,所谓反射就是能检查程序在运行时的状态。我们一般用到的包是 reflect 包。

  • 获取类型元数据:t := reflect.TypeOf(i)
  • 获取对象存储值:v := reflect.ValueOf(i)
t := reflect.TypeOf(i)    // 得到类型的元数据,通过t我们能获取类型定义里面的所有元素
v := reflect.ValueOf(i)   // 得到实际的值,通过 v 我们获取存储在里面的值,还可以去改变值

name := t.Elem().Field(0).Name  // 获取定义在 struct 里面第一个字段的字段名
value := v.Elem().Field(0).String()  // 获取存储在第一个字段里面的值
1
2
3
4
5

# 并发

Go语言支持并发编程,goroutine 是 Go 并行设计的核心。goroutine本质上就是协程,与进程和线程的关系类似,线程和协程师一对多的关系,协程共享线程的数据。特点是:

  • 内存占用小:协程为2KB左右,而线程要几M,在大并发下创建线程会导致内存被大量占用
  • 上下文切换开销小:共享线程的大部分数据,只切换协程小部分的数据
  • 不需要做并发处理:在同个线程内,所以不需要做加锁和其他并发处理(可见性),协程间通过管道通信
  • 需要手动控制调度

Go语言中通过go关键字创建协程,runtime 包中有几个处理 goroutine 的函数:

  • Gosched():让出CPU给其他协程
  • Goexit:退出当前执行的 goroutine,但是 defer 函数还会继续调用
  • NumGoroutine:返回正在执行和排队的任务总数
  • NumCPU:返回 CPU 核数量
// 在本案例中,由于协程本身就是运行在单线程中,所以go创建的协程不一定会被执行,很可能还没执行

func say(s string) {
	for i := 0; i < 5; i++ {
    // 表示让 CPU 把时间片让给别人,下次某个时候继续恢复执行该 goroutine
		runtime.Gosched()
		fmt.Println(s)
	}
}


func main() {
	go say("world") // 开一个新的 Goroutines 执行
	say("hello") // 当前 Goroutines 执行
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

channels

goroutine间通过channel进行通信,channel是一个通信管道,提供了双向通信功能,可以通过他发送和接收数据。默认的channel的大小为0,每次发送/接收数据都会阻塞直到数据被接收/发送。

ci := make(chan int)
cs := make(chan string)
cf := make(chan interface{})

ch <- v    // 发送 v 到 channel ch.
v := <-ch  // 从 ch 中接收数据,并赋值给v
1
2
3
4
5
6
func say(s string, ch chan int) {
	for i := 0; i < 5; i++ {
		ch <- i
	}
  
  // 有for + range如果不主动close会造成死锁
	close(ch)
}


func main() {
	ch := make(chan int)
	go say("world", ch) // 开一个新的 Goroutines 执行
	
  // 遍历channel中所有元素直到channel被close
	for i := range ch{
		fmt.Print(i)
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

select

select可以实现多路复用的功能,通过select可以监听多个channel,select默认是阻塞的,监听到消息后会随机选择一个管道执行。

// 此处的变量声明采用省略式写法,等价于:(c chan int, quit chan int)
func fibonacci(c, quit chan int) {
	x, y := 1, 1
	for {
		select {
    // 向管道c发送元素,并阻塞直到有人接收则触发代码
		case c <- x:
      // 等价于x=y,y=x+y
			x, y = y, x + y 
    // 监听管道quit并阻塞,直到收到管道消息并触发代码
		case <-quit:
			fmt.Println("quit")
			return
		}
	}
}

func main() {
	c := make(chan int)
	quit := make(chan int)
	go func() {
		for i := 0; i < 10; i++ {
      // 匿名式写法接收数据
			fmt.Println(<-c)
		}
		quit <- 0
	}()
	fibonacci(c, quit)
}
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

# 三、Go常见面试题

通过面试题快速学习Go语言

# 个人Go体验

  • 代码简洁灵活
    • 类的公私有:首字母大小写
    • 变量定义:可以不指定具体类型
    • 方法返回值:多值返回
    • 继承非常方便:直接将某个struct结构体作为字段即可拥有其所有字段,通过类似struct.field.field的方式读取变量
  • 可读性较差
    • 自定义method:Java是聚合到某个类中,有哪些方法一目了然,而Go可能分散在各个地方,可读性较差
    • 灵活带来的可能的可读性差:比如返回值可以匿名多返回,而不声明返回类型及变量名可能导致可读性降低
  • 函数编程让人头晕

# Go和Java有什么不同

  1. 性能上:golang的性能比Java更好,占用内存更少,使用goroutine避免内核态和用户态切换成本
  2. 编译部署:Java通过虚拟机编译,使用JVM跨平台编译;Go中不存在虚拟机,而是针对不同的平台,编译对应的机器码
  3. 功能上:
    • 接口:java等面向对象编程的接口是侵入式接口,需要明确声明自己实现了某个接口。 而Golang的非侵入式接口不需要通过任何关键字,只要一个类型实现了接口的所有方法,就是这个接口的实现。
    • 继承:Java的继承通过extends关键字完成,不支持多继承;Go语言的继承通过Struct的方式,子类只需要把基类作为成员放在子类的定义中,支持多继承。
    • 异常处理:java中错误(Error)和异常(Exception)被分类管理,golang中只有error,一旦发生错误逐层返回,直到被处理。
    • 并发:通常借助于共享内存(全局变量)作为线程间通信的媒介,通常会有线程不安全问题,使用了加锁(同步化)、使用原子类、使用volatile提升可见性等解决;但在Golang中使用的是通道(channel)作为协程间通信的媒介,多个goroutine之间通过Channel来通信,chan的读取和写入操作为原子操作,所以是安全的。
    • 垃圾回收和内存管理 (opens new window)
Last Updated: 2022-10-19 10:30:06 ├F10: PM┤