综合

变量声明和赋值

  • go自己推断类型:var d = true
  • 初始变量为零值: var e int
  • 初始化声明::=(右侧可以调用有返回值的函数,该变量的值则为返回值)

常量声明和赋值

  • 对应变量var, 用const关键词
  • 数值声明时若未指定类型,可以使用int64等给定一个类型,也能根据上下文需要给定一个类型

数组

创建:var a [5]intvar twoD [2][3]int

长度:len(a)

数组将以[..., ..., ..., ...]的形式fmt.Println

指针

  • 没有使用指针类型时,传入函数的是形参的拷贝,函数中的赋值不会改变实参的值

  • 使用指针类型时,引用的是该变量的内存地址,将会改变该变量的值,同时避免调用时产生拷贝

struct(结构类型)

struct是带类型的字段集合

声明:

type person struct {
    name string
    age  int
}

操作:

  • 创建元素:person{"Bob", 20},打印出来为{Bob 20}
  • 指定字段:person{name: "Alice", age: 30},打印格式同上,没有指定的字段为零值
  • 生成结构体指针&person{name: "Ann", age: 40}
  • 访问某字段:person.name
  • struct中的字段值是可变的

struct tag,用来辅助反射

方法

func (r *rect) area() int其中(r *rect)为指针类型的接收器,同理,还有值类型的接收器

调用:

//声明一个结构类型
type rect struct {
    width, height int
}
//定义方法,其接收者为结构类型的值或指针
func (r *rect) area() int {
    return r.width * r.height
}

func (r rect) perim() int {
    return 2*r.width + 2*r.height
}

func main() {
    //为该类型创建元素
    r := rect{width: 10, height: 5}
    //调用这里两个方法,go会自动处理方法调用时的值和指针转化
    fmt.Println("area: ", r.area())
    fmt.Println("perim:", r.perim())

    rp := &r
    fmt.Println("area: ", rp.area())
    fmt.Println("perim:", rp.perim())
}

错误处理

  • 使用一个独立、明确的返回值来传递错误信息

  • error是一个类型(内建的接口,有其方法Error。New方法是errors包里的)

  • 也可以自定义一个错误结构类型,和其方法

Go语言(golang)的错误(error)处理的推荐方案

定时器

time.NewTimer(Duration)创建定时器

<-time.NewTimer(d).C来阻塞实现定时

区别于time.Sleep的地方是可以用time.NewTimer.Stop()来取消

打点器

time.NewTicker(Duration)创建一个打点器

for循环遍历time.NewTicker(Duration).C的通道消息实现按时间间隔重复执行。

如果没用goroutine,将会一直执行;如果用goroutine而没有设置持续时间,则会在调用ticker.Stop()main函数执行完毕时停止。

工作池

实现方法:

  • 创建通道

  • (可以用for循环)创建出几个goroutine,处于阻塞状态,等待通道中的任务

  • (可以用for循环)将任务发送到通道

这些gorourine将以协程的方式执行任务。

注意需要用阻塞的方式等待任务完成。

速率限制

主要目的为控制资源利用,通过协程、通道、打点器实现。

  • limiter := time.Tick(d)limiter通道每隔d段时间接收一个值

  • 使用<-limiter阻塞

在速率限制中临时突破限制:

  • 创建一个time.Time类型的缓冲通道burstyLimiter,然后再传入需要数量的time.Time类型的值(time.Now()
  • 用一个goroutine实现time.Tick(d)通道向该通道传入burstyLimiter
  • 在之前需要<-limiter的地方使用<-burstLimiter

panic(异常处理)

panic 的一个基本用法就是在一个函数返回了错误值但是我们并不知道(或 者不想)处理时终止运行。

panic 是一个函数,接收一个interface{}类型(任何类型)的参数,然后在defer之后终止程序,抛出异常。

panic 之后逻辑走到defer时,可以调用recover函数捕捉panic,但程序仍然会在defer之后返回

Go语言异常处理defer\panic\recover

defer

defer 被用来确保一个函数调用在程序执行结束前执行。同样用来执行一些清理工作。

被defer调用的函数会在main结束时执行。

组合函数

函数接收一个匿名函数或具名函数作为参数,例:

func Any(vs []string, f func(string) bool) bool {
    for _, v := range vs {
        if f(v) {
            return true
        }
    }
    return false
}

匿名函数的实现可以在调用时写出来,例:

fmt.Println(Any(strs, func(v string) bool {
        return strings.HasPrefix(v, "p")
    }))

正则表达式(备查)

这个测试一个字符串是否符合一个表达式。 match, _ := regexp.MatchString("p([a-z]+)ch", "peach") fmt.Println(match)
上面我们是直接使用字符串,但是对于一些其他的正则任 务,你需要 Compile 一个优化的 Regexp 结构体。 r, _ := regexp.Compile("p([a-z]+)ch")
这个结构体有很多方法。这里是类似我们前面看到的一个 匹配测试。 fmt.Println(r.MatchString("peach"))
这是查找匹配字符串的。 fmt.Println(r.FindString("peach punch"))
这个也是查找第一次匹配的字符串的,但是返回的匹配开 始和结束位置索引,而不是匹配的内容。 fmt.Println(r.FindStringIndex("peach punch"))
Submatch 返回完全匹配和局部匹配的字符串。例如, 这里会返回 p([a-z]+)ch 和 `([a-z]+) 的信息。 fmt.Println(r.FindStringSubmatch("peach punch"))
类似的,这个会返回完全匹配和局部匹配的索引位置。 fmt.Println(r.FindStringSubmatchIndex("peach punch"))
All 的这个函数返回所有的匹配项,而不仅仅是首 次匹配项。例如查找匹配表达式的所有项。 fmt.Println(r.FindAllString("peach punch pinch", -1))
All 同样可以对应到上面的所有函数。 fmt.Println(r.FindAllStringSubmatchIndex( "peach punch pinch", -1))
这个函数提供一个正整数来限制匹配次数。 fmt.Println(r.FindAllString("peach punch pinch", 2))
上面的例子中,我们使用了字符串作为参数,并使用了 如 MatchString 这样的方法。我们也可以提供 []byte 参数并将 String 从函数命中去掉。 fmt.Println(r.Match([]byte("peach")))
创建正则表达式常量时,可以使用 Compile 的变体MustCompile 。因为 Compile 返回两个值,不能用于常量。 r = regexp.MustCompile("p([a-z]+)ch") fmt.Println(r)
regexp 包也可以用来替换部分字符串为其他值。 fmt.Println(r.ReplaceAllString("a peach", "<fruit>"))
Func 变量允许传递匹配内容到一个给定的函数中, in := []byte("a peach") out := r.ReplaceAllFunc(in, bytes.ToUpper) fmt.Println(string(out))

输出如下:

true
true
peach
[0 5]
[peach ea]
[0 5 1 3]
[peach punch pinch]
[[0 5 1 3] [6 11 7 9] [12 17 13 15]]
[peach punch]
true
p([a-z]+)ch
a <fruit>
a PEACH

同步状态

原子计数

使用 sync/atomic 包管理状态

在goroutine中使用 atomic.Addxxxatomic.Loadxxx atomic.Storexxx 来保证同一时刻只有一个goroutine来操作变量

**Tips: **runtime.Gosched 为释放该goroutine,使其回到队列中。

互斥锁

mutex = &sync.Mutex{}

在goroutine中使用 mutex.Lock()mutex.Unlock 来保证同一时刻只有一个goroutine来操作变量

Go状态协程

通过goroutine和通道的同步特性来让共享的state跨多个goroutine同步访问。

思路:

  • 共享的state由一个单独的goroutine拥有
  • 创建两个无缓冲通道分别实现读和写请求与操作之间的通信
  • 读和写分别创建若干个goroutine,每个goroutine将请求(struct)发送到对应通道
  • 拥有state的goroutine单线程处理请求,将结果发送到struct中的通道
  • 本次通信的goroutine会用struct中的通道阻塞,直到接收到结果值

请求的发送和单独的goroutine的接收是同步的,且同一时间只有这两个goroutine在利用通道通信

接口

命名了的方法签名的集合

是一组方法,也是一种类型

//创建一个接口
type geometry interface {
    area() float64
    perim() float64
}

//创建了两个struct
type rect struct {
    width, height float64
}
type circle struct {
    radius float64
}

//rect的area()和perim()两个方法
func (r rect) area() float64 {
    return r.width * r.height
}
func (r rect) perim() float64 {
    return 2*r.width + 2*r.height
}

//circle的area()和perim()两个方法
func (c circle) area() float64 {
    return math.Pi * c.radius * c.radius
}
func (c circle) perim() float64 {
    return 2 * math.Pi * c.radius
}

//measure接受一个geometry的接口类型
func measure(g geometry) {
    fmt.Println(g)
    fmt.Println(g.area())
    fmt.Println(g.perim())
}

//rect和circle都实现了geometry接口
func main() {
    r := rect{width: 3, height: 4}
    c := circle{radius: 5}
    measure(r)
    measure(c)
}

五个关键点

理解 Go interface 的 5 个关键点

interface是一种类型

  • 不带任何方法的interface叫empty interface

  • 如果一个类型实现了一个interface中所有方法,我们说类型实现了该Interface

interface变量存储的是实现者的值

  • 如果有多种类型实现了某个interface,这些类型的值都可以直接使用interface的变量存储
  • 在使用 interface 时不需要显式在 struct 上声明要实现哪个 interface ,只需要实现对应 interface 中的方法即可,go 会自动进行 interface 的检查,并在运行时执行从其他类型到 interface 的自动转换
  • 实现了多个 interface时,go会在使用对应 interface 时实现自动转换

如何判断 interface 变量存储的是哪种类型

value, ok := em.(T):em 是 interface 类型的变量,T代表要断言的类型,value 是 interface 变量存储的值,ok 是 bool 类型表示是否为该断言的类型 T

可以用if判断,也可以用switch(这里的t是要断言的类型):

switch t := i.(type) {
case *S:
    fmt.Println("i store *S", t)
case *R:
    fmt.Println("i store *R", t)
}

空的interface

  • 所有类型都实现了空的interface:如果定义一个函数参数是 interface{} 类型,这个函数应该可以接受任何类型作为它的参数
  • go 不会对 类型是interface{} 的 slice 进行转换,所以《Go的类型》中的一个不常用的例子对[]string进行了转换

原因:interface{} 会占用两个字长的存储空间,一个是自身的 methods 数据,一个是指向其存储值的指针,也就是 interface 变量存储的值,因而 slice []interface{} 其长度是固定的N*2,但是 []T 的长度是N*sizeof(T),两种 slice 实际存储值的大小是有区别的(文中只介绍两种 slice 的不同,至于为什么不能转换猜测可能是 runtime 转换代价比较大)。

interface 的实现者的 receiver 如何选择

  • interface 定义时并没有严格规定实现者的方法 receiver 是个 value receiver 还是 pointer receiver,但在调用时如果方法的receiver是pointer,而传入的是一份拷贝,也就没有实现该interface;相反,则可以正确执行

原因:有了指针总是能得到指针指向的值是什么,如果是 value 调用,go 将无从得知 value 的原始值是什么,因为 value 是份拷贝。go 会把指针进行隐式转换得到 value,但反过来则不行

  • 对于 receiver 是 value 的 method,任何在 method 内部对 value 做出的改变都不影响调用者看到的 value,这就是按值传递

切片

切片和数组是不同的类型,但打印结果类似

append()方法

B = append(B, i * i)

第一个参数是原切片,第二个参数是要增加的值,中间是“=”而不是“:=”!

切片排序

【Go语言】基本类型排序和 slice 排序
根据类型使用如下方法:

sort.Ints(intList)
sort.Float64s(float8List)
sort.Strings(stringList)

直接对原切片排序,没有返回值。

sort.IntsAreSorted()检查是否已经排序,返回一个bool值。

自定义排序

创建对应类型,让该类型实现Len()Less()Swap() 方法以实现sort.Interface

需要自定义的地方一般在Less()

调用sort.Sort(ByLength)进行排序,需要使用如ByLength(fruits)对切片转型

切片的最后一个索引

感觉不是很优雅。。。

B[len(B) - 1]

切片去重

没有现成的函数可用,找到了个性能好的函数:

func removeDuplicateElement(addrs []string) []string {
    result := make([]string, 0, len(addrs))
    temp := map[string]struct{}{}

    for _, item := range addrs {
        if _, ok := temp[item]; !ok {
            temp[item] = struct{}{}
            result = append(result, item)
        }
    }

    return result
}

判断某元素是否在切片内

只能用遍历的方式

copy()

copy(c, s):将s赋值给c,c需要先声明c := make([]string len(s)

字符串

几乎都是在strings这个包里,注意新字符串要用“:=”声明赋值

字符串大小写转换

strings.ToUpper(str)
strings.ToLower(str)

统计字符在字符串中出现的次数

strings.Count(str, s)

遍历字符串

go字符串的遍历输出

range 在字符串中迭代 unicode 码点(code point)。 第一个返回值是字符的起始字节位置,然后第二个是字符本身。

  • 以字节数组的方式遍历:for i := 0; i < len(str); i++

  • 以Unicode字符遍历:for index, val := range str

使用string()将结果转化为字符串

分割字符串

strings.Split()

返回字符串数组,跟迭代不同,索引出来的值类型是字符串

替换字符串中字符

strings.Replace()

返回的是字符串

字符串拼接

同python,用“+”拼接

查找子字符串

strings.Index(str, substr)

跟Python一样,如果包含子字符串返回开始的索引值,否则返回-1。

整数转为字符串

strconv.Itoa()

字符串函数(strings包中的常用函数)

p("Contains: ", s.Contains("test", "es")) //是否包含 true
p("Count: ", s.Count("test", "t")) //字符串出现字符的次数 2
p("HasPrefix: ", s.HasPrefix("test", "te")) //判断字符串首部 true
p("HasSuffix: ", s.HasSuffix("test", "st")) //判断字符串结尾 true
p("Index: ", s.Index("test", "e")) //查询字符串位置 1
p("Join: ", s.Join([]string{"a", "b"}, "-"))//字符串数组 连接 a-b
p("Repeat: ", s.Repeat("a", 5)) //重复一个字符串 aaaaa
p("Replace: ", s.Replace("foo", "o", "0", -1)) //字符串替换 指定起始位置为小于0,则全部替换 f00
p("Replace: ", s.Replace("foo", "o", "0", 1)) //字符串替换 指定起始位置1 f0o
p("Split: ", s.Split("a-b-c-d-e", "-")) //字符串切割 [a b c d e]
p("ToLower: ", s.ToLower("TEST")) //字符串 小写转换 test
p("ToUpper: ", s.ToUpper("test")) //字符串 大写转换 TEST
//其它非strings包
p("Len: ", len("hello")) //字符串长度
p("Char:", "hello"[1]) //标取字符串中的字符,类型为byte

字符串格式化(备查)

Go 为常规 Go 值的格式化设计提供了多种打印方式。例 如,这里打印了 point 结构体的一个实例。 p := point{1, 2} fmt.Printf("%v\n", p)
如果值是一个结构体,%+v 的格式化输出内容将包括 结构体的字段名。 fmt.Printf("%+v\n", p)
%#v 形式则输出这个值的 Go 语法表示。例如,值的 运行源代码片段。 fmt.Printf("%#v\n", p)
需要打印值的类型,使用 %T fmt.Printf("%T\n", p)
格式化布尔值是简单的。 fmt.Printf("%t\n", true)
格式化整型数有多种方式,使用 %d进行标准的十进 制格式化。 fmt.Printf("%d\n", 123)
这个输出二进制表示形式。 fmt.Printf("%b\n", 14)
这个输出给定整数的对应字符。 fmt.Printf("%c\n", 33)
%x 提供十六进制编码。 fmt.Printf("%x\n", 456)
对于浮点型同样有很多的格式化选项。使用 %f 进 行最基本的十进制格式化。 fmt.Printf("%f\n", 78.9)
%e%E 将浮点型格式化为(稍微有一点不 同的)科学记数法表示形式。 fmt.Printf("%e\n", 123400000.0) fmt.Printf("%E\n", 123400000.0)
使用 %s 进行基本的字符串输出。 fmt.Printf("%s\n", "\"string\"")
像 Go 源代码中那样带有双引号的输出,使用 %q fmt.Printf("%q\n", "\"string\"")
和上面的整型数一样,%x 输出使用 base-16 编码的字 符串,每个字节使用 2 个字符表示。 fmt.Printf("%x\n", "hex this")
要输出一个指针的值,使用 %p fmt.Printf("%p\n", &p)
当输出数字的时候,你将经常想要控制输出结果的宽度和 精度,可以使用在 % 后面使用数字来控制输出宽度。 默认结果使用右对齐并且通过空格来填充空白部分。 ` fmt.Printf(“
你也可以指定浮点型的输出宽度,同时也可以通过 宽度. 精度 的语法来指定输出的精度。 ` fmt.Printf(“
要左对齐,使用 - 标志。 ` fmt.Printf(“
你也许也想控制字符串输出时的宽度,特别是要确保他们在 类表格输出时的对齐。这是基本的右对齐宽度表示。 ` fmt.Printf(“
要左对齐,和数字一样,使用 - 标志。 ` fmt.Printf(“
到目前为止,我们已经看过 Printf了,它通过 os.Stdout输出格式化的字符串。Sprintf 则格式化并返回一个字 符串而不带任何输出。 s := fmt.Sprintf("a %s", "string") fmt.Println(s)
你可以使用 Fprintf 来格式化并输出到 io.Writers 而不是 os.Stdout fmt.Fprintf(os.Stderr, "an %s\n", "error")

输出如下:

{1 2}
{x:1 y:2}
main.point{x:1, y:2}
main.point
true
123
1110
!
1c8
78.900000
1.234000e+08
1.234000E+08
"string"
"\"string\""
6865782074686973
0x42135100
|    12|   345|
|  1.20|  3.45|
|1.20  |3.45  |
|   foo|     b|
|foo   |b     |
a string
an error

循环

for 是go的唯一循环

不带条件的循环

会一直循环下去,直到breakreturn

伪while循环

先学的python,想在go中实现这种循环,但没有,可以在for循环中用if代替。

条件判断

逻辑运算符

  • &&:逻辑AND运算符
  • ||:逻辑OR运算符

多条件判断

case中可以用”,”分隔多个条件;switch中没有表达式的形式可作为if...else..的备用;

如果switch的表达式是一个类型,case中将比较类型而不是值

switch express {
    case express:
        ...
    case express1, express2:
        ...
    default:
        ...
}

在if中声明变量

可以用于该判断所有的分支

select

select语句是随机选择一个可运行的case运行,运行一个case后则停止

映射

创建映射

data := make(map[key-type]value-type)

给键赋值

data[key] = value, 如果该键的值已经存在,则会被新值覆盖

打印

输出所有键值对:map[k1:7 k2:13]

移除

delete(m, "k2")第一个参数是映射,第二个是要移除的键

取值

value, bool := m["k2"]第一个返回值是“值”,第二个是bool类型的值,指示键是否存在

遍历

for k, v := range kvsfor k := range kvs则只遍历键

函数

参数和返回

多个同类型参数可仅声明一次plusPlus(a, b, c int)

声明返回的类型,如果没有返回可以不声明

多返回值

func vals() (int, int)

该特性经常被用来返回结果和错误信息

多参数

func sum(nums ...int)

调用:

  • sum(1, 2, 3)
  • 如果参数是切片(nums):sum(nums...)

闭包

func() int是在func intSeq()内定义的匿名函数,隐藏变量i

也可以说:func intSeq()返回func() int,同时包含了i

func intSeq() func() int {
    i := 0
    return func() int {
        i++
        return i
    }
}

递归

也是调用自身

func fact(n int) int {
    if n == 0 {
        return 1
    }
    return n * fact(n-1)
}

goroutine

使用go func创建gorotine

分配逻辑处理器

  • 分配一个逻辑处理器给调度器使用runtime.GOMAXPROCS(1),其中GOMAXPROCS函数允许程序更改调度器可以使用的逻辑处理器的数量
  • 给每个可用的核心分配一个逻辑处理器runtime.GOMAXPROCS(runtime.NumCPU()),其中NumCPU函数返回可以使用的物理处理器数量

用wg来等待程序完成

var wg sync.WaitGroup
wg.Add(2)

goroutine完成时记录

defer wg.Done()

所有goroutin完成时退出

wg.Wait()

通道

通道(Channels)是连接多个 Go 协程的管道。可以从一个 Go 协程将值发送到通道,然后在别的 Go 协程中接收

创建

  • 无缓冲make(chan type)
  • 有缓冲make(chan string, 2)

发送和接收

默认发送和接收操作是阻塞的,直到发送方和接收方都准备完毕

  • 发送:channel <-
  • 接收: <- channel

通道同步

可以在函数中向缓冲通道传入一个值,然后在main函数结尾传出;

也可以用sync.WaitGroup方法

通道方向

使用通道作为函数的参数时,可以指定该通道是用来发送还是接收的,用来提升程序类型安全

如:func pong(pings <-chan string, pongs chan<- string)

通道选择器

goroutine + channel + select

goroutine中向无缓冲通道发送值,select的每个case等待每个通道传入,即同时等待多个通道。

如果要将这几个通道的值依次打印,对select使用for循环

超时处理

超时处理使用通道选择器。

select {
    case res := <-c2:
        fmt.Println(res)
    case <- time.After(time.Second * 3):
        fmt.Println("timeout 2")
    }

time.After()表示time.Duration长的时候后返回一条time.Time类型的通道消息。

select语句阻塞等待最先返回数据的channel,当先接收到time.After的通道数据时,select则会停止阻塞并执行该case的代码。

非阻塞通道操作

我的理解为如果通道中没有可发送的值,select模块就会一直阻塞,通过default,通道中有值时则处理,没有时则跳过,不会一直阻塞。

关闭通道

close(chan)关闭一个通道,意味着不能再向这个通道发送值了。

如果不关闭会继续阻塞执行,等待接收,导致报错:

fatal error: all goroutines are asleep - deadlock!

从通道接收的第一个为值,第二个为bool值:

go func() {
        for {
            j, more := <-jobs
            if more {
                fmt.Println("received job", j)
            } else {
                fmt.Println("received all jobs")
                done <- true
                return
            }
        }
    }()

通过对more使用if条件句判断是否完成,然后传递给done通道

如果 jobs 已经关闭了,并且通道中所有的值都已经接收 完毕,那么 more 的值将是 false

通道遍历

通道中的值是可以迭代的,但可能需要将通道先关闭,不然会在循环中继续阻塞执行,等待接收。

标准库

flag

标准库—命令行参数解析FLAG

区分几个概念:

  • 命令行参数(或参数):是指运行程序提供的参数
  • 已定义命令行参数:是指程序中通过flag.Xxx等这种形式定义了的参数
  • 非flag(non-flag)命令行参数(或保留的命令行参数):后文解释

定义flags:

  • var ip = flag.Int("flagname", 1234, "help message for flagname")
  • var flagvar int
    flag.IntVar(&flagvar, "flagname", 1234, "help message for flagname")
  • flag.Var(&flagVal, "name", "help message for flagname")

net/http

http.ListenAndServe 建立监听

http.HandleFunc 进行处理,第二个参数为处理函数

request.FormValue Get请求中的url参数,与之相似的还有request.URL.Query().Get

Comments