notes
  • notes
  • codes
    • 安卓脚本
    • redis入门指南
    • js 原型链
    • 如何发布npm
    • go字符串
    • redis
    • this指向
    • go1.13
    • go by example
    • hook
    • go指南 - 官网
    • git基本操作
  • vim
  • training
    • 9月19日 ubc讲座
    • zby
      • DAY 3 :硬拉 | ZBY
      • DAY 2:卧推 | ZBY
      • DAY 1:深蹲 | ZBY
      • DAY 4 :计划 | ZBY
    • 汉唐
    • 周旋康复课
  • book notes
    • 思考致富
    • 邓普顿教你逆向投资
    • 阮琦
    • 魔鬼经济学
    • 网络是怎样连接的
    • 好奇心
    • 魔鬼约会学5.0
    • 股票作手回忆录
    • 漫步华尔街
    • 码农翻身
    • 十分钟速成课 - 哲学
    • 魔鬼答疑
    • 大话数据结构
    • 魔鬼约会学笔记
    • 算法图解
  • 狼人杀
  • 图书馆
  • typora
  • imovie
Powered by GitBook
On this page
  • 基础
  • 基本类型
  • 类型转换
  • 流程控制
  • for
  • if
  • switch
  • defer
  • 更多类型
  • 指针
  • 结构体
  • 数组
  • 切片
  • range
  • 练习:切片
  • 映射map
  • 练习:映射
  • 函数
  • 闭包
  • 练习:斐波纳契闭包
  • 方法和接口
  • 方法
  • 接口
  • 类型断言
  • 类型选择
  • stringer
  • 练习:Stringer
  • 错误
  • 练习:错误
  • reader
  • 练习:reader
  • 练习:rot13Reader
  • 图像
  • 练习:图像
  • 并发
  • go routine
  • 信道
  • 带缓冲的信道
  • range 和 close
  • select
  • 默认选择
  • 练习:等价二叉查找树
  • sync.Mutex
  • web爬虫

Was this helpful?

  1. codes

go指南 - 官网

PrevioushookNextgit基本操作

Last updated 5 years ago

Was this helpful?

,应该是最好的入门了吧

基础

每个 Go 程序都是由包构成的。

程序从 main 包开始运行。

一般包名与导入路径的最后一个元素一致。例如,"math/rand" 包中的源码均以 package rand 语句开始。

import (
    "fmt"
    "math"
)
// 上面这种导入方式更好
import "fmt"
import "math"

如果一个名字以大写字母开头,那么它就是已导出的

如,math.pi就是未导出的,就没有定义,不能访问和调用。

math.Pi就导出了。导入一个包,只能引用已经导出的名字。

类型在变量名之后。

c的类型是定义在前面的。如果函数的参数是函数,或者返回值是函数,那么就会难以理解

int (*fp)(int (*ff)(int x, int y), int b)
int (*(*fp)(int (*)(int, int), int))(int, int)

go从后置类型的定义进化而来

x: int
p: pointer to int
a: array[3] of int

变成
x int
p *int
a [3]int

用go写就是

f func(func(int,int) int, int) int
f func(func(int,int) int, int) func(int, int) int

但是指针的*是前置的

var p *int
x = *p

// 而不是
var p *int
x = p*

因为可能被理解为乘法

变量初始化如果赋值了,那么可以省略类型。具体被赋值成啥,看精度。如int,float64,complex128等等

var c, python, java = true, false, "no!"

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

:= 结构不能在函数外使用,只能用var

基本类型

bool

string

int int8 int16 int32 int64 uint uint8 uint16 uint32 uint64 uintptr

byte // uint8 的别名

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

float32 float64

complex64 complex128

变量声明也可以“分组”成一个语法块。

var (
    ToBe   bool       = false
    MaxInt uint64     = 1<<64 - 1
    z      complex128 = cmplx.Sqrt(-5 + 12i)
)

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

0,false,“”

类型转换

i := 42      // int
f := float64(i)
u := uint(f)

流程控制

for

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

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

  • 初始化语句:在第一次迭代前执行。仅在for作用域中可见。

  • 条件表达式:在每次迭代前求值

  • 后置语句:在每次迭代的结尾执行

可以省略分号变成while,甚至省略条件变成死循环。

if

不需要小括号,可以在前面加一个分号来执行简单语句。作用域就在if的范围内。

if v := math.Pow(x, n); v < lim {
  return v
}

switch

相当于一大串的if-else。只运行选定的case,因为自动提供了break。

switch os := runtime.GOOS; os {
case "darwin":
  fmt.Println("OS X.")
case "linux":
  fmt.Println("Linux.")
default:
  // freebsd, openbsd,
  // plan9, windows...
  fmt.Printf("%s.\n", os)
}

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

不加条件的switch能看起来更清楚。

defer

defer 语句会将函数推迟到外层函数返回之后执行。【函数结束前,执行这个defer内容】

func main() {
    defer fmt.Println("world")  // hello 返回后才会执行 world

    fmt.Println("hello")
}

defer很多,就会压入堆栈中。后进先出

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

    for i := 0; i < 10; i++ {
        defer fmt.Println(i)      // 0-9压入,9-0推出
    }

    fmt.Println("done")
}

更多类型

指针

指针保存了值的内存地址。go没有指针运算。

  • & 操作符会生成一个指向其操作数的指针。 &i获得i的内存地址,如0xc000090000

  • * 操作符表示通过指针指向变量的值。 *后面要加上地址,这能获取这个地址的值,如*(&i)=i,都是i的值。

i := 42, 2701

p := &i         // 指向 i,此时p里面保存的是一个内存地址,如0xc000090000
                                // 当然p也是变量,也有存p的地址。如&p=0xc00000e010
fmt.Println(*p) // 通过指针p读取 i 的值
*p = 21         // 通过指针p设置 i 的值 【相当于i=21】
fmt.Println(i)  // 查看 i 的值【相当于*p】

p := &i中的p是i的地址,*p是顺着地址找到了i的值

结构体

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

【感觉是数组的升级版,数组只能全部是一个类型,而结构体可以不同东西有不同类型】

type Vertex struct {
    X int
    Y int
}
// 写成这样也可以
type Vertex struct {
    X, Y int
}

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


func main() {
  fmt.Println(Vertex{1, 2}) // {1 2}

  v := Vertex{1, 2}
    v.X = 4                        // 用点来访问
    fmt.Println(v.X)
}

数组

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

var a [10]int,表示将变量 a 声明为拥有 10 个整数的数组。

数组不能改变大小。长度是其类型的一部分

【感觉channel就是数组做出来的】

切片

引用一部分数组。它并不存储任何数据,它只是引用了底层数组中的一段。

更改切片的元素会修改其底层数组中对应的元素。【直接通过指针改变了对应的值】

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

a[1:4]包含数组 a 中下标从 1 到 3 的元素。和python一样,不写就默认是上下界。

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

  // 也可以让编译器自己去统计
  names := [...]string{   // ...代替数字即可
        "John",
        "Paul",
        "George",
        "Ringo",
    }

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

    b[0] = "XXX"
    fmt.Println(a, b)  // [John XXX] [XXX George]
    fmt.Println(names)

  // a[:] == a[0:4] == a[:4] == a[0:]
}

除了用[1:4]来切片,还可以用无长度的数组来创建

// 创建数组
a := [6]int{2, 3, 5, 7, 11, 13}

// 创建上面一样的数组,然后用切片来引用
q := []int{2, 3, 5, 7, 11, 13}
// 相当于 q:= a[:]


s := []struct {   // 类型可以是结构体
  i int
  b bool
}{
  {2, true},
  {3, false},
  {5, true},
  {7, true},
  {11, false},
  {13, true},
}
fmt.Println(s) // [{2 true} {3 false} {5 true} {7 true} {11 false} {13 true}]

切片的长度和容量

切片s,长度是len(s),容量是cap(s)。

左指针改容量,右指针不改容量。因为切片的index必须是一个非负数,所以取不到-1,所以左指针就规定了容量。

容量,也就是可能取到的长度。

func main() {
    s := []int{2, 3, 5, 7, 11, 13}
    printSlice(s)                // len=6 cap=6 [2 3 5 7 11 13]

    // 截取切片使其长度为 0
    s = s[:0]
    printSlice(s)                // len=0 cap=6 []

    // 拓展其长度
    s = s[:4]
    printSlice(s)                // len=4 cap=6 [2 3 5 7]

    // 舍弃前两个值
    s = s[2:]
    printSlice(s)                // len=2 cap=4 [5 7]

    s = s[:0]
    printSlice(s)                // len=0 cap=4 []
}

func printSlice(s []int) {
    fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}

长度是切片的内容长度,容量是从左边指针的切片开始算起的。

切片的零值

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

make创建切片

a := make([]int, 5)   // 感觉只是用来创建全0的切片
// s == []int{0, 0, 0, 0, 0}
printSlice("a", a)        // a len=5 cap=5 [0 0 0 0 0]

b := make([]int, 0, 5)  // 长度0,容量5
printSlice("b", b)            // b len=0 cap=5 []

c := b[:2]                        // 长度变成2
printSlice("c", c)        // c len=2 cap=5 [0 0]

d := c[2:5]                        // 容量-2,变成了3,长度是3
printSlice("d", d)        // d len=3 cap=3 [0 0 0]

i := b[:cap(b)]                // 可以直接算出b的最大容量
printSlice("i", i)        // i len=5 cap=5 [0 0 0 0 0]

k := b[:]                            // 这样会继承b的长度,也就是0
printSlice("k", k)        // k len=0 cap=5 []

e := make([]int, 0, 3)
f := e[:5]
printSlice("f", f)  // panic: runtime error: slice bounds out of range
// 容量只有3,取不到5

切片追加元素append

func append(slice []Type, elems ...Type) []Type

append 内建函数将元素追加到切片的末尾。 若它有足够的容量,其目标就会重新切片以容纳新的元素。否则,就会分配一个新的基本数组。 append 返回更新后的切片。因此必须存储追加后的结果,通常为包含该切片自身的变量:

slice = append(slice, elem1, elem2)
slice = append(slice, anotherSlice...)

作为一种特殊的情况,将字符追加到字节数组之后是合法的,就像这样:

slice = append([]byte("hello "), "world"...)
var s []int
printSlice(s)                // len=0 cap=0 []

// 添加一个空切片
s = append(s, 0)        // 原数组容量为0,不够添加,因此新建一个更大的数组分配给这个切片
printSlice(s)                // len=1 cap=2 [0]   // 新数组的容量为2,是添加的一个数的两倍

// 这个切片会按需增长
s = append(s, 1)        // 容量够,就直接加。【估计是把原数组的[0 0]变成了[0 1]】
printSlice(s)                 // len=2 cap=2 [0 1]

// 可以一次性添加多个元素
s = append(s, 2, 3, 4)  // 添加了3个,容量不够再扩容。
printSlice(s)              // len=5 cap=8 [0 1 2 3 4]
// 新数组变成了8,因为增加了3个,翻倍要6个位置,原来有俩,6+2=8

Go的数组是值语义。一个数组变量表示整个数组,它不是指向第一个元素的指针(不像 C 语言的数组)。

(为了避免复制数组,你可以传递一个指向数组的指针,但是数组指针并不是数组。)

可以将数组看作一个特殊的struct,结构的字段名对应数组的索引,同时成员的数目固定。

切片的内幕

一个切片是一个数组片段的描述。它包含了指向数组的指针,片段的长度, 和容量(片段的最大长度)。

make([]byte, 5)相当于

s = s[2:4]

切片操作并不复制切片指向的元素。它创建一个新的切片并复用原来切片的底层数组。【改变头的指针地址】

通过一个新切片修改元素会影响到原始切片的对应元素。

切片的生长

要增加切片的容量必须创建一个新的、更大容量的切片,然后将原有切片的内容复制到新的切片。

可能的陷阱

切片操作并不会复制底层的数组。整个数组将被保存在内存中,直到它不再被引用。

这样的话,有时候可能会因为一个小的内存引用导致保存所有的数据。

【简言之要用切片来操作,而非操作整个数组。感觉切片的出现就是为了避免整个数组的操作】

range

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

每次迭代都会返回两个值。第一个值为当前元素的下标,第二个值为该下标所对应元素的一份副本。

可以用_来忽略

var pow = []int{1, 2, 4, 8, 16, 32, 64, 128}

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

  // 忽略
  for _, value := range pow
  for i, _ := range pow
    // 忽略第二个,可以忽略_
  for i := range pow
}

练习:切片

实现 Pic。它应当返回一个长度为 dy 的切片,其中每个元素是一个长度为 dx,元素类型为 uint8 的切片。当你运行此程序时,它会将每个整数解释为灰度值(好吧,其实是蓝度值)并显示它所对应的图像。

图像的选择由你来定。几个有趣的函数包括 (x+y)/2, x*y, x^y, x*log(y) 和 x%(y+1)。

(提示:需要使用循环来分配 [][]uint8 中的每个 []uint8;请使用 uint8(intValue) 在类型之间转换;你可能会用到 math 包中的函数。)

package main

import "golang.org/x/tour/pic"

func Pic(dx, dy int) [][]uint8 {
    result := make([][]uint8, dy)
    for x := range result {
        item := make([]uint8, dx)
        for y := range item {
            item[y] = uint8((x + y) / 2)
        }
        result[x] = item
    }
    return result
}

func main() {
    pic.Show(Pic)
}

感觉x^y最好看

映射map

将键映射到值。零值为nil,没有键也不能添加键。

type Vertex struct {    // 设定Vertex结构体
    Lat, Long float64
}

func main() {
    m := make(map[string]Vertex)  // 新建一个map,key是string,value是Vertex
    m["Bell Labs"] = Vertex{         // 给map 的key赋值
        40.68433, -74.39967,
    }
    fmt.Println(m["Bell Labs"])        // {40.68433 -74.39967}
}

// 或者也可以直接定义:
var m = map[string]Vertex{  // [string]Vertex是为了表明key和value类型吧
    "Bell Labs": Vertex{
        40.68433, -74.39967,
    },
    "Google": Vertex{
        37.42202, -122.08408,
    },
}

// 也可以这么定义
var m = map[string]Vertex{
    "Bell Labs": {40.68433, -74.39967},
    "Google":    {37.42202, -122.08408},
}

// 删除
delete(m, key)
// 检验值是否存在
elem, ok := m[key]
// 若 key 在 m 中,ok 为 true ;否则,ok 为 false, elem也是零值

练习:映射

实现 WordCount。它应当返回一个映射,其中包含字符串 s 中每个“单词”的个数。函数 wc.Test 会对此函数执行一系列测试用例,并输出成功还是失败。

package main

import (
//    "fmt"
    "runtime"
    "strings"
)

import (
    "golang.org/x/tour/wc"
)

func WordCount(s string) map[string]int {
    str := strings.Fields(s)
    m := make(map[string]int)
    for _, elem := range str {
        if _, ok := m[elem]; ok {
            m[elem] += 1
        } else {
            m[elem] = 1
        }
        runtime.Gosched()
    }
    return m
}

func main() {
    result := WordCount("I am learning Go!")
    fmt.Println(result)
    wc.Test(WordCount)
}

函数

函数也是值,可以像值一样被传递。

闭包

func adder() func(int) int {
    sum := 0
    return func(x int) int {
        sum += x
        return sum
    }
}

练习:斐波纳契闭包

package main

import "fmt"

// 返回一个“返回int的函数”
func fibonacci() func() int {
    first := 0
    second := 1
    return func() int {
        result := first
        first, second = second, (first + second)  // 这种赋值方式可以保留second的值
        return result
    }
}

func main() {
    f := fibonacci()
    for i := 0; i < 10; i++ {
        fmt.Println(f())
    }
}

方法和接口

方法

就是给结构体加一个方法。

type Vertex struct {    // 结构体
    X, Y float64
}

func (v Vertex) Abs() float64 {     // 在结构体下面,加一个Abs()方法,返回float64
    return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func main() {
    v := Vertex{3, 4}
    fmt.Println(v.Abs())   // 调用结构体的方法,结果是5
}

// 如果是正常的函数,是这么写的
func Abs(v Vertex) float64 {
    return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

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

// 非结构体,也可以有方法
// 感觉就是在某种type下面增加一种方法。
// 但类型定义和方法声明必须在同一包内。且不能为内建类型声明方法。
type MyFloat float64

func (f MyFloat) Abs() float64 {
    if f < 0 {
        return float64(-f)
    }
    return float64(f)
}

func main() {
    f := MyFloat(-math.Sqrt2)
    fmt.Println(f.Abs())
}

这些都是在值下面增加方法。因此处理的都是输入的值的副本

如果用指针,就是在直接操作值本身。

func (v Vertex) Scale(f float64) { // 这是在操作值的副本
    v.X = v.X * f
    v.Y = v.Y * f
}
// 函数结束后,副本就会被销毁,v的两个值都不会变

func (v *Vertex) Scale(f float64) {  // 这是在操作指针
    v.X = v.X * f
    v.Y = v.Y * f
}
// 直接修改了指向的内容,函数结束后v的两个值都乘f了
func main() {
    v := Vertex{3, 4}
    v.Scale(10)                    // 使用的时候不需要加&
    fmt.Println(v.Abs())
}

// 用函数写:
type Vertex struct {
    X, Y float64
}

func Abs(v Vertex) float64 {
    return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func Scale(v *Vertex, f float64) {
    v.X = v.X * f
    v.Y = v.Y * f
}

func main() {
    v := Vertex{3, 4}
    Scale(&v, 10)                 // 传进去的是指针,要加一个&
    fmt.Println(Abs(v))
}

函数如果需要传入指针(输入有*),那么必须传入(参数&,不然报错)

而type下面的方法就不需要,解释器会默认处理好。

一般都是传地址,复制值成本太高

func (v *Vertex) Scale(f float64) {
    v.X = v.X * f
    v.Y = v.Y * f
}

func (v *Vertex) Abs() float64 {
    return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func main() {
    v := &Vertex{3, 4}     // 不传地址也能跑对,建议穿地址
    fmt.Printf("Before scaling: %+v, Abs: %v\n", v, v.Abs())
    v.Scale(5)
    fmt.Printf("After scaling: %+v, Abs: %v\n", v, v.Abs())
}

接口

// 定义两个类型
type MyFloat float64

type Vertex struct {
    X, Y float64
}

f := MyFloat(-math.Sqrt2) // f是MyFloat的一个实例
v := Vertex{3, 4}

b := f
b = v   // 这样会报错,因为b在f的时候,类型已经定义为MyFloat了,不能重复定义


// 定义一个空接口
type Abser interface {}   // 一定要有个括号

var a Abser    // 定义a为接口类型,就什么东西都能赋值了,类似于any
// 这可以用来处理未知类型的值

a = f
a = v
a = &v


// 如果a想要用某个方法,可以在下面定义
type Abser interface {
    Abs() float64
}

// 不过需要赋值的类型,下面得有这个方法,不然会报错
func (f MyFloat) Abs() float64 { // myfloat下面增加一个abs()方法
...
}

func (v *Vertex) Abs() float64 { // vertex下面增加一个abs()方法,需要传入地址
...
}

a = f
a = v
a = &v
a.Abs()
// 能赋值也能调用方法


// 如果展开赋值就是这样:
var b Abser = Vertex{3, 4}
// 没办法用:=定义类型
// b := Vertex{3, 4}  // 这就变成Vertex类型了,而不是Abser

一个防御输入值为nil的方法:

type T struct {
    S string
}

func (t *T) M() {       // 增加一个函数
    if t == nil {                    // 如果是nil
        fmt.Println("<nil>")    // 就做处理
        return
    }
    fmt.Println(t.S)
}

如果值是nil,那么调用其底下方法,就会产生panic报错。

因为不知道该用哪个类型的方法。【可能因为nil下面并没有这个方法】

func main() {
    var i I                // nil
    i.M()                    // panic
}

类型断言

语法:t := i.(T),断言i为T类型。

var i interface{} = "hello"    // i是任意类型,此处赋值为string

s := i.(string)            // 断言为 string
fmt.Println(s)

f = i.(float64)         // 断言为float64
fmt.Println(f)            // 断言错,就会报错(panic)

// 避免断言错的报错,就要收第二个参数,即断言是否正确
s, ok := i.(string)
fmt.Println(s, ok)

f, ok := i.(float64)
fmt.Println(f, ok)

类型选择

从几个类型断言中选择分支的结构。

就是一种根据不同输入类型来选择操作的switch结构。类型用type表示

switch v := i.(type) {     // 括号里是 type
case int:                                        // 如果输入类型是int
  fmt.Printf("Twice %v is %v\n", v, v*2)
case string:                                // 如果输入类型是string
  fmt.Printf("%q is %v bytes long\n", v, len(v))
default:                                            // 默认情况,v和i的接口类型和值相同
  fmt.Printf("I don't know about type %T!\n", v)
}

stringer

fmt默认的打印方法,是调用结构体下面的String() 方法。

因此可以通过修改String()的方法,来修改该结构体的打印值。

【感觉这种能操纵默认方法的操作,非常有毒】

type Person struct {
    Name string
    Age  int
}

func main() {
    a := Person{"123", 3}
    b := Person{"234", 5}
    fmt.Println(a, b)                        // {123 3} {234 5}
}


// 修改默认的String()方法
func (p Person) String() string {
    return fmt.Sprintf("%v (%v years)", p.Name, p.Age)
}

// 就会根据这个String()来打印,因为fmt默认调用的是这个方法
...
    fmt.Println(a, b)    // 123 (3 years) 234 (5 years)
...


// 如果把String()改成;
func (p Person) String() string {
    return fmt.Sprintln("abc")
}
// 结果会打印
// abc


// 其实如果想改变该结构体的输出方法,完全可以通过加一个函数,而不是直接改变默认函数
func (p Person) PrintABC() string {
    return fmt.Sprintln("abc")
}

...
    fmt.Println("z.PrintABC()", z.PrintABC())
...

练习:Stringer

输入IPAddr{1, 2, 3, 4} 打印为 "1.2.3.4"。

go似乎不能直接对数组进行操作,这是一个不可变的东西。而又查到了,可以对string进行split和join。

因此一开始的想法,是把数组一个个拿出来转成string,然后再进行split和join成想要的地址样子。

不过byte转string,能查到的方法,只有在前面加一个string(),如string(p)。但这个仅对[]byte类型有用,而这里的p的类型是IPAddr,所以就不能用。

查到了这个方法来解决:

package main

import (
    "fmt"
    "strconv"
)

func (p IPAddr) String() string {
    lastIndex := len(p) - 1
    var s string
    for i, v := range p {
        s += strconv.Itoa(int(v))     // uint8 > int > string
        if i != lastIndex {                // 加点
            s += "."
        }
    }
    return s
}

func main() {
    hosts := map[string]IPAddr{
        "loopback":  {127, 0, 0, 1},
        "googleDNS": {8, 8, 8, 8},
    }
    for name, ip := range hosts {
        fmt.Printf("%v: %v\n", name, ip)
    }
}

感觉加点可以不用那么麻烦,最后删掉一位字符串即可

func (p IPAddr) String() string {
    var s string
    for _, v := range p {
        s += strconv.Itoa(int(v)) + "."            // 每个后面都加一个点
    }
    return s[:len(s)-1]                    // 最后少返回一位
}

不过go里面的数组都是值,是不可变的,因此字符串拼接的操作,其实是创建了一个新的字符串,其值是两个字符串拼接的而已。包括最后的切一位,也是生成了一个新的字符串。

也就是说,循环了len(p)次数,意味着产生了len(p)个字符串,不过之前的都被废弃了,只剩下最后一个而已。换言之这对内存成本很高。

然后发现了一个绕过拼接字符串的方法。

func (p IPAddr) String() string {
    return fmt.Sprintf("%v.%v.%v.%v", p[0], p[1], p[2], p[3])
}

错误

error 类型是一个内建接口

i, err := strconv.Atoi("42")
if err != nil {                                // 错误处理,如果有错就运行
    fmt.Printf("couldn't convert number: %v\n", err)
    return
}

error 为 nil 时表示成功;非 nil 的 error 表示失败。

练习:错误

package main

import (
    "fmt"
    "math"
)

type ErrNegativeSqrt float64

func (e ErrNegativeSqrt) Error() string {
    return fmt.Sprint("cannot sqrt negative number: ", float64(e))
}

func mySqrt(x float64) (float64, error) {
    if x < 0 {
        return 0.0, ErrNegativeSqrt(x)    // 把x从float64转成ErrNegativeSqrt
    }
    z := float64(1.0)
    for delta := float64(100); delta > 0.000001; {
        pre := z
        z -= (z*z - x) / (2 * z)
        delta = math.Abs(z - pre)
    }
    return z, nil
}

func main() {
    fmt.Println(mySqrt(2))
    fmt.Println(mySqrt(-2))
}

ErrNegativeSqrt(x)是把x从float64转成ErrNegativeSqrt。

作为error输出的时候,会优先调用Error()来打印。然而这个函数返回的也是一个打印,因此如果用e本身,就会触发死循环。得转换一下格式才行。

reader

一次读取多少字节

func main() {
    r := strings.NewReader("Hello, Reader!")  // 必须要这么定义,只是string就读取不了

    b := make([]byte, 8)    // 一次读取8字节
    for {
        n, err := r.Read(b)    // 从r中读取b个大小的东西
        fmt.Printf("n = %v err = %v b = %v\n", n, err, b) 
    // n = 8 err = <nil> b = [72 101 108 108 111 44 32 82]
        fmt.Printf("b[:n] = %q\n", b[:n])
    // b[:n] = "Hello, R",读取了这么多
        if err == io.EOF { // 如果读光了就报错 err = EOF
            break
        }
    }
}

练习:reader

package main

import "golang.org/x/tour/reader"

type MyReader struct{}

func (m MyReader) Read(b []byte) (int, error) {
    b[0] = 'A'
    return 1, nil
}

func main() {
    reader.Validate(MyReader{})
}

不是很懂这个😂😂

练习:rot13Reader

package main

import (
    "io"
    "os"
    "strings"
)

type rot13Reader struct {
    r io.Reader
}

func rot13(b byte) byte {
    switch {
    case 'A' <= b && b <= 'M':
        b = b + 13
    case 'M' < b && b <= 'Z':
        b = b - 13
    case 'a' <= b && b <= 'm':
        b = b + 13
    case 'm' < b && b <= 'z':
        b = b - 13
    }
    return b
}

func (mr rot13Reader) Read(b []byte) (int, error) {
    n, e := mr.r.Read(b)
    for i := 0; i < n; i++ {
        b[i] = rot13(b[i])
    }
    return n, e
}

func main() {
    s := strings.NewReader("Lbh penpxrq gur pbqr!")
    r := rot13Reader{s}
    io.Copy(os.Stdout, &r)
}

图像

image包

type Image interface {
    ColorModel() color.Model
    Bounds() Rectangle
    At(x, y int) color.Color
}

练习:图像

package main

import (
    "image"
    "image/color"

    "golang.org/x/tour/pic"
)

type Image struct{}

func (i Image) ColorModel() color.Model {
    return color.RGBAModel
}

func (i Image) Bounds() image.Rectangle {
    return image.Rect(0, 0, 200, 200)
}

func (i Image) At(x, y int) color.Color {
    return color.RGBA{R: uint8(x), G: uint8(y), B: uint8(255), A: uint8(255)}
}

func main() {
    m := Image{}
    pic.ShowImage(m)
}

并发

go routine

goroutine是由 Go 运行时管理的轻量级线程。

go f(x, y, z)

会启动一个新的 Go 程并执行

f(x, y, z)

在新的 Go 程中只负责执行 f ,其他的求值发生在当前的 Go 程中。

Go 程在相同的地址空间中运行,因此在访问共享的内存时必须进行同步。

信道

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

ch <- v    // 将 v 发送至信道 ch。
v := <-ch  // 从 ch 接收值并赋予 v。

(“箭头”就是数据流的方向。)

和映射与切片一样,信道在使用前必须创建:

ch := make(chan int)

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

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)
}

如果计算量小的话,就会在第二个协程开启前,就已经把第一个协程内容删掉了

带缓冲的信道

make初始化信道,第二个参数是缓冲长度

ch := make(chan int, 100)

仅当信道的缓冲区填满后,向其发送数据时才会阻塞。当缓冲区为空时,接受方会阻塞。

【满了不让塞,空了不让拿】

range 和 close

发送者可通过 close 关闭一个信道来表示没有需要发送的值了。

接收者可以通过为接收的第二个参数,来判断信道是否被关闭:若没有值可以接收且信道已被关闭,那么在执行完

v, ok := <-ch

之后 ok 会被设置为 false。

循环 for i := range c 会不断从信道接收值,直到它被关闭。

注意: 只有发送者才能关闭信道,而接收者不能。向一个已经关闭的信道发送数据会引发程序恐慌(panic)。

还要注意: 信道与文件不同,通常情况下无需关闭它们。只有在必须告诉接收者不再有需要发送的值时才有必要关闭,例如终止一个 range 循环。

func fibonacci(n int, c chan int) {
    x, y := 0, 1
    for i := 0; i < n; i++ {
        c <- x
        x, y = y, x+y
    }
    close(c)
}

func main() {
    c := make(chan int, 10)
    go fibonacci(cap(c), c)
    for i := range c {
        fmt.Println(i)
    }
    _,ok:=<-c
    fmt.Println(ok)

}

select

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

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

func fibonacci(c, quit chan int) {
    x, y := 0, 1
    for {
        select {
        case c <- x:
            x, y = y, x+y
        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)
}

可以用struct{}改写,这样省内存空间。

func fibonacci(c chan int, quit chan struct{}) {
    x, y := 0, 1
    for {
        select {
        case c <- x:
            x, y = y, x+y
        case <-quit:
            fmt.Println("quit")
            return
        }
    }
}

func main() {
    c := make(chan int)
    quit := make(chan struct{})
    go func() {
        for i := 0; i < 10; i++ {
            fmt.Println(<-c)
        }
        quit <- struct{}{}
    // 第一个括号,是struct的类型,是空;第二个是其内容,是空
    }()
    fibonacci(c, quit)
}

默认选择

为了在尝试发送或者接收时不发生阻塞,可使用 default 分支:

select {
case i := <-c:
    // 使用 i
default:
    // 从 c 中接收会阻塞时执行
}
func main() {
    tick := time.Tick(100 * time.Millisecond)
    boom := time.After(500 * time.Millisecond)
    for {
        select {
        case <-tick:
            fmt.Println("tick.")
        case <-boom:
            fmt.Println("BOOM!")
            return
        default:
            fmt.Println("    .")
            time.Sleep(50 * time.Millisecond)
        }
    }
}

练习:等价二叉查找树

树的结构是这样的:

type Tree struct {
    Left  *Tree
    Value int
    Right *Tree
}

要写一个walk函数,来一个个把树的内容传到信道里面,然后用same函数来比较两个树是否一样。

package main

import (
    "fmt"
    "golang.org/x/tour/tree"
)

//  递归向channel传值
func sendValue(t *tree.Tree, ch chan int) {
    if t != nil {
        sendValue(t.Left, ch)
        ch <- t.Value
        sendValue(t.Right, ch)
    }
}

//  发送value,结束后关闭channel
func Walk(t *tree.Tree, ch chan int) {
    sendValue(t, ch)
    close(ch)
}

// 使用写好的Walk函数来确定两个tree对象  是否一样 原理还是判断value值
func Same(t1, t2 *tree.Tree) bool {
    ch1 := make(chan int)
    ch2 := make(chan int)
    go Walk(t1, ch1)
    go Walk(t2, ch2)
    for i := range ch1 { // ch1 关闭后   for循环自动跳出
        if i != <-ch2 {
            return false
        }
    }
    return true
}

func main() {
    // 打印 tree.New(1)的值
    var ch = make(chan int)
    go Walk(tree.New(1), ch)
    for v := range ch {
        fmt.Println(v)
    }

    //  比较两个tree的value值是否相等
    fmt.Println(Same(tree.New(1), tree.New(1)))
    fmt.Println(Same(tree.New(1), tree.New(2)))
}

sync.Mutex

若想避免冲突,保证每次只有一个 Go 程能够访问一个共享的变量,从而

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

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

  • Lock

  • Unlock

我们可以通过在代码前调用 Lock 方法,在代码后调用 Unlock 方法来保证一段代码的互斥执行。 我们也可以用 defer 语句来保证互斥锁一定会被解锁

// SafeCounter 的并发使用是安全的。
type SafeCounter struct {
    v   map[string]int
    mux sync.Mutex  // 互斥锁
}

// Inc 增加给定 key 的计数器的值。
func (c *SafeCounter) Inc(key string) {
    c.mux.Lock()
    // Lock 之后同一时刻只有一个 goroutine 能访问 c.v
    c.v[key]++
    c.mux.Unlock()
}

// Value返回给定 key 的计数器的当前值。
func (c *SafeCounter) Value(key string) int {
    c.mux.Lock()
    // Lock 之后同一时刻只有一个 goroutine 能访问 c.v
    defer c.mux.Unlock()
    return c.v[key]
}

func main() {
    c := SafeCounter{v: make(map[string]int)}
    for i := 0; i < 1000; i++ {
        go c.Inc("somekey")
    }

    time.Sleep(time.Second)
    fmt.Println(c.Value("somekey"))
}

web爬虫

image-20190726131521502

当一个数组变量被赋值或者被传递的时候,实际上会复制整个数组。【js当中也是这样,重新赋值一个值,如string,相当于照样新建了一个值。与原来的毫无关系。】

作者:BigManing 来源:CSDN 原文:

image-20190726162610193
image-20190726163632583

你会发现 很有帮助。

作者:BigManing 来源:CSDN 原文:

主要是Sprintf这个函数的操作。

就是用reader解密咯。加密方法是。用一遍加密,再用一遍解密。因为就是把字母替换为其13位后面的字母。

image-20190822171738736

// 原文链接:

官网的文档
为什么要这么做?
深入defer
参考
参考
Go 切片:用法和本质
js参考
https://blog.csdn.net/qq_27818541/article/details/54346106
strings.Fields
https://blog.csdn.net/qq_27818541/article/details/54347998
https://blog.csdn.net/qq_27818541/article/details/54347335
https://blog.csdn.net/u013564276/article/details/49804633
参考
https://blog.csdn.net/qq_27818541/article/details/54347939
https://linkscue.com/2018/02/28/go-tour-practice-error/
ROT13
https://blog.csdn.net/qq_27818541/article/details/54379030
https://blog.csdn.net/qq_27818541/article/details/54411990
image-20190726153457126
image-20190726153604851
image-20190726153710804