Go简易教程笔记

104 阅读9分钟

Go 是一门编译型,具有静态类型和类 C 语言语法的语言,并且有垃圾回收(GC)机制。

变量声明赋值

格式 : var + 变量名 + 变量类型

var age int
age = 1000

简写为

var age int = 9000

短变量声明符::=,它可以自动推断变量类型。

var age := 9000

同时声明多个变量

name,age := "张三",18

多个变量同时声明时,只要其中有一个变量是新的,就可以继续使用 :=,对于已声明类型的变量,只能赋值相同类型的值。

注意 :

  1. 在相同作用域下,同一个变量不能被声明两次。
  2. 不允许在程序中拥有未使用的变量。
  3. import的包未使用也会导致编译失败。

变量类型

按类别区分几种类型:布尔型,数字型,字符串型,派生类型。

派生类型可分为:
  1. 指针类型:pointer

  2. 数组类型

  3. 结构化类型:struct

  4. Map类型

  5. 切片类型

  6. 接口类型:interface

  7. 函数类型

  8. channel类型

运行代码

命令:go run 文件名,它包含了 编译执行 。它使用一个临时目录来构建程序,执行完然后清理掉目录。

加上 --work 参数可以查看临时文件的位置。

文件编译命令:go build 文件名,将会产生一个可执行文件,你可以运行它。

开发环境时,以上两个命令时都可以使用。正式环境时,使用 go build 产生的二进制文件,并执行它。

导入包

import 关键字被用于去声明文件中代码要使用的包。

内置函数 :

len:返回字符串的长度或返回字典值的长度或返回数组的元素的数量。

无网环境,获取本地文档

godoc -http=:6060

函数声明

格式:func + 函数名称(参数+参数类型) + 返回值类型

以下三个函数:一个没有返回值,一个有一个返回值,一个有两个返回值。

func log(message string){
}
func add(a int, b int) int{
}
func power(name string) (int,bool){
}

函数返回值赋值给空白字符 _

示例:

_,exists := power("zhangsan")
if exists == false {
    // 错误情况处理
}

参数有相同类型,可以使用以下简洁语法

func add(a, b int) int{
}

结构体

Go 不像 Java 等语言是一门面向对象的语言,当然也没有重载和多态等和面相对象相关的概念。

Go 所具备的结构体概念,可以将一些方法和结构体关联。

定义结构体

格式: type + 结构体名称 + strcut

举例:

type Saiyan struct {
	Name string
	Power int
}

创建结构体的值:

goku := Saiyan {
	Name:"goku",
	Power:900001,
}

不写字段名,依赖字段顺序去初始化结构体

goku := Saiyan{
	"goku",
	 900001,
}

注意: 结构末尾的 , 是必须的。

Go 中传递参数到函数是镜像赋值,运行以下程序运行结果是 90001。因为这里只是修改 原始值 goku 的赋值版本,而不是它本身。

func main(){
	// go 传递参数到函数是镜像赋值
	goku := Saiyan{
		"Goku",
		 90001,
	}
	Super(goku)
	fmt.Println(goku.Power)
}
func Super(s Saiyan){
	s.Power += 10000
}

如何修改 goku 本身呢,需要传递指针到函数内,具体示例如下:

func main(){
	goku := &Saiyan{"Goku",90001}
	Super(goku)
	fmt.Println(goku.Power)
}
func Super(s *Saiyan){
	s.Power += 10000
}

注意: 这里修改了两个地方。& :取地址操作符,用于获取值的地址。修改了 Super 参数的期望类型。它之前期望是一个 Saiyan 类型,现在它期望的是一个地址类型 *Saiyan*X :指向类型 X 值的指针。

指针真正价值在于能够分享它所指向的值。

构造器

结构体没有构造器。但是可以创建一个返回所期望类型的实例(类似于工厂)。

func NewSaiyan(name string,power int) Saiyan{
	return Saiyan{
		Name:name,
         Power:power,
	}
}

New : 即使 Go 中没有构造函数,却有一个内置的 New 函数 ,使用它来分配类型所需要的内存。作用等同于 &X{}

goku := new(Saiyan)
goku.Name = "Goku"
goku.Power = 1000
等同于
goku:= &Saiyan{"Goku",1000}

不过推荐使用第一种方式,看起来更具备可读性。

组合

Go 支持组合,这是将一个结构包含进另外一个结构的行为。举例:

type Person struct {
	Name string
}
func (p *Person) Introduce() {
	fmt.Printf("Hi, I'm %s\n", p.Name)
}
type Saiyan struct {
	*Person
	Power int
}
func main() {
	goku := &Saiyan{
		Person: &Person{"Goku"},
		Power:  9001,
	}
	goku.Introduce()
	fmt.Println(goku.Name)
	// 等同于
	fmt.Println(goku.Person.Name)
}

数组 Arrays

声明 : var + 数组名[数组长度] + 类型

举例:

var scores [10]int
scores[0] = 123

初始化数组指定值

scores := [4]int{1,2,3,4}

注意点:

  1. 数组长度是固定的。

  2. 声明一个数组需要指定它的长度,指定长度后不可以改变。

    声明数组示例

  3. len 函数获取数组的长度。

遍历数组:range 在遍历迭代中使用。

for index,value := range scores{
    // to do something
}

数组非常高效但是十分死板。很多时候,事先我们并不知道数组的长度,针对这个情况,切片 slices 就出来了。

切片 Slice

几种创建切片的方式

  1. 创建 slice,用于当元素数量未知时与 append 连接。

    var names []string
    
  2. 事先知道数组中的值的时候,可以使用以下方式声明。和数组有点类似,但是不需要在方括号内声明长度。

    scores := []int{1,23,6,4,5}
    
  3. 使用 make 函数创建切片,格式:make([]类型,长度,容量即用来指定预留的空间长度) 。当你想要写入切片具体的索引时,这种方式很有用。

    scores := make([]int,10)
    
  4. 创建一个初始容量的 slice ,我们大概知道元素的容量时很有用。

    scores := make([]int,0,10)
    

    创建一个切片具体来说,我们必须要为一个底层数组分配一段内存,同时初始化这个切片的长度和容量。

    当我们为 slice 分配内存的时候,尽可能预估到 slice 的最大长度,通过 make 传递第三个参数为 slice 预留好内存空间,这样可以避免二次分配内存带来的开销,极大地提高效率。

扩展切片:通过关键字 append 来实现

func main(){
	scores := make([]int,0,10)
	scores = append(scores,5)
	fmt.Println(scores)
}

追加一个长度为 0 的切片将会设置为第一个元素,当我们想设置索引为 7 的索引值,可以重新切片:

func main(){
	scores := make([]int,0,10)
	scores = scores[0:8]
	scores[7] = 123
	fmt.Println(scores)
}

调整切片大小的最大范围是此切片的容量。

Go 使用 2x 算法来增加数组的长度。

举例

func main() {
	scores := make([]int, 0, 5)
	c := cap(scores)
	fmt.Println(c)
	for i := 0; i < 25; i++ {
		scores = append(scores, i)
		if cap(scores) != c {
			c = cap(scores)
			fmt.Println(c)
		}
	}
}
// 结果:5 10 20 40

对于编译器而言,是追加一个值到已经有五个值的切片:

func main() {
	scores := make([]int, 5)
	scores = append(scores, 12342)
	fmt.Println(scores)
}
// 结果:[0,0,0,0,12342]

[X:]X 到尾,[:X] 从开始到 XGo 语言不支持负数索引。

移除切片中最后一个元素

scores := []int{1,2,3,4,5}
scores = scores[:len(scores)-1]

生成 1000 以内的随机整数

import("math/rand")
rand.Int31n(1000)

数组正序排序

import("sort")
scores := []int{1,3,4,232,}
sort.Ints(scores)

内置函数:copy

func main() {
	// scores := make([]int, 100)
	scores := []int{0,1,2,3444,43,343}
	for i := 0; i < 100; i++ {
	  scores[i] = int(rand.Int31n(1000))
	}
	sort.Ints(scores)
	worst := make([]int, 5)
	copy(worst,scores[5:100])
	fmt.Println(worst)
  }

映射 Map

Go 语言中的映射类似于其它语言的 hash 表。它们的工作方式就是:可以定义键和值,可以获取,设置和删除其中的值。

创建映射:使用 make 创建

lookup := make(map[string]int)

获取映射键的数量

len(lookup)

删除映射中某个键值。没有返回值,可以再不存在的键值上调用。

delete(lookup,"power")

映射是动态变化的。设置第二个参数到 make 方法中可以设置一个初始化大小。

lookup := make(map[string]int,100)

如果提前知道映射会有多少键值,那么设置初始化大小可以改善性能。

将映射设置为结构体的字段时,可以这样定义:

type Saiyan struct{
	Name string
    Friends map[string]*Saiyan
}

初始化上述结构体:

goku := &Saiyan{
    Name:"goku",
    Friends:make(map["goku"]*Saiyan),
}

复合方式

lookup :=map[string]int{
    "goku":9000,
    "meme":23442,
}

迭代映射

使用 forrange 关键字迭代映射

for key,value := range lookup {
}

迭代映射没有顺序。每次迭代映射将会随机返回键值对。

包管理

**包外可见性:**如果一个类型或函数名称首字母以大写开始的话,它就具备了包外可见性。如果以一个小写字母开始,它就不可以。

**结构体字段:**如果一个字段以小写字母命名,则只有包内的代码可以访问他们。

go get 库名:获取第三方库。

**依赖管理:**如果我们在一个项目内使用 go get , 它将浏览所有的文件,查找所有 improts 的第三款库并下载他们。某种程度上,我们的代码变成了 Gemfilepackage.json'

go get -u:它将更新所有的包。

go get -u FULL_PACKAGE_NAME:更新一个具体的包。

不足之处:go get 没有办法指定版本。

这个时候需要引入第三方的依赖管理工具:goopgode

接口

接口是定义了合约但并没有实现类型。接口有助于将代码和特定的实现分离。

举例:

type Logger interface{
    Log(message string)
}
type SqlLogger struct { ... }
type ConsoleLogger struct { ... }

错误处理

Go 首选的错误处理方式是返回值而不是异常。

strconv.Atoi 函数:它可以接受一个字符串并将它转换成一个整数。

你可以创建自己的错误类型,但是必须实现内建 error 接口的契约。

Go 标准库中有一个使用 error 变量的通用模式。比如 io 包中有一个 EOF 变量它是这样定义的:

var EOF = errors.New("EOF")

Go 有两个函数 panicrecover ,但是很少使用。

  • panic : 抛出异常。
  • recover : 类似于 catch
关键字 Defer

尽管 Go 有自带的垃圾回收器,但是有一些显式资源还是需要我们手动去释放。比如使用完文件后需要 close 它们。

当一个函数有多个返回点,Go 给出的解决方案使用 defer 关键字。

无论什么情况,在函数返回之后, defer 将被执行。

应用场景举例:

  • 函数退出时的日志记录。
  • 资源管理。
Go 语言风格

当你在一个项目内,你可以运用格式化规则到这个项目及其子目录。

go fmt ./....

它不仅缩进你的代码,也对齐了声明的字段和按字母书序导入。

空接口和转化

Go 没有继承,不过有一个没有任何方法的空接口 interface{}

将一个接口变量转化为显式的类型,可以用 .(TYPE)

return a.(int)

类型转换

switch a.(type) {
  case int:
    fmt.Printf("a is now an int and equals %d\n", a)
  case bool, string:
    // ...
  default:
    // ...
}
字符串和字节数组

字符串和字节数组关系是紧密相关的,它们之间可以轻松互转。

stra := "this is str"
byts := []byte(stra)
strb = string(byts)

当你使用 []byte()Xstring(X) 时,实际上是创建了数据的副本。因为字符串是不可变的。

函数类型

函数是一种类型。它可以用在任何地方,作为字段类型,参数或者返回值。

package main
import "fmt"
type Add func(a int, b int) int
func main() {
	fmt.Println(process(func(a int, b int)int {
		return a + b
	}))
}
func process(adder Add) int {
	return adder(1, 5)
}

并发

Go 是一门并发友好的语言,原因它提供了两种强大机制的简单语法: 协程通道

Go 协程

协程类似于一个线程,但是由 go 而不是操作系统决定。在协程中运行的代码可以与其它代码同时运行。

如何开始协程?

使用关键字 go ,然后使用想要执行的函数。

注意:

  1. 协程易于创建而且开销很小。( M:N 线程模型: M 个应用线程(协程)运行在 N 个系统操作线程上。一个协程的开销比操作系统线程相比少几 KB
  2. 隐藏了映射和调度复杂性。我们只需要声明这段代码需要并发执行让 go 自己去实现它。
  3. 主进程在退出前,协程才有机会执行。主进程退出前不会等待全部协程执行完毕,需要协调我们的代码。
同步

编写并发代码需要特别注意在哪里读取和写入一个值。

从变量中读取变量是唯一安全的并发处理变量的方式。

写操作必须保持同步,常用的操作还是互斥量 mutex 。互斥量序列化会锁住锁下的代码访问。

package main
import (
	"fmt"
	"time"
	"sync"
)
var (
	counter = 0
	lock    sync.Mutex
)
func main() {
	for i := 0; i < 25; i++ {
		go incr()
	}
	time.Sleep(time.Millisecond * 10)
}
func incr() {
	lock.Lock()
	defer lock.Unlock()
	counter++
	fmt.Println(counter)
}

读写互斥锁:主要提供了两种功能 锁定写入锁定读取 。它的区别是允许多个同时读取,同时确保写入是独占的。在 Go 中 , RWMetux 就是读写互斥锁。

通道

为了解决协调并发代码,Go 提供了通道。

并发编程最大的调整源于 数据共享

一个通道和其它变量一样,都有一个类型。这个类型是在通道中 传递的数据 的类型 。

格式 : make(chan 类型, 长度)

举例:创建一个通道用于传递一个整数。

c := make(chan int)

通道只支持两种操作 接收发送 。接收和发送操作是阻塞的。

往通道发送一个数据:

CHANNEL <- DATA

从通道接收一个数据:

VAR := <-CHANNEL

举例:

package main
import (
	"fmt"
	"time"
	"math/rand"
)
type Worker struct {
	id int
}
func main() {
	c := make(chan int)
	for i := 0; i < 5; i++ {
		worker := &Worker{id: i}
		go worker.process(c)
	}
	for {
		// 往通道发送数据
		c <- rand.Int()
		time.Sleep(time.Millisecond * 50)
	}
}
func (w *Worker) process(c chan int) {
	for {
		// 从通道接收数据
		data := <-c
		fmt.Printf("worker %d got %d \n", w.id, data)
	}
}

我们不确定哪一个 worker 会接收到什么数据。但是 Go 可以保证 发送到通道的数据只会被一个接收器接收。

缓冲通道: 如果没有 worker 可用,我们想去临时存储数据在某些队列中。通道内建这种缓冲容量,当我们使用 make 创建通道的时候,可以设置通道的长度。缓冲通道不会增加容量,他们只提供待处理工作的队列,以及处理突然飙升的任务量的好方法。

c := make(chan int,100)

select: 主要目的是管理多个通道, select 将阻塞直到第一个通道可用。当没有通道可用,如果提供了 default ,它就会执行。

for {
	select {
		case c <- rand.Int() :
		// 执行代码
		default:
		// 这里可以留空静默删除数据或做其他操作
		fmt.Println("dropped")
	}
}

超时: 为了阻塞最长时间,我们可以使用 time.After() 函数。

selcet {
	case c <-rand.Int():
	case <-time.After(time.Millisecond * 100):
		fmt.Println("time out")
}
time.Millisecond * 50

time.After() 返回了一个通道 所以我们在 select 中可以使用它。time.After 是一个 chan time.Time 类型的通道。

select 的工作方式是相同的:

  1. 第一个可用的通道被选中。
  2. 当有多个通道时,随机选择一个通道使用。
  3. 如果没有通道可用, default 将会被执行。
  4. 如果没有 defaultselect 将会被阻塞。

Goroutines 有效地帮我们抽象了需要并发执行的代码。通道帮助消除数据共享时共享数据可能发生的一些严重错误。