第一章 简介

  • goroutine
    goroutine很像线程,但是它占用的内存远少于线程,需要的代码
  • 通道
    channel是一种内置的数据结构,可以让用户在不同的goroutine之间同步发送具有类型的数据
  • Go语言类型系统
    Go语言提供了灵活的,邬吉成的类型系统,无需降低运行性能就能最大成都上复用代码。

Go使用组合(composition)设计模式,只需要简单的讲一个类型嵌入到另一个类型,就能复用所有的功能。

  • 内存管理
    Go也拥有垃圾回收机制。

第二章快速开始一个go程序

1.main函数保存在名为main的包里,如果不在main包里,构建工具就不会生成可执行的文件。
2.一个包定义一组编译过的代码,类似于命名空间。

import (
    "log"
    "os"
    _ "github.com/goinaction/code/chapter2/sample/matchers"
)

"_" 是为了让go语言对包做初始化,但是并不使用包里的标识符。为了让程序的可读性更强,go编译器不允许声明某个导入却不使用, 下划线是让编译器接收这种导入。

func init() {
    loc.SetOutput(os.Stdout)
}

程序中每个代码文件里的init函数都会在main函数执行前调用。

标识符以大写字母开头,为公开。 如果以小写字母开头则是不公开。不能被其他包中对象引用。

make(map[string]string)

map是Go语言里的一个引用类型,需要使用make来构造。

feeds, err := RetrieveFeeds()

:= 简化变量声明运算符。 这个运算符和var声明的变量没有任何区别

restuls := make(chan *Result)

在 go语言中, channel, map和 slice 一样,也是引用类型。不过通道本身实现的是一组带类型的值,这组值用于在goroutine之间传递数据。通道内制同步机制,从而可以保证通信安全。

如果main函数返回,整个程序也就终止了,Go程序终止时,还会关闭所有之前启动而且还在运行 goroutine

for _, feed := range feeds {
}

for range实现对切片迭代,range可以用于迭代数组,字符串,切片,映射和通道。使用for range时,会返回两个值,第一个值时迭代的元素在切片里元素的位置,第二个时元素值的一个副本。
下划线标识符时占位符, 占据保存range 返回的索引值变量的位置。

matcher, exists := matchers[feed.Type]
if !exists {
    // business logic
}
go func(param int) {
    // code
}{x}

使用关键字go 启动一个goroutine,并对这个goroutine做并发调度

在go语言中, 所有的变量都以值的方式传递

Go语言支持闭包,
因为有闭包,函数可以直接访问那些没有作为参数的变量。匿名函数并没有拿到这些变量的而副本,而是直接访问外层函数作用域中声明的这些变量。

type Feed struct {
    Name string `json:"site"`
    URI string `json:"link"`
    Type string `json:"type"`
}

"`"里面的部分被称作为标记。这个标记里描述了JSON解码的元数据。

defer file.Close()

defer关键字安排随后的函数调用在函数返回后才会执行。

func (dec *Decoder) Decode(v interface{}) error

Decode方法接受一个类型为interface{}的值作为参数。 这个类型在Go语言里很特殊,一般会配合reflect包里提供的反射功能一起使用。

type Matcher interface {
    Search(feed * Feed, searchTerm string) ([]*Result, error)
}

声明一个接口
命名的时候,如果接口类型值包含一个方法,那么这个类型的名字以er结尾.

如果要让用户定义的类型实现一个接口,这个用户定义的类型要实现接口类型里面声明的所有方法。

func (m defaultMatcher) Search(feed *Feed, serachTerm string) ([]*Result, error) {
    return nil, nil
}

Search方法于 defaultMatcher类型的值绑定在一起,意味着我们可以使用defaultMatcher类型的值或者指针来调用Search方法。

因为大部分方法在被调用后都需要维护接收者的值的状态,所以最佳实践是,将方法的接收者声明为指针。

与直接通过值或者指针调用方法不同,如果直接通过接口类型的值调用方法,规则有很大不同。

  • 使用指针作接收者声明方法,只能在接口类型的值是一个指针的时候被调用。
  • 使用值作为接受者声明方法,在接口类型的值为值或指针的时候豆科鱼i被调用。

第三章 打包和工具链

  • 同一个目录下,所有的.go文件必须同一个包名
  • main的包具有特殊的含义。Go语言的编译程序会试图把这种名字的包编译为二进制可执行文件。
import (
    "fmt"
    "strings"
)
  • 如果需要导入多个包,习惯上是将import语句包装在一个导入块中。

编译器会使用Go环境变量设置的路径,通过引入的相对路径来查找磁盘上的包。标准库中的包会在安装Go的位置找到。Go开发者创建的包会在GOPATH环境变狼指定的目录里查找。
GOPATH指定的这些目录就是开发者的个人工作空间。

一旦编译器找到一个满足import的语句的包,就会进一步停止查找。

go get 远程导入 获取任意指定的url的包

go vet 代码检查

go fmt 代码格式化

第四章 数组、切片和映射

数组

  • 声明

    var array [5]int
    array := [5]int{1,2,3,4,5}
    
    // 自动计算长度
    array := [...]int{1,2,3,4,5
array := [5]*int{0: new(int), 1: new(int)}
*array[0] = 10
*array[1] = 20

赋值数组指针值会复制指针的值,不会复制指针所指向的值。

    array1 := [2]*string{new(string),new(string)}
    var array2 [2]*string

    *array1[0] = "Red"
    *array1[1] = "Blue"
    array2 = array1

    *array2[0] ="black"

    fmt.Println(*array1[0])
    fmt.Println(*array2[0])

在函数间传递数组

在函数间传递变量是,总是以值的方式传递。
如果比那辆是一个数组,意味着整个数组,不管有多长,都会完整复制,并传递给函数。
使用指针会更有效的利用内存,性能也更好。

var array [1e6]int

foo(&array)

切片内部的实现和基础

切片是围绕动态数组的概念构建的,可以自动增长和缩小。 通过append来实现的

内部实现

切片有三个字段的数据结构
这些结构包含go需要操作底层数组的元数据
指向底层数组的指针、切片访问的元素个数(即长度)、切片允许增长到的元素个数(容量)

创建和初始化

1 make 和切面字面量
slice := make([]string,5)
//
slice := make([]string,3,5)

如果基于这个切片创建新的切片,新切片回合原有切片共享底层数组,也能通过后期操作来访问多于容量的元素。

slice := []string{"Red","Blue","Green","Yellow"}
slice := []int{10,20,30}

这种方法和创建数组类似,只是不需要指定[]运算符里面的值

//初始化了第100个元素
slice := []string{99:""}
nil 和空切片
var slice []int

空切片

slice := make([]int,0)
slice := []int{}

空切片再底层数组包含0个元素,也没有分配任何存储空间

不管使用nil切片还是空切片,对其调用内置函数append和len 和cap的效果都是一样的

使用切片

使用切片创建切片

slice := []int{10,20,30,40,50}
newSlice := slice[1:3]

这两个切片共享底层的一段数组,但是不同的数组会使用不同的部分。

对于底层数组容量是k的切片
slice[i:j]
长度 [j-i]
容量 [k-i]

切片只能访问到其长度范围内的元素,超出长度的元素将会产生访问异常

切片增长
Go 内置的 append 函数会处理所有细节

slice := []int{10,20,30,40}
newSlice := slice[1:3]

newSlice = append(newSlice, 60)

调用 append时 会返回一个包含修改结果的新切片。

append会处理切片底层数组的容量,在切片容量小于1000个元素时,总是会成倍增加。一旦元素超过1000, 容量的增长因子会设为1.25
创建切片时的3个索引

第三个索引可以控制新切片的容量

slice := source[2:3:4]
对于 slice[i:j:k] 或 [2:3:4]
长度 j - i = 3 - 2 = 1
容量 k - i = 4 - 2 = 2

如果在创建切片是设置切片的容量和长度一样,就可以强制让新切片的第一个append操作创建新的底层数组,于原有的底层数组分离。新切片于原有底层数组分离后,可以安全的进行后续修改。

迭代切片
for index, value := range slice{
}

需要强调的是, range创建了每个元素的副本,而不是直接对该元素的引用。
因为迭代返回的变量是一个迭代过程中根据切片一次复制的新变量, 所以value的地址总是相同的。 要想获取每个元素的地址,可以使用切片变量和索引值。

多为切片

slice := [][]int{{10},{10,20}}

在函数见传递切片

切片的尺寸很小,在函数见复制和传递切片成本也很低。

映射

创建和初始化
dict := make(map[string]int)


dict := map[string]string {"Red" :"1", "Orange":"2"}
使用映射
value, exist := dict["Blue"]
if exist {
}

迭代

for key, value := range dict{
}
在函数见传递映射

实际上并不会制造出这个映射的副本,而时对这个映射的所有引用都会察觉到修改

5 Go语言的类型系统

Go 使用中静态类型的编程语言, 这意味着,编译器需要在编译时知晓程序里每个值的类型。 如果提前知道类型信息,编译器就可以确保程序合理地使用值。 这有助于减少潜在的异常,并使编译器有机会对代码进行一些性能优化和提高执行效率。

5.1 用户定义类型

Go语言里声明用户定义的类型有两种方法, 最常用的是使用关键字struct

type user string {
    name       string
    email      string
    ext        int
    privileged bool
}
 var bill user

当声明结构时, 结构里每个字段都会用零值初始化。

lisa := user {
    name : "Lisa"
    email  "lisa@mail.com"
    ext: 123
    privileged = false
}

另一种生命用户定义类型的方式是基于一个已有的类型

type Duration int64

需要注意的是 新定义的类型 和原有类型之间不能相互赋值。

5.2 方法


type user struct {
    name  string
}

// notify使用接受值实现了一个方法
func (u user) notify(){
   fmt.Printf("name: <%s>\n", u.name)
}

关键字func 和函数名之间的参数被称为接收者, 将函数与接收者的类型绑在一起

如果一个函数有接收者,那么这个函数就称为方法

Go 语言中有两种类型的接收者 值接收和指针接收

如果使用值接收者声明方法,调用时回使用这个值的副本来执行。

也可以使用指针来调用使用值接收者声明的方法。
(*lisa).notify()

这里指针被解引用为值,这就符合了值接收者的要求。再强调一次,notify操作的是一个副本,只不过这次操作的是lisa指针指向的值的副本。

func (u *user) changeEmail(email string) {
}

当调用使用指针接收者声明的方法时, 这个方法会共享调用方法时的接收者所指向的值。

也可以使用值来调用指针接收者声明的方法。

5.3 类型的本质

在声明一个新类型之后,声明一个该类型的方法, 首先要回答一个问题: 这个类型的本质是什么?
如果给这个类型增加或者删除某个值,是要创建一个新值,还是要更改当前的值? 如果是要创建一个新值,该类型的方法就使用值接收者。如果要修改当前值,就是用指针接收者。
这个答案也会影响程序内部传递这个类型的值的方式:是按值传递还是按指针传递。

5.3.1 内置类型

5.3.2 引用类型

Go语言里面引用类型如下: 切片、映射、通道、接口和函数类型

一个值接收者正像预期的那样通过赋值来传递引用,从而不需要通过指针来共享引用类型的值这种传递方式也可以用用到函数或者方法的参数传递。

5.3.3 结构类型

接口类型可以用来描述一组数据值。

时使用值接收者还是指针接收者,不应该由该方法是否修改了接收者的值来决定,这个决策应该基于该类型的本质。 需要让类型值符合某个接口的时候,即便类型本质是非原始的,也可以原则使用值接收者声明方法。

5.4 接口

type notiferer interface {
 notify()
}

类型赋值接口后的内部布局:
接口值是一个两个字长度的数据结构,第一个字包含一个只想内部表的指针,这个内部表叫做itable, 包含了所存储的值的类型信息。 itable包含了已存储的值的类型信息和与这个值象关联的一组方法,第二个字指向一个所存储值的指针。

当一个指针赋值给接口之后,类型信息会存储一个指向保存的类型的指针。

用指针接收者来实现解口时,为什么user类型无法实现该接口,需要先了解方法集。
方法及定义了一组关联到给定类型的值或指针的方法。定义方法时用的接收者的类型决定了这个方式发关联到值还是指针。

Values             Method Receivers
-----------------------------------------
T                     t T
*T                    t T  and  t *T

T类型的值的方法集只包含值接收者声明的方法,而指向T类型的指针的方法集既包含值接收者声明的方法,也包含指针接收者声明的方法。

MethodsReceivers      Values
---------------------------------
t T                    T and *T
t *T                   *T

使用指针接受者来实现一个解口,那么只有指向那个类型的指针才能够实现对应的接口。 如果使用值接收者来实现一个接口, 那么那个类型的值和指针都能够实现对应的接口。

5.4.1 堕胎

5.5 嵌入类型

通过嵌入类型,内部类型的相关标识符会提升到外部类型。

由于内部类型的提升, 内部类型实现的接口会自动提升到外部类型。

外部类型实现了相同方法,内部类型就不会提升,但是还可以通过使用内部类型的值来调用内部类型实现的方法。

创建一个未公开的值,并将这个值返回给调用者
要让这个行为可行需要有两个理由

  1. 公开或者未公开的标识符,不是一个值
  2. 段变量声明操作符,有能力捕获引用的类型,并创建一个未公开的类型的变量。

永远不能现实创建一个未公开的类型的变量,不过短变量声明操作符可以这么做。

未公开的内部类型里面的公开字段依旧是公开的。

6 并发

Go语言里的并发是指能让某个函数独立于其他函数运行的能力。
goroutine Go会将其视为一个独立工作的单元。这个单元会被调度到可用的逻辑处理器上执行。
Go语言运行时的调度器是一个复杂的软件,能管理被创建的所有goroutine并为其分配执行时间。
这个调度器在操作系统之上,讲操作系统的线程与语言运行时的逻辑处理器绑定。

Go语言的并发同步模型来自一个叫做通信顺序进程(CommunicatingSequential Process)

6.1 并发与并行

操作系统会在物理处理器上调度线程来运行。而Go语言的运行时会在逻辑处理器上调度go routine来运行。 每个逻辑处理器都分别绑定到单个操作系统线程。

1. 创建一个 goroutine 并准备运行,这个goroutine会被放到调度器的全局运行队列
2. 调度器就会将这些队列中的goroutine分配给一个逻辑处理器并放到这个逻辑处理器的本地队列上

有时候 正在运行的goroutine 需要执行一个阻塞系统调用,如打开一个文件。当这类调用发生时,线程和goroutine 会从逻辑处理器上分离,该线程机会继续阻塞,等待系统调用的返回。
这个逻辑处理器就失去了用来运行的线程,调度器会创建一个新县城,并将其绑定到处理器上。

一旦被阻塞的系统调用执行完成并返回, 对应的goroutine会放回到本地运行队列。而之前的线程会保存好,以便之后可以继续使用。
如果一个goroutine需要做一个网络I/O调用,流程上会有些不一样, goroutine回合逻辑处理器分离,并移到集成了网络轮询器的运行时。 一旦该轮询其指示某个网络读或者写操作已经就绪,对应的gotoutine就会重新分配到逻辑处理器上来完成错做。

6.2 goroutine

go func() {
    defer wg.Done()
    
    for count := 0; count < 3; count++ {
        fmt.Print("%d",count)
    }
}()

创建一个goroutine

关键字 defer 会修改函数调用时机,在正在致性的函数返回时,才真正调用defer声明的函数

wg.Add(1)

go func(){
   defer wg.Done()
}()

wg.Wait()

runtime.GOMAXPROCS(runtime。NumCPU()) 通过它可以指定调度器可用的逻辑处理器的数量

6.3 竞争状态

go build -race
可以检测代码中的竞争状态

6.4 锁住共享资源

6.4.1 原子函数

import "sync/atomic"


atomic.AddInt64(&count, 1)
LoadInt64()
StoreInt64()

提供了一种安全读写的标志

6.4.2 互斥锁

mutex sync.Mutex

mutex.Lock(){
}
mutex.Unlock()

6.5 通道

go 通道 提供了同步交换数据的机制。
可以通过通道共享内置类型,明明类型,接口类型和引用类型的值或者指针。

unbuffered := make(chan int)
buffered := make(chan string, 10)

向通道发送值或者指针需要用到<-操作符

buffered := make(chan string, 10)
buffered <- "GoPher"
value := buffered

6.5.1 无缓冲通道

在接收前没有能力保存任何值的通道,这种类型的通道要求发送goroutine 和接收goroutine 同时准备好。
如果没有同时准备好则则会阻塞。


ball, ok := <- court
if ok! {
    fmt.Printf("Player ends")
}

6.5.2 有缓冲通道

只有在通道中没有要接收的值时,才会阻塞。

当关闭通道时, goroutine依旧可以从通道接收数据,但是不能再向通道写入数据。
从一个已经关闭且没有数据的通道里获取数据,总会立刻返回,并返回一个通道类型的零值

Select

func (r *Runner) gotInterrupt() bool {
    select {
    case <- r.interrupt:
        .........
    default:
        return false
    }
}
}

Q

goroutine实现原理?
select 多路复用?
init 函数的执行顺序
匿名函数的匿名函数可以访问最外层的变量吗?(闭包有层级限制吗?)
interface {} ??

沂狰码农
16 声望1 粉丝

CODE BETTER


« 上一篇
DOCKER