blog icon indicating copy to clipboard operation
blog copied to clipboard

Go 学习笔记

Open Lmagic16 opened this issue 5 years ago • 0 comments

Go 学习笔记

目录:

  1. 简介
  2. Go 程序的基本结构
  3. 流程控制语句
  4. 更多类型:结构体、数组、切片、映射
  5. 方法和接口
  6. 并发机制

一、简介

官网:https://golang.org/

Go 语言之旅:https://tour.go-zh.org/list

Go 是从 2007 年末由 Robert Griesemer, Rob Pike, Ken Thompson 主持开发,后来还加入了 Ian Lance Taylor, Russ Cox 等人,并最终于 2009 年 11 月开源,在 2012 年早些时候发布了 Go 1 稳定版本。现在 Go 的开发已经是完全开放的,并且拥有一个活跃的社区。

Go 语言特性:并发、静态类型的编译性语言、垃圾回收的便利性、运行时反射快速,有点像动态类型的解释语言(JS)。

Go 语言被设计成一门应用于搭载 Web 服务器,存储集群或类似用途的巨型中央服务器的系统编程语言。

对于高性能分布式系统领域而言,Go 语言无疑比大多数其它语言有着更高的开发效率。它提供了海量并行的支持,这对于游戏服务端的开发而言是再好不过了。

二、Go 程序的基本结构

1. 包

每个 Go 程序都是由包构成的。程序从 main 包开始运行。

通过 import 语句来导入包。

**注意:**在 Go 中,如果一个名字以大写字母开头,那么它就是已导出的。未以大写字母开头的是未导出的。任何“未导出”的名字在该包外均无法访问。

2. 函数

函数通过 func 关键字来声明。函数可以没有参数或接受多个参数。参数需要有类型声明。

当连续两个或多个函数的已命名形参类型相同时,除最后一个类型以外,其它都可以省略。

函数可以返回任意数量的返回值。

package main

import "fmt"

func add(x int, y int) int {
	return x + y
}

func main() {
	fmt.Println(add(42, 13))
}

3. 变量

var 语句用于声明一个变量列表,跟函数的参数列表一样,类型在最后。

变量声明可以包含初始值,每个变量对应一个。

如果初始化值已存在,则可以省略类型;变量会从初始值中获得类型。

package main

import "fmt"

var i, j int = 1, 2

func main() {
	var c, python, java = true, false, "no!"
	fmt.Println(i, j, c, python, java)
}
// output:1 2 true false no!

短变量声明:

在函数中,简洁赋值语句 := 可在类型明确的地方代替 var 声明。

函数外的每个语句都必须以关键字开始(var, func 等等),因此 := 结构不能在函数外使用。

package main

import "fmt"

func main() {
	var i, j int = 1, 2
	k := 3
	c, python, java := true, false, "no!"

	fmt.Println(i, j, k, c, python, java)
}
// output: 1 2 3 true false no!

4. 基本类型

Go 的基本类型有:

bool

string

int  int8  int16  int32  int64
uint uint8 uint16 uint32 uint64 uintptr

byte // uint8 的别名

rune // int32 的别名
    // 表示一个 Unicode 码点

float32 float64

complex64 complex128

零值:

没有明确初始值的变量声明会被赋予它们的 零值

零值是:

  • 数值类型为 0
  • 布尔类型为 false
  • 字符串为 ""(空字符串)。

类型转换:

表达式 T(v) 将值 v 转换为类型 T

Go 在不同类型的项之间赋值时需要显式转换。

var i int = 42
var f float64 = float64(i)
var u uint = uint(f)

类型推导:

在声明一个变量而不指定其类型时(即使用不带类型的 := 语法或 var = 表达式语法),变量的类型由右值推导得出。

当右值声明了类型时,新变量的类型与其相同;当右边包含未指明类型的数值常量时,新变量的类型就可能是 int, float64complex128 了,这取决于常量的精度。

5. 常量

常量的声明与变量类似,只不过是使用 const 关键字。

常量可以是字符、字符串、布尔值或数值。

常量不能用 := 语法声明。

三、流程控制语句

1. for

Go 只有一种循环结构:for 循环。

基本的 for 循环由三部分组成,它们用分号隔开:

  • 初始化语句:在第一次迭代前执行
  • 条件表达式:在每次迭代前求值
  • 后置语句:在每次迭代的结尾执行

初始化语句通常为一句短变量声明,该变量声明仅在 for 语句的作用域中可见。

一旦条件表达式的布尔值为 false,循环迭代就会终止。

初始化语句和后置语句是可选的。

for 是 Go 中的 “while”。

注意:和 C、Java、JavaScript 之类的语言不同,Go 的 for 语句后面的三个构成部分外没有小括号, 大括号 { } 则是必须的。

package main

import "fmt"

func main() {
	sum := 0
	for i := 0; i < 10; i++ {
		sum += i
	}
	fmt.Println(sum)
}
//无限循环。如果省略循环条件,该循环就不会结束,因此无限循环可以写得很紧凑。
for {
	}

2. if

Go 的 if 语句与 for 循环类似,表达式外无需小括号 ( ) ,而大括号 { } 则是必须的。

if 的简短语句:

for 一样, if 语句可以在条件表达式前执行一个简单的语句。该语句声明的变量作用域仅在 if 之内。

if 的简短语句中声明的变量同样可以在任何对应的 else 块中使用。

package main

import (
	"fmt"
	"math"
)

func pow(x, n, lim float64) float64 {
	if v := math.Pow(x, n); v < lim {
		return v
	} else {
		fmt.Printf("%g >= %g\n", v, lim)
	}
	// 这里开始就不能使用 v 了
	return lim
}

func main() {
	fmt.Println(
		pow(3, 2, 10),
		pow(3, 3, 20),
	)
}

3. switch

Go 的 switch 语句类似于 C、C++、Java、JavaScript 和 PHP 中的,不过 Go 只运行选定的 case,而非之后所有的 case。 实际上,Go 自动提供了在这些语言中每个 case 后面所需的 break 语句。 除非以 fallthrough 语句结束,否则分支会自动终止。 Go 的另一点重要的不同在于 switch 的 case 无需为常量,且取值不必为整数。

switch 的 case 语句从上到下顺次执行,直到匹配成功时停止。

4. defer

defer 语句会将函数推迟到外层函数返回之后执行。

推迟调用的函数其参数会立即求值,但直到外层函数返回前该函数都不会被调用。

推迟的函数调用会被压入一个栈中。当外层函数返回时,被推迟的函数会按照后进先出的顺序调用。

package main

import "fmt"

func main() {
	fmt.Println("counting")

	for i := 0; i < 3; i++ {
		defer fmt.Println(i)
	}

	fmt.Println("done")
}
// output: 
counting
done
2
1
0

四、更多类型:结构体、数组、切片、映射

1. 指针

Go 拥有指针。指针保存了值的内存地址。

类型 *T 是指向 T 类型值的指针。其零值为 nil

& 操作符会生成一个指向其操作数的指针。

* 操作符表示指针指向的底层值。

与 C 不同,Go 没有指针运算。

package main

import "fmt"

func main() {
	i, j := 42, 2701

	p := &i         // 指向 i
    fmt.Println(*p) // 通过指针读取 i 的值: 42
	*p = 21         // 通过指针设置 i 的值
    fmt.Println(i)  // 查看 i 的值: 21

	p = &j         // 指向 j
	*p = *p / 37   // 通过指针对 j 进行除法运算
    fmt.Println(j) // 查看 j 的值: 73
}
// output:
42
21
73

2. 结构体

一个结构体(struct)就是一组字段(field)。

结构体字段使用点号来访问。

**结构体指针:**结构体字段可以通过结构体指针来访问。如果我们有一个指向结构体的指针 p,那么可以通过 (*p).X 来访问其字段 X。不过这么写太啰嗦了,所以语言也允许我们使用隐式间接引用,直接写 p.X 就可以。

结构体文法:

结构体文法通过直接列出字段的值来新分配一个结构体。

使用 Name: 语法可以仅列出部分字段。(字段名的顺序无关。)

特殊的前缀 & 返回一个指向结构体的指针。

package main

import "fmt"

type Vertex struct {
	X, Y int
}

var (
	v1 = Vertex{1, 2}  // 创建一个 Vertex 类型的结构体
	v2 = Vertex{X: 1}  // Y:0 被隐式地赋予
	v3 = Vertex{}      // X:0 Y:0
	p  = &Vertex{1, 2} // 创建一个 *Vertex 类型的结构体(指针)
)

func main() {
	fmt.Println(v1, p, v2, v3)
}
// output: {1 2} &{1 2} {1 0} {0 0}

3. 数组

类型 [n]T 表示拥有 nT 类型的值的数组。

数组的长度是其类型的一部分,因此数组不能改变大小。这看起来是个限制,不过没关系,Go 提供了更加便利的方式来使用数组。

4. 切片

每个数组的大小都是固定的。而切片则为数组元素提供动态大小的、灵活的视角。在实践中,切片比数组更常用。

类型 []T 表示一个元素类型为 T 的切片。

package main

import "fmt"

func main() {
	names := [4]string{
		"John",
		"Paul",
		"George",
		"Ringo",
	}
	fmt.Println(names)

	a := names[0:2]
	b := names[1:3]
	fmt.Println(a, b)

	b[0] = "XXX"
	fmt.Println(a, b)
	fmt.Println(names)
}
// output:
[John Paul George Ringo]
[John Paul] [Paul George]
[John XXX] [XXX George]
[John XXX George Ringo]

切片就像数组的引用:

切片并不存储任何数据,它只是描述了底层数组中的一段。

更改切片的元素会修改其底层数组中对应的元素。

与它共享底层数组的切片都会观测到这些修改。

切片的默认行为:切片下界的默认值为 0,上界则是该切片的长度。

切片拥有 长度容量,可通过表达式 len(s)cap(s) 来获取。

切片的零值是 nil。nil 切片的长度和容量为 0 且没有底层数组。

切片操作:

为切片追加新的元素是种常用的操作,为此 Go 提供了内建的 append 函数。

append 的结果是一个包含原切片所有元素加上新添加元素的切片。

s 的底层数组太小,不足以容纳所有给定的值时,它就会分配一个更大的数组。返回的切片会指向这个新分配的数组。

5. Range

for 循环的 range 形式可遍历切片或映射。

package main

import "fmt"

var pow = []int{1, 2, 4}

func main() {
	for i, v := range pow {
		fmt.Printf("2**%d = %d\n", i, v)
	}
}
// output: 
2**0 = 1
2**1 = 2
2**2 = 4

6. 映射

映射将键映射到值。

映射的零值为 nilnil 映射既没有键,也不能添加键。

make 函数会返回给定类型的映射,并将其初始化备用。

package main

import "fmt"

type Vertex struct {
	Lat, Long float64
}

var m = map[string]Vertex{
	"Bell Labs": Vertex{
		40.68433, -74.39967,
	},
	"Google": Vertex{
		37.42202, -122.08408,
	},
}

func main() {
    fmt.Println(m) // map[Bell Labs:{40.68433 -74.39967} Google:{37.42202 -122.08408}]
    m["Answer"] = 42 // 插入或修改元素
	fmt.Println("The value:", m["Answer"]) // 42
    delete(m, "Answer") // 删除元素
	fmt.Println("The value:", m["Answer"]) // 0
	v, ok := m["Answer"] // 通过双赋值检测某个键是否存在:
	fmt.Println("The value:", v, "Present?", ok) // The value: 0 Present? false
}

五、方法和接口

1. 方法

Go 没有类。不过你可以为结构体类型定义方法。

方法就是一类带特殊的 接收者 参数的函数。

方法接收者在它自己的参数列表内,位于 func 关键字和方法名之间。

package main

import (
	"fmt"
	"math"
)

type Vertex struct {
	X, Y float64
}

// Abs 方法拥有一个名为 v,类型为 Vertex 的接收者。
func (v Vertex) Abs() float64 {
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func main() {
	v := Vertex{3, 4}
	fmt.Println(v.Abs()) // 5
}

方法只是个带接收者参数的函数。

也可以为非结构体类型声明方法。

可以为指针接收者声明方法。

2. 接口

接口类型 是由一组方法签名定义的集合。可以理解为的概念。

接口类型的变量可以保存任何实现了这些方法的值。

Go 语言提供了另外一种数据类型即接口,它把所有的具有共性的方法定义在一起,任何其他类型只要实现了这些方法就是实现了这个接口。

package main

import (
    "fmt"
)

// 我们定义了一个接口Phone,接口里面有一个方法call()
type Phone interface {
    call()
}

type NokiaPhone struct {
}

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

type IPhone struct {
}

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

func main() {
    var phone Phone // 定义了一个Phone类型变量

    phone = new(NokiaPhone) // 为变量phone赋值NokiaPhone
    phone.call() // I am Nokia, I can call you!

    phone = new(IPhone) // 为变量phone赋值IPhone
    phone.call() // I am iPhone, I can call you!

}

类型断言与类型选择:

package main

import "fmt"

func do(i interface{}) {
	switch v := i.(type) { // 判断 一个接口值是否保存了一个特定的类型
	case int:
		fmt.Printf("Twice %v is %v\n", v, v*2)
	case string:
		fmt.Printf("%q is %v bytes long\n", v, len(v))
	default:
		fmt.Printf("I don't know about type %T!\n", v)
	}
}

func main() {
	do(21) // Twice 21 is 42
	do("hello") // "hello" is 5 bytes long
	do(true) // I don't know about type bool!
}

六、并发机制

1. Go 程(Goroutines)

Go 程(goroutine)是由 Go 运行时管理的轻量级线程。go f(x, y, z) 会启动一个新的 Go 程并执行 f(x, y, z),f, x, yz 的求值发生在当前的 Go 程中,而 f 的执行发生在新的 Go 程中。Go 程在相同的地址空间中运行,因此在访问共享的内存时必须进行同步。

Goroutines 具有自己的调用堆栈,该调用堆栈会根据需要进行扩展和收缩。

2. 信道

信道是带有类型的管道,你可以通过它用信道操作符 <- 来发送或者接收值。

信道非常适合在各个 Go 程间进行通信。

ch := make(chan int) // 和映射与切片一样,信道在使用前必须创建
ch <- v    // 将 v 发送至信道 ch。
v := <-ch  // 从 ch 接收值并赋予 v。
// “箭头”就是数据流的方向。

默认情况下,发送和接收操作在另一端准备好之前都会阻塞。这使得 Go 程可以在没有显式的锁或竞态变量的情况下进行同步。

package main

import "fmt"

func sum(s []int, c chan int) {
	sum := 0
	for _, v := range s {
		sum += v
	}
	c <- 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)
}
// output: -5 17 12

互斥锁:

但是如果我们并不需要通信呢?比如说,若我们只是想保证每次只有一个 Go 程能够访问一个共享的变量,从而避免冲突?

这里涉及的概念叫做 互斥(mutualexclusion)* ,我们通常使用 互斥锁(Mutex) 这一数据结构来提供这种机制。

Go 标准库中提供了 sync.Mutex 互斥锁类型及其两个方法:Luck 和 Unlock。

select 语句:

select 语句使一个 Go 程可以等待多个通信操作。

select 会阻塞到某个分支可以继续执行为止,这时就会执行该分支。当多个分支都准备好时会随机选择一个执行。

Lmagic16 avatar Nov 16 '20 07:11 Lmagic16