我的golang笔记

隔壁老王 等级 655 3 3

面向对象思想

面向对象简介

  • 编程思想

    与编程语言无关。

    C语言、Go中的 结构体 就是后来面向对象编程语言中的类。

    面向对象编程:高内聚,低耦合。

  • 特性

    • 继承 —— 匿名字段(实名字段)
    • 封装 —— 方法
    • 多态 —— 接口(interface)
  • Go 语言是典型的面向对象编程语言。

通过程序描述对象

  • 创建类(指定类属性)

    • 类属性:静态。---- 名词、形容词
    // go语言中的类就是结构体--类型(int/bool/byte/string)
    type Student struct {    // 定义Student类 
        name string            // 类属性(域)、成员属性、成员变量
        age int
        marry bool
        addr string            // 
    }
    
    // 按照类创建对应的类对象(实例、对象)
    var stu Student = Student{"亚瑟", 36, false, "峡谷"}
  • 绑定类方法

    • 方法:动态。--- 动词。
    func (s Student) study() {
        fmt.Println("I 'm a student, good good study, day day up")
    }
    // 使用学生类对象,调用学生的方法
    stu.study() 

匿名字段

  • 实名字段

    • 在继承中。子类包含父类属性。字段名 和 字段类型都有。
    type Person struct {        // 父类
        name string
        age int
        sex string
    }
    
    type Student struct {        // 子类
        per Person            // 实名字段。
        score int
    }    
  • 匿名字段

    • 在继承中。子类包含父类属性。只有字段类型。
    type Person struct {        // 父类
        name string
        age int
        sex string
    }    
    type Student struct {        // 子类
        Person        // 匿名名字段。
        score int
    }    
    
  • 子类对象在访问父类成员时,实名字段必须,通过字段名访问父类成员。

    stu.per.name    stu.per.age        // 实名
    stu.Person.name        //匿名字段完整写法
    stu.name    stu.age    //匿名,Go优化后简便写法  -- 推荐

同名字段

  • 父类属性和子类属性(字段名、字段类型)相同
  • 子类访问同名字段时,采用 “就近原则” 。使用子类属性。(前提:匿名字段。)
  • 子类、父类同名字段,占用的内存不同。

我的golang笔记

当程序中,多次出现 “同名字段” 时, 采用 实名字段方法解决。尽量避免。

指针匿名字段

  • 在子类继承父类属性时,包含 指针类型的 父类属性。

    // 创建父类
    type Person4 struct {
        name string            // 同名字段
        age int
        marry bool
    }
    
    // 创建子类,从父类继承
    type Student4 struct {
        *Person4            // 使用指针匿名字段,从父类继承
        score int
    }
    
  • 子类访问指针匿名字段:

    (*stu.Person4).name = "三傻"        // 方法1:完整打印父类成员属性。
    (stu.Person4).name = "三傻"        // 方法2:Go优化后写法
    stu.name = "三傻"                    // 方法3:直接访问父类属性。

    我的golang笔记

  • 指针匿名字段,在访问时,应先检查是否为 nil 指针。再使用。

    // 给指针匿名字段初始化
    if stu.Person4 == nil {
        stu.Person4 = new(Person4)
    }

继承关系

  • 程序编译的过程

    1. 预处理 --- 代码的替换

    2. 编译 --- 词法分析、语法分析、开辟内存(data(初始化的全局变量)/rodata/bss)、转换为汇编

    3. 汇编 --- 转换为 二进制

    4. 链接 --- 数据段合并、地址回填。

结论:

  • 不允许 父子类之间,使用 普通字段 相互继承。(指针字段可以)

  • 类中不可以使用 普通字段 自己嵌套自己 (指针字段可以)

    type LinkNode struct {
        Data int                // 8
        //Next LinkNode            // ???
        Next *LinkNode            // 8
    }

多重继承

  1. C —— B —— A
// 父类的父类
type Human6 struct {
    name string
    age int
}
// 父类
type Person6 struct {
    Human6
    sex string
}
// 子类
type Student6 struct {
    Person6
    score int
}

// 创建子类对象
stu := Student6{Person6{Human6{"林黛玉", 21}, "男"}, 100}

// 访问对象成员
stu.Person6.Human6.name = "贾宝玉"        // 完整写法
stu.Person6.name = "贾宝玉"
stu.Human6.name = "贾宝玉"
stu.name = "贾宝玉"    
  1. C —— B

    C —— A

// 子类的父类
type Human7 struct {
    name string
    age int
}
// 子类的另一个父类
type Person7 struct {
    addr string
    sex string
}
// 子类
type Student7 struct {
    Human7
    Person7
    score int
}

func main() {
    // 创建子类对象
    var stu Student7

    // 访问对象成员,赋值
    stu.name = "林冲"            // stu.Human7.name = "xxx"
    stu.age = 31
    stu.sex = "男"
    stu.addr = "汴梁"

    fmt.Println(stu)
}
  • 应该在编程过程中,尽量避免多重继承。防止过高耦合,防止大量同名字段产生。

方法的定义和使用

// 类方法的定义语法
func (绑定对象/方法接受者) 类方法名(形参列表)返回值列表 {
    方法体(编码)
    return 返回值列表
}
  • 使用方法
// 1. 创建类对象 
var stu Student

// 2. 使用类对象,调用类方法
stu.类方法(实参列表)
// 方法调用时,有两处赋值
    1)实参,赋值给形参。
    2)方法调用对象,给“绑定对象”赋值。

// 创建类
type Student8 struct {
    name string
    age  int
}

// add 被指定为 Student8 类的方法。 只有Student8类对象才能调用该方法。
func (s Student8) add(a int, b int) {
    sum := a + b
    fmt.Println("sum =", sum)
}

func main() {
    //add(10, 20) // 函数调用。
    // 创建类对象
    var stu Student8        // stu :=
    // 使用类对象,调用类方法
    stu.add(10, 20)
}

结构体类型方法

  • 子类对象可以继承父类的属性同时也可以继承父类方法。
// 子类的另一个父类
type Person9 struct {
    name string
    addr string
    sex  string
}
// 子类
type Student9 struct {
    Person9        // 继承!!!—— 子类有父类的属性。同时也能使用父类的方法。
    score int
}

// 绑定 父类方法 
func (per Person9) personInfo() {
    fmt.Printf("大家好,我叫%s,我是%s生,我来自%s\n", per.name, per.sex, per.addr)
}

func main() {
    //var per = Person9{"武大郎", "阳谷县", "男"}
    //per.personInfo()
    stu := Student9{Person9{"武二郎", "阳谷县", "女"}, 98}
    stu.personInfo()
}

基础类型方法

// 创建基础数据类
type INT int // 基础数据类型名 --- 大写 --- 类

// 绑定方法
func (a INT) add(b INT) INT {
    return a + b
}

func main() {
    //定义类对象
    //var m int = 10    // 不是 INT 类对象
    //var n int = 20

    var m INT = 10        // 是 INT 类对象
    var n INT = 20

    // 调用类方法
    ret := m.add(n)
    fmt.Println("ret =", ret)
}

结构体指针方法

  • 方法的调用者,是一个普通结构体对象

我的golang笔记

  • 方法的调用者, 是结构体指针变量。

我的golang笔记

【总结】:

  1. 结构变量,作为方法绑定对象时,只能做“读”,无法修改对象的属性。

  2. 结构体指针变量,作为方法的绑定对象时,既可以“读”, 又可以“写”。可以 修改对象的属性。

    并且,Go语言优化后的使用方法,与普通结构体变量方法一致。

    // 子类的另一个父类
    type Person11 struct {
        name string
        addr string
        sex  string
    }
    // 子类
    type Student11 struct {
        Person11
        score int
    }
    
    func (per *Person11) change() {
        // 修改 name 属性
        //(*per).name = "杨二郎"        // 真正使用地址访问属性
        per.name="杨小妹"            // Go优化的 解引用。
        fmt.Println(*per)
    }
    
    func main() {
        stu := Student11{Person11{"武二郎", "阳谷县", "女"}, 98}
        // 借助change函数,修改 name
        //(&stu).change()
        stu.change()        // Go语言优化后的简便写法。
        // 打印
        fmt.Println(stu)
    }

方法重写

  • 子类对象绑定的 方法名 与父类的方法名一致。发生方法重写。
  • 重写特性:
    • 默认子类对象调用 子类方法。
    • 子类对象使用父类的类名索引父类方法。
// 公共类
type Person13 struct {
    name string
    age  int
    sex  string
}

// 程序类,从父类继承
type Developer13 struct {
    Person13
    workyears int
}

// 绑定父类方法
func (per *Person13) SayHello13() {
    fmt.Printf("大家好,我叫%s,我今年%d岁,我是%s生,", per.name, per.age, per.sex)
}

// 绑定子类方法 -- 程序员 -- 方法重写。
func (dev *Developer13) SayHello13(a int) int {
    fmt.Println("我是子类:", dev, "a =", a)
    return a+10
}

func main() {
    //创建子类对象 —— 程序员
    dev := Developer13{Person13{"图灵", 57, "男"}, 30}

    // 默认方法重写后,子类对象调用子类方法。
    ret := dev.SayHello13(9527)
    fmt.Println("ret=", ret)

    // 方法重写后,子类可以 使用 父类类名索引父类方法。
    dev.Person13.SayHello13()
}

方法值和方法表达式

方法值

  • 就是内存中 text (代码区) (只读)上的一块地址值。与函数值类似。
  • 可以使用指针保存,方法值。

方法表达式

  • 定义 函数指针变量

    var p func(int) int    // 类型受函数原型影响。
  • 使用函数名、方法名 初始化 p

    p = stu.test        // 方法名
    p = add        // 函数名
  • 借助 p 调用函数。因此函数、方法原型

    ret = p(10)  // 传递实参。

面向对象计算器实现

1. 继承、封装

// 继承和封装实现加、减法
// 父类
type Operate struct {
    n1 int
    n2 int
}
// 创建 加法子类,继承于父类
type AddOpt struct {
    Operate
}
// 创建 加法子类,继承于父类
type SubOpt struct {
    Operate
}

// 绑定加法方法
func (a *AddOpt) Result() int {
    return a.n1 + a.n2
}
// 绑定加法方法
func (s *SubOpt) Result() int {
    return s.n1 - s.n2
}

func main() {
/*    // 创建加法对象
    add := AddOpt{Operate{10, 20}}
    // 加法对象,调用方法
    ret := add.Result()
    fmt.Println("ret =", ret)*/

    // 创建减法对象
    sub := SubOpt{Operate{10, 20}}
    // 减法对象,调用方法
    ret := sub.Result()
    fmt.Println("ret =", ret)
}

2. 接口

// 添加接口,使用接口调用方法。
// 创建接口
type Calcer interface {
    Result() int
}
// 父类
type Operate3 struct {
    n1 int
    n2 int
}
// 创建 加法子类,继承于父类
type AddOpt3 struct {
    Operate3
}

// 创建 加法子类,继承于父类
type SubOpt3 struct {
    Operate3
}
// 绑定加法方法
func (a *AddOpt3) Result() int {
    return a.n1 + a.n2
}
// 绑定加法方法
func (s *SubOpt3) Result() int {
    return s.n1 - s.n2
}

func main() {
    // 创建减法对象
    sub := SubOpt3{Operate3{10, 20}}
    // 创建接口变量
    var cal Calcer
    // 使用类对象给接口变量赋值
    cal = &sub
    // 使用接口变量调用方法
    ret := cal.Result()
    fmt.Println("ret =", ret)
}

3. 多态

// 
// 创建接口
type Calcer5 interface {
    Result5() int
}

// 父类
type Operate5 struct {
    n1 int
    n2 int
}
// 创建 加法子类,继承于父类
type AddOpt5 struct {
    Operate5
}
// 创建 加法子类,继承于父类
type MulOpt5 struct {
    Operate5
}

// 创建 加法子类,继承于父类
type SubOpt5 struct {
    Operate5
}
// 绑定加法方法
func (a *AddOpt5) Result5() int {
    return a.n1 + a.n2
}
// 绑定加法方法
func (s *SubOpt5) Result5() int {
    return s.n1 - s.n2
}
// 绑定乘法方法
func (s *MulOpt5) Result5() int {
    return s.n1 * s.n2
}
// 添加多态, 定义函数,指定接口做参数
func test(res Calcer5) int {
    return res.Result5()
}

func main() {
    add := AddOpt5{Operate5{10, 20}}
    sub := SubOpt5{Operate5{10, 20}}
    mul := MulOpt5{Operate5{10, 20}}

    ret := test(&sub)
    fmt.Println("ret =", ret)
    ret = test(&add)
    fmt.Println("ret =", ret)
    ret = test(&mul)
    fmt.Println("ret =", ret)
}

4. 工厂模式

// 提供满足条件的数据,不关心过程,直接获取结果
// 创建接口
type Calcer7 interface {
    Result7() int
}
// 父类
type Operate7 struct {
    n1 int
    n2 int
}
// 创建 加法子类,继承于父类
type AddOpt7 struct {
    Operate7
}
// 创建 加法子类,继承于父类
type MulOpt7 struct {
    Operate7
}

// 创建 加法子类,继承于父类
type SubOpt7 struct {
    Operate7
}
// 绑定加法方法
func (a *AddOpt7) Result7() int {
    return a.n1 + a.n2
}
// 绑定加法方法
func (s *SubOpt7) Result7() int {
    return s.n1 - s.n2
}
// 绑定乘法方法
func (s *MulOpt7) Result7() int {
    return s.n1 * s.n2
}
// 添加多态, 定义函数,指定接口做参数
func test7(res Calcer7) int {
    return res.Result7()
}

// 创建工厂类
type Factory struct {
    // 没有类属性。只用来绑定方法。
}
// 绑定工厂类方法
func (f *Factory)CreateFactory(a int, b int, ch byte)  {
    // 分开处理各种运算式
    switch ch {
    case '+':
        add := AddOpt7{Operate7{a, b}}
        fmt.Printf("%d %c %d = %d\n",a, ch, b, test7(&add))
    case '-':
        sub := SubOpt7{Operate7{a, b}}
        fmt.Printf("%d %c %d = %d\n",a, ch, b, test7(&sub))
    case '*':
        mul := MulOpt7{Operate7{a, b}}
        fmt.Printf("%d %c %d = %d\n",a, ch, b, test7(&mul))
    default:
        fmt.Println("暂不支持此种运算")
    }
}

func main() {
    // 创建工厂类对象
    var f Factory
    f.CreateFactory(10, 20, '+')
    f.CreateFactory(10, 20, '-')
    f.CreateFactory(10, 20, '*')
    f.CreateFactory(10, 20, '/')
}
  • 使用 继承、封装、接口、多态、工厂模式。—— 计算器。

什么是接口

接口:规则和标准。不参加具体的实现。只包含方法原型。

函数:

  1. 函数定义:func关键字 函数名(形参列表) 返回值列表 {

    ​ 函数体

    ​ return 返回值列表

    ​ }

  2. 函数调用:函数名(实参列表)

  3. 函数原型:函数名(形参列表) 返回值列表

接口的定义和使用

定义接口:

type 接口名 interface {
    方法名(形参列表) 返回值列表        // 方法原型
}

建议,接口名以er结尾。

使用接口

  1. 定义接口

    type Humaner interface { PrintInfo() }

  2. 按接口的定义语法,实现方法(创建类,绑定类方法)

    type Person struct {
    }
    
    func (per *Person) PrintInfo() {
        fmt.Printf("大家好,我叫%s,我今年%d岁,我%s结婚,来自于%s\n",
            per.name, per.age, per.marry, per.addr)
    }
  3. 创建接口变量

    var h Humaner

  4. 使用类对象给接口变量赋值

    h = &per

  5. 使用接口变量调用方法。

    h.PrintInfo()

多态

Go语言实现多态

  • Go语言实现多态,封装一个普通函数,将接口设计为函数的参数。
  • 函数内,使用参数(接口)。调用方法。

多态使用步骤

  1. 定义接口
  2. 创建类,绑定方法,实现接口
  3. 创建函数,指定接口类型为参数
  4. 函数内,使用接口,调用方法
// 定义接口
type Humaner4 interface {
    Info()
}

// 创建Teacher类
type Teacher4 struct {
    name string
    age  int
}

// 创建 Doctor 类
type Doctor4 struct {
    name string
    age  int
    knife string
}
// 给类按接口绑定方法 。
func (d *Doctor4) Info() {
    fmt.Println("name=", d.name, "age=", d.age, "knife=", d.knife)
}

// 给类按接口绑定方法 —— 绑定给了Teacher4 , 只有Teacher4 对象能调用。
func (t *Teacher4) Info() {
    fmt.Println("name=", t.name, "age=", t.age)
}

// 添加多态。指定接口作为参数         —— 没有绑定给任何对象,可以任意调用。
func Info(human Humaner4)  {
    // 使用接口变量调用方法
    human.Info()
}

func main() {
    // 创建类对象
    teacher := Teacher4{"袁腾飞", 36}
    Info(&teacher)   // 使用多态

    // 创建 doctor 实例
    doc := Doctor4{"华佗", 97, "金丝大环刀"}
    Info(&doc)
}

小结

  • 编码未来!
  • 前期编写的程序,按接口实现多态,能适应未来新对象、方法使用。

接口继承和转换 [子集-超集] (了解)

  • 接口可以使用 继承来得到父类接口的 方法。 语法参照 类继承语法。

    // 创建接口                            --- 子集 (少)
    type Humaner8 interface {
        SayHello()
    }
    
    // 创建 Developer8 继承 Humaner8      --- 超集(多)
    type Developer8 interface {
        Humaner8
        SayHelloWorld()
    }
  • 转换:

    • 子集 不可以赋值给 超集。
    • 超集可以赋值给 子集。

空接口

  • var i interface{}
  • 默认值:
  • 默认类型:
  • 作用:接收任意类型的数据。
  • 特性:
    • 接收何种数据,就变为何种类型。
    • 只能存储,不能用来运算。如果想使用运算,需要 “类型断言”

类型断言

  • 应用场景:

    • 只能应用于 “空接口” ( interface{} )类型。
  • 语法:

    • value, ok := 变量名.(类型) ---- 判断变量是否为 () 内的类型
      • 是!ok 返回 true, value 保存变量值。
      • 不是!ok 返回 false,value 保存变量对应实际类型的 零值。
    value, ok := i1.(bool)
    if ok == true {
        fmt.Println("value=", value, "ok=", ok)
    } 
    
    // 简化成:
    if val, isType := i1.(bool); isType {
         fmt.Println("value=", value, "ok=", ok)
    }
    
    // 可以使用 switch ... case
    switch v.(type) {    // 取出 interface 变量的 类型
        case int :
            fmt.Println(v, "是 int 类型数据")
            // 默认,相当于 每个 case分支中,都自动添加了 break。 go语言中,不存在case穿透
    }

异常处理

异常种类

  • 编辑错误:语法错误!GoLand 工具可以查找。 无法编译程序,不能生成 .exe
  • 编译错误:第三方函数库使用时。可以进行编译,无法生成 .exe
  • 运行时错误:既可以编译,也可以生成 .exe。只出现在程运行期间。程序不能正常终止。

==运行时异常==error -- 未雨绸缪

  1. ==封装异常==:

    func test11(a, b int) (ret int, err error) {
        // 判断除数是否为0
        if b == 0 {
            // 产生错误信息。
            err = errors.New("除数不允许为0!")
            return
        }
        ret = a / b
        return
    }
  2. 处理异常:

    ret, err := test11(10, 0)
    if err != nil {
        fmt.Println("test11 err:", err)
        return 
    }

Panic异常

  • 也是运行时!比 error 更为严重!不可逆!—— 只能在编码初期提早预见。
  • 异常产生
    • 自动产生:由系统判断,产生。
    • 手动产生:立即终止程序。
  • 解决方法,需要使用 defer、recover。

defer关键字

  • 用来对指令(函数、表达式)进行延迟调用。延迟到当前指令对应的函数运行结束, 栈帧释放前。

recover 错误拦截

  • 拦截 panic 异常。单独无法工作。必须依赖 defer

  • 固定用法:

    // 函数起始位置,注册 recover 拦截监听。在函数调用结束时,返回监听结果。
    defer func() {
        fmt.Println(recover())
    }()
    panic异常。 

Channel

channel的定义

  • 定义语法:
    • var ch = make(chan 通道中传递的数据类型, 容量大小)
      • 例:ch := make(chan int)
      • 例:ch := make(chan string, 0)
  • 读写:
    • 读channel :
      • <-ch 读到数据,丢弃
      • num := <-ch 读到数据,存入 num中
    • 写 channel:
      • ch <- data data类型严格与 定义的语法一致
  • 特性:
    1. 通道中的数据只能单向流动。一端读端、另外必须写端。
    2. 通道中的数据只能一次读取,不能重复读。先进先出。
    3. 读端 和 写端在不同的 goroutine 之间。
    4. 读端读,写端不在线,读端阻塞。写端写,读端不在线,写端阻塞。
  • 系统 3 个特殊文件:系统打开、系统关闭。
    • stdin:标准输入文件(标准输入缓冲区) —— 键盘 —— 0
    • stdout:标准输出文件(标准输出缓冲区) —— 屏幕 —— 1
    • stderr:标准错误文件(标准错误缓冲区) —— 屏幕 —— 2

go程间通信

  • 多个go程间,如果有多份共享资源时,需要分别同步。

我的golang笔记

channel 的分类

无缓冲channel

  • 要求 读端和写端必须同时在线。对端不在线,本端阻塞。

有缓冲channel

  • 定义语法: ch := make(chan 数据类型 ,容量) 容量是有实际数据。 != 0
  • 读端,不在线,写端可以将数据直接写入缓冲区(不阻塞)。直到缓冲区被写满,依然没有读端,写端阻塞。

同步通信、异步通信

  • 同步通信:—— 无缓冲 channel
    • 一个调用发出,如果没有得到结果,那么该调用不返回。 —— 阻塞
    • 相当于 打电话。双方必须同时在线。
  • 异步通信:—— 有缓冲 channel
    • 一个调用发出,不等待结果,直接返回。 —— 不阻塞。
    • 相当于 发短信。双方不需同时在线。一端发送完,立即返回。

关闭channel

  • 当写端,写完数据后,使用 close(ch) 关闭 channel

  • 读端,可以判断channel是否可读:

    for {
        if num, isOK := <-ch; isOk {        // 不能写成 ch
            fmt.Println("num=", num)  // 读到ch中数据
        } else {            // 写端关闭
            break    // 结束读操作。
        }
    }

    如果写端没关闭:

    • isOk == true, 数据被读至 num 中

    如果写端关闭:

    • isOK == false,num为数据类型 零值(默认值)

    简便写法

    for num := range ch {        // 不能写成 <-ch
        fmt.Println("num=", num)
    }
  • 关闭channel的特性:

    1. 如果写端没有关闭,暂停写入,读端阻塞等待。
    2. 如果写端已经关闭,不能写入数据。再写入,报错:panic:send on closed channel
    3. 如果写端已经关闭,读端依然能读取,读到的是数据类型零值(默认值)。

单向channel

定义语法:

  • 双向channel:

    • 定义语法:var ch chan int
  • 单向读channel:

    • 定义语法:var chr <-chan int
    • chr = ch 双向channel 给单向读 channel 赋值。
    • 只能做 读操作。如果写,报错:
      • invalid operation: chr <- 888 (send to receive-only type <-chan int)
  • 单向写channel:

    • 定义语法:var chw chan<- int
    • chw = ch 双向channel 给单向写 channel 赋值。
    • 只能做 写操作,如果读,报错:
      • invalid operation: <-chw (receive from send-only type chan<- int)
  • 单向channel *不能 *给双向channel赋值。

单向channel的使用:

  • 主要应用于函数传参。单向 channel 可以在语法层面限定 channel 使用的方式。
    • 单向读:不能进行 写操作。
    • 单向写:不能进行 读操作。

生产者消费者模型

  • 生产者模块:产生数据。放置到公共区
  • 消费者模块:从公共区消费数据。
  • 公共区(缓冲区):
    1. 解耦:生产者和消费者,耦合度降低
    2. 并发:生产者和消费者处理数据的速度可以不一致。不影响各自并发。
    3. 缓存:生产者可以利用公共区的缓冲能力,暂存数据,提高生产效率。

定时器:Timer

  1. 创建定时器:

    func NewTimer(d Duration) *Timer
    
    timer := time.NewTimer(time.Second * 3)

    type Timer struct {

      C <-**chan** Time        // 当定时时长到达时,系统会写入当前时间
      r runtimeTimer

    }

  2. 读 Timer 的C 成员。定时时间没到,会阻塞。定时时长到达时,系统会写入当前时间,解除阻塞

    t := <-timer.C

time.After()

合并 上述 1. 2. 为一步操作

   fmt.Println("  ",time.Now())
   t := <-time.After(time.Second * 3)
   fmt.Println("t:", t)

Select关键字

select 基本使用

  • 作用:监听channel上的数据流动

  • 语法:

    for {
        select {
            case <- chan1:
            // 如果chan1成功读到数据,则进行该case处理语句
            case chan2 <- 1:
            // 如果成功向chan2写入数据,则进行该case处理语句
            default:
            // 如果上面都没有成功,则进入default处理流程
        }
    }
  • 特性:

    1. 每一个case分支,都必须一个 IO操作(channel r/w事件)。
    2. 通常将 select 置于 for 循环中。
    3. 一个case监听的 channel 不满足监听条件。当前case分支阻塞。
    4. 当所有case分支都不满足监听条件时,select如果包含default分支,走default;select等待case。
    5. 当监听的多个case分支中,同时有多个case满足,随机选择任意一个执行。
    6. 为防止忙轮询,可以适当选择省略 default
  • break关键字不能应用于结束整个 select。 只能跳出一个case分支。

借助select实现超时

  • 作用:借助 select 和 time.After() 实现超时处理

  • 步骤:

    1. 创建 select , 启动监听 channel

      for {
          select {
              case num := <-ch:
                  fmt.Println("num =", num)
      
              case <-time.After(time.Second * 3):  //1.创建 Timer, 2.读 C 成员
                  fmt.Println("子go程读到系统时间, 定时满 3 秒")
                  quit <- false
              case
      
            case
      
            case
          }
       }
    2. 监听超时定时器:

      case <-time.After(time.Second * 3)

    3. 当select监听的其他case分支满足时,time.After所在的case分支,会被重置成初始定时时长。

    4. 直到在select 监听期间,没有任何case满足监听条件。time.After 才能定时满。

死锁

  • 不是锁的一种。是错误使用锁的现象。

常见死锁

  1. 单个go程使用同一个channel自己读、自己写。
  2. 多个go程使用 channel通信,go程创建之前,对channel读、写造成死锁。
  3. 多个go程使用多个channel 通信,相互依赖造成死锁。
  4. 多个go程使用 锁(读写锁、互斥锁)和 channel 通信。

互斥锁mutex

  • 作用:保护公共区,防止多个控制流共同访问共享资源时造成数据混乱。

  • 建议锁。不具备强制性。

  • 使用注意事项:

    1. 访问公共区之前,加锁。访问结束立即解锁。
    2. 粒度越小越好
  • 使用方法:

    1. 创建互斥量 var mutex sync.Mutex

    2. 加锁 mutex.Lock()

      ...... 访问公共区

    3. 解锁 mutex.Unlock()

读写锁RWMutex

特性

  1. 读共享,写独占
  2. 写锁优先级高
  3. 锁只有一把,具备两种属性(r/w)。加锁时根据使用需求,选择锁属性。
  • 建议锁。不具备强制性。

使用注意事项:

  1. 访问公共区之前,加锁。访问结束立即解锁。
  2. 粒度越小越好

使用方法:

  1. 创建读写锁 var rwmutex sync.RWMutex

  2. 加读锁 rwmutex.RLock() 加写锁 rwmutex.Lock()

    ​ ...... 访问公共区

  3. 解读锁 rwmutex.RUnlock() 解写锁 rwmutex.Unlock()

  • 建议:不要将锁 和 channel 混合 使用。 —— 条件变量。

条件变量

​ 条件变量,不是锁的一种!要结合锁使用。

模型分析

我的golang笔记

条件变量使用的函数

  • wait 方法

    func (c *Cond) Wait()
    1. 阻塞等待条件变量满足

    2. 释放已经掌握的互斥锁。(解锁)。要求,在调用wait之前,先加锁。c.L.Lock()

      。。。。 阻塞等待条件变量满足

    3. 解除阻塞,重新加锁。

  • signal方法

    func (c *Cond) Signal()
    • 唤醒阻塞在条件变量上的 go程。
  • Broadcast方法

    func (c *Cond) Broadcast()
    • 唤醒阻塞在条件变量上的 所有 go程。

条件变量实现步骤:-- 生产者

  1. 创建条件变量 var cond sync.Cond

  2. 初始化条件变量的成员 L ,指定锁(互斥、读写) cond.L = new(sync.Mutex)

  3. 加锁 cond.L.Lock()

  4. 判断条件变量满足, 调用 wait

    for len(ch) == cap(ch) {        // 此处必须使用 for 循环判断 wait是否满足条件。
        cond.Wait()        // 1 2 3
    }
  5. 产生数据,写入公共区

  6. 唤醒阻塞在条件变量上的 对端。cond.Signal()

  7. 解锁 cond.L.UnLock()

waitGroup

  • 主go程等待子go程结束运行。

实现步骤

  1. 创建 waitGroup对象。 var wg sync.WaitGroup
  2. 添加 主go程等待的子go程个数。 wg.Add(数量)
  3. 在各个子go程结束时,调用 wg.Done()。 将主go等待的数量-1. defer wg.Done
    • 如果是实名子go程的话, 传地址。
  4. 在主go程中等待。 wg.wait()
func printer10(str string)  {
    for _, ch := range str {
        fmt.Printf("%c", ch)
        time.Sleep(time.Millisecond * 222)
    }
}

func person10(wg *sync.WaitGroup)  {
    // 在zigo程内部, 运行结束时, Done, 将主go等地的数量 -1
    defer wg.Done()
    printer10("hello")
}

func person11(wg *sync.WaitGroup)  {
    defer wg.Done()
    printer10("world")
}

func main()  {
    // 创建 waitGroup对象
    var wg sync.WaitGroup

    // 添加要等待的子go程个数
    wg.Add(2)

    go person10(&wg)        // 实名go程传参,必须传&
    go person11(&wg)

    // 主go程调用 wait等待
    wg.Wait()
*/
/*    for {
        ;
    }*//*
}*/

网络概述

协议

  • 一组通信规则。要求数据通信双方,在通信过程中,严格遵守。

典型协议

应用层:Http、ftp

传输层:TCP、UDP

网络层:IP、ICMP、IGMP

链路层:ARP、RARP

网络分层模型

  • OSI七层模型:
    • 物、数、网、传、会、表、应
  • TCP/IP 4层模型:
    • 链、网、传、应

网络数据通信过程:

数据封装:数据 —— 应用层(非必须) —— 传输层 —— 网络层 —— 链路层 —— 以太网

数据解封装:以太网 —— 链路层 —— 网络层 —— 传输层 —— 应用层(非必须) —— 数据

我的golang笔记

各层功能:

  • 链路层:

    • 从 设备 到 设备
    • 源mac —— 目标mac(不需要用户指定)
    • ARP协议: 借助IP,获取 MAC地址。 RARP: 借助 MAC地址,找到 IP地址。
  • 网络层:

    • 从 节点 到 节点(主机)
    • 源IP —— 目标IP(需要用户指定)
    • IP协议:通过IP地址,在网络环境中唯一标识一台主机。
      • 大小:4字节。 每一个字节取值范围 0-255
  • 传输层:

    • 从 进程 到 进程
    • 源port —— 目标port (需要用户指定)
    • TCP/UDP:通过端口号(port)在一台主机上 唯一标识一个 进程。
      • IP + port 在网络环境中,唯一标识一个应用(进程)—— socket
      • 大小:2字节。 2^16 = 65536 (0-65535)

我的golang笔记

  • 应用层:
    • 从数据封装 到 数据解封
    • 源应用协议 —— 目标应用协议。
    • 应用层协议可选。非必须。

socket编程

网络应用设计模型

C/S B/S
优点 缓存数据量大,协议选择灵活。 用户安全性高。开发工作量小。跨平台性较好
缺点 用户安全性低。开发工作量大。跨平台性差。 不能缓存大量数据。协议选择受限。功能受限。
应用场景 大型应用、游戏(提前缓存数据) 跨平台性要求较高。

socket

  • 在一次数据通信过程中 ,socket 必须 成对 出现。

  • socket :双向 全双工通信。(socket 即可以读又可以写。)

    • 双向 半双工通信。channel。
    • 单工通信:遥控器。
  • socket 内核实现:

我的golang笔记

TCP-CS-Server

实现流程

  1. 创建监听器 listener

  2. 启动监听,接收客户端连接请求 listener.Accept() --- conn

    for {

  3. conn.read 客户端发送的数据

  4. 使用、处理数据

  5. 回发 数据给客户端 write

  6. 关闭连接 Close

    }

  7. 客户端使用 nc (netcat)测试:

    • 语法:nc 服务器IP地址 服务器port

我的golang笔记

==使用的方法==

  1. Listen函数 —— 创建监听器

    func Listen(net, laddr string) (Listener, error)
    • 参数1:协议类型:tcp、udp (必须小写)

    • 参数2:服务器端的 IP: port (192.168.31.11:8000)

    • 返回值:成功创建的监听器

      type Listener interface {
       // Addr返回该接口的网络地址
       Addr() Addr
       // Accept等待并返回下一个连接到该接口的连接
       Accept() (c Conn, err error)
       // Close关闭该接口,并使任何阻塞的Accept操作都会不再阻塞并返回错误。
       Close() error
      }
  2. Accept 方法 —— 阻塞等待客户端连接

    func (listener *Listener) Accept() (c Conn, err error)
    • 返回值:成功与客户端建立的 连接conn(socket)

      type Conn interface {
       // Read从连接中读取数据
       // Read方法可能会在超过某个固定时间限制后超时返回错误,该错误的Timeout()方法返回真
       Read(b []byte) (n int, err error)
       // Write从连接中写入数据
       // Write方法可能会在超过某个固定时间限制后超时返回错误,该错误的Timeout()方法返回真
       Write(b []byte) (n int, err error)
       // Close方法关闭该连接
       // 并会导致任何阻塞中的Read或Write方法不再阻塞并返回错误
       Close() error
       // 返回本地网络地址
       LocalAddr() Addr
       // 返回远端网络地址
       RemoteAddr() Addr
          。。。
      }

TCP-CS-Client

实现流程:

  1. 创建与服务器的连接 net.Dial() -- conn(socket)

    for {

  2. 发送数据给服务器 conn.write

  3. 接收服务器回发的 数据 conn.read

  4. 关闭连接。

    }

使用的方法

  • Dial方法

    func Dial(network, address string) (Conn, error)
    • 参数1:使用的协议:udp、tcp

    • 参数2:服务器的 IP:port (127.0.0.1:8000)

    • 返回:conn 用于与服务器进行数据通信的 socket

      type Conn interface {
       // Read从连接中读取数据
       // Read方法可能会在超过某个固定时间限制后超时返回错误,该错误的Timeout()方法返回真
       Read(b []byte) (n int, err error)
       // Write从连接中写入数据
       // Write方法可能会在超过某个固定时间限制后超时返回错误,该错误的Timeout()方法返回真
       Write(b []byte) (n int, err error)
       // Close方法关闭该连接
       // 并会导致任何阻塞中的Read或Write方法不再阻塞并返回错误
       Close() error
       // 返回本地网络地址
       LocalAddr() Addr
       // 返回远端网络地址
       RemoteAddr() Addr
      }

单客户端和单服务器通信

我的golang笔记

服务器:包含 2 个socket。

  • 一个用于监听客户端连接事件 Listenter。

  • 另一个用户 与 客户端进行数据通信。conn

客户端:包含 1 个 socket

  • 用来与服务器进行数据通信。

并发服务器:

单服务器 多客户端(1:N)

思路分析

我的golang笔记

服务器包含: 主go程、子go程们。

  • 主go程:负责监听 客户端的 “连接” 事件。当有客户端连接上来 ,创建 子go程 与客户端进行通信
  • 子go程:负责与客户端进行实时数据交互。并发。

实现流程

  1. 主go程中,创建监听器 Listener

  2. 使用 for 监听 listener —— Accept() —— conn

  3. 当有客户端发送连接请求时,创建 子go程。 使用 conn 与客户端进行数据通信

  4. 封装、实现函数:完成单个子go程与客户端的 read、write —— handleConnet(conn)

    关闭当前go程 conn —— defer

    ​ for {

    1. 读取客户端的数据 conn.read()

    2. 获取客户端的 IP+port 。 conn.Remote().string()

    3. 处理数据 小——大 toUpper()

    4. 写回大写数据到客户端。

      }

判断关闭

  • 网络通信中,通过 read 返回值 为 0 判断对端关闭。

    n, err := conn.read(buf)
    if n == 0 {
        // 检查到 conn 对端关闭。
    }
    if err != nil && err != io.EOF {
        // 处理错误。
    }

实现流程:

  1. 创建 与服务器通信 socket —— net.Dial()

  2. 创建 子go程循环监听 用户输入

    go func () {
        for {
            os.Stdin.Read()
        }
    }()
  3. 将读到的数据写给 服务器 conn.Write

  4. 主go程 循环 读取服务器回发的 数据。

    for {
        conn.read()
    }
  5. 将读到的数据打印到屏幕。

  6. 将 读到 的 “exit” 做为正常关闭连接标记。

    1. 在 服务器端,添加 “exit” 判断。 结束位置,包含 ‘\n’
    2. if string(buf[:n]) == “exit\n”

TCP通信过程

三次握手:建立连接

  1. 主动发起连接请求端(客户端), 发SYN标志位,携带 序号。

  2. 被动接收连接请求端(服务器), 回复 ACK标志位 携带 确认序号, 发SYN标志位,携带 序号。

  3. 主动发起连接请求端(客户端),发送ACK标志位 携带 确认序号。

    —— 第2个ACK发送完成。标志 三次握手完成。连接建立成功。

    —— 服务器:Accept 函数返回。

    —— 客户端:Dial 函数返回。

我的golang笔记

四次挥手:断开连接

  1. 主动关闭连接请求端(客户端), 发 FIN 标志位,携带序号。

  2. 被动关闭连接请求端(服务器),接收FIN标志位,发送 ACK 标志位。携带 确认序号。

    ​ —— 客户端 “半关闭” 完成。底层依赖 内核实心socket原理。

  3. 被动关闭连接请求端(服务器),发送 FIN 标志位, 携带序号。

  4. 主动关闭连接请求端(客户端),接收FIN标志位,发送 ACK 标志位。携带 确认序号。

    ​ —— 最后一个ACK被 被动关闭连接请求端 接收到,标志着,4次挥手完成。关闭连接完成。

  • 滑动窗口:通知通信的对端,本地剩余存储空间的大小。

我的golang笔记

TCP状态转换

我的golang笔记

  • 主动发起连接 (客户端):

    CLOSED —— 发送 SYN —— SYN_SENT —— 接收 ACK 、SYN ,发送ACK —— ESTABLISHED(数据通信)

  • 主动关闭连接(客户端):

    ESTABLISHED —— 发送 FIN —— FIN_WAIT_1 —— 接收 ACK —— ==FIN_WAIT_2 (半关闭)== —— 接收 FIN、发送ACK —— TIME_WAIT —— 等待 2MSL 时长 —— CLOSED

    • 2MSL作用: 确保 断开连接的 最后一个 ACK能被对端收到。等待一个固定时长(约40s)
    • FIN_WAIT2、TIME_WAIT、2MSL 只出现在 “主动端”
  • 被动接收连接 (服务器):

    CLOSED —— LISTEN —— 接收 SYN,发送 ACK、SYN —— SYN_RCVD —— 接收 ACK —— ESTABLISHED

  • 被动断开连接(服务器):

    ESTABLISHED —— 接收 FIN ,发送ACK —— CLOSE_WAIT (对应半关闭的 FIN_WAIT_2) —— 发送 FIN —— LAST_ACK —— 接收 最后 ACK —— CLOSED

  • 查看 网络通信状态 的命令 netstat

    • 语法1: netstat -apn | grep 关键字(端口号)
    • 语法2: lsof -i: 端口号

UDP通信

  • TCP:面向连接的可靠的 流式数据包传递。 流式。 对不稳定的网络层做完全弥补。借助回执,丢包重传。
  • UDP:无连接的 不可靠的 报文传输。报式。 对不稳定的网络层,直接还原真实状态。丢包不处理。
TCP UDP
优点 稳定(顺序、速度)、可靠 传输效率高、系统资源占用少、程序实现简单
缺点 传输效率低、系统资源占用多、程序实现复杂 不稳定(顺序、速度)、不可靠
使用场景 对数据的准确性、稳定性要求较高场合。上传、下载。 对数据实时性要求较高,可以适当允许数据丢失场合。游戏、视频、电话会议

UDP-CS-Server

实现流程:

  1. 获取UDP地址结构 —— UDPAddr

    func ResolveUDPAddr(net, addr string) (*UDPAddr, error)
    • 参数1:“udp”

    • 参数2:“127.0.0.1:8000” IP+port

    • 返回值:UDP地址结构

      type UDPAddr struct {
       IP   IP
       Port int
       Zone string // IPv6范围寻址域
      }
  2. 绑定地址结构体,得到通信套接字 —— udpConn

    func ListenUDP(net string, laddr *UDPAddr) (*UDPConn, error)
    • 参数1:“udp”
    • 参数2:ResolveUDPAddr 的返回值 : UDPAddr
    • 返回值:用于通信的 socket
  3. 接收客户端发送数据 —— ReadFromUDP() —— n,cltAddr, err

    func (c *UDPConn) ReadFromUDP(b []byte) (n int, addr *UDPAddr, err error)
    • 参数1:缓冲区
    • 返回值:
      • n:读到的实际字节数
      • addr:对端的 地址结构(IP+Port)
  4. 写数据给客户端 —— WriteToUDP(数据,cltAddr) —— n, err

    func (c *UDPConn) WriteToUDP(b []byte, addr *UDPAddr) (int, error)
    • 参数1:实际写出的数据
    • 参数2:对端的地址
    • 返回值:实际写出的字节数。

UDP-CS-Client

  • 实现流程:

    1. 创建用于与服务器通信的 套接字 conn

      net.Dial("udp", 服务器的IP+prot)   --- conn
    2. 后续代码实现,参照 TCP-CS-Client。

UDP-CS-并发server

  • UDP服务器,默认支持客户端并发访问。

网络文件传输

获取文件属性函数stat

func Stat(name string) (FileInfo, error) 

type FileInfo interface { Name() string

Size() int64

​ Mode() FileMode

​ ModTime() time.Time

​ IsDir() bool

​ Sys() interface{}

}

  • 命令行参数

    • 在main函数启动时,向整个程序传参。

    • 语法: go run xxx.go argv1 argv2 argv3 argv4 。。。。

      xxx.exe: 第 0 个参数。
      argv1 :第 1 个参数。
      argv2 :第 2个参数。    
      argv3 :第 3 个参数。
      argv4 :第 4 个参数
    • 使用: list := os.Args —— for idx, str := range list { fmt.println() 可以遍历 list }

    ​ 参数 = list[3]

发送端 (客户端)

实现流程:

  1. 获取main命令行参数,得到文件 绝对路径 filePath
  2. 使用stat获取文件属性,得到 文件名(不带路径) fileName
  3. 与服务器建立连接 net.Dial(“tcp”, 服务器IP+port) ——conn
  4. conn.Write() 将文件名写给接收端(服务器)
  5. 读取接收端回发的 “ok”, 并判断。
  6. 封装、实现、调用函数。SendFile( 带路径文件名,conn )
    1. 只读 打开文件 Open - f
    2. 创建 缓冲区 存文件内容。buf
    3. 循环 读取文件内容,写给服务器。 读多少,写多少。
    4. 判断 f 结束 n == 0 。
    5. 关闭文件、conn。

接收端(服务器)

实现流程:

  1. 创建监听器 Listener

  2. 启动监听 Accept() -- conn

  3. 读取客户端发送的文件名,存储。

  4. 回发 “ok”

  5. 封装、实现、调用函数。RecvFile( 文件名,conn )

    1. 创建新文件 os.Create() - f
    2. 创建 缓冲区 存conn读到的文件内容。buf
    3. 循环写入本地文件
    4. 判断conn结束 n == 0 。
    5. 关闭文件、conn

Http概述

  • WEB工作方式:

    • DNS服务器:转换 域名对应的 IP地址。
    • 访问WEB服务器流程:访问 DNS服务器 —— IP地址 —— WEB服务器(http服务器)
  • Http协议(明文):

    • 超文本传输协议。WEB通信,使用基本协议。
    • https协议(加密): SSL、TLS (网景)
  • URL:

    • 统一资源定位符。 定位到网络中某一个服务器上的某一个数据资源。
    • 大致格式:协议(http)服务器名称(IP、域名)路径名、文件名。

HTTP协议格式

http请求协议格式

  1. 请求行:请求方法 “空格” 请求URL “空格” 协议及版本号 e.g. GET /itcast.txt HTTP/1.1
  2. 请求头:格式: key :value
  3. 空行:表示请求头结束。只使用 \r\n
  4. 请求包体:是否含有包体,取决于请求方法。 POST 方法含有包体。 GET 不含有包体。

我的golang笔记

http应答协议格式

  1. 应答行:协议版本号 “空格” 状态码 “空格” 状态描述 e.g. HTTP/1.1 200 ok
  2. 应答头:格式: key :value
  3. 空行:表示应答头结束。只使用 \r\n
  4. 应答包包体
    1. 成功:请求的实际数据
    2. 失败:错误信息。(404 not found)

我的golang笔记

回调函数

  • 实现本质:函数指针
  • 函数指针语法:type 函数指针类型名 指向的函数原型(func 形参列表 返回值列表)。
    • type FuncP func(x string, y bool)int
  • 回调函数概念:
    • 用户自定义一个函数,不直接调用,当满足某一特定条件时,有函数指针完成调用,或者由系统自动调用。
package main

import (
    "fmt"
    "unsafe"
)

// 定义函数指针类型
type FUNCP func (x int, y bool) int // FUNCP 数据类型. int / string /bool

// 指向一类函数(有两个参数(int/bool) 有一个 int 类型返回值)
func useCallback(x int, y bool, p FUNCP) int {
    return p(x, y)
}

// 回调函数
func addOne(x int, y bool) int {
    if y == true {
        x += 1
    }
    return x
}

// 回调函数
func subTen(x int, y bool) int {
    if y == true {
        x -= 10
    }
    return x
}

func main() {
    //addOne(10, true)

    var p FUNCP
    p = addOne
    fmt.Printf("type=%T, size=%v\n", p, unsafe.Sizeof(p))

    ret := useCallback(10, true, p)
    fmt.Println("ret =", ret)

    ret2 := useCallback(20, true, subTen)
    fmt.Println("ret =", ret2)
}

Go语言实现的 HTTP服务器

注册回调函数

func HandleFunc(pattern string, handler func(ResponseWriter, *Request))
  • pattern:服务器提供给客户端访问 url(路径、文件)

  • handler 回调函数。—— 系统自动调用

    • func handler(w http.ResponseWriter, req *http.Request)
    • w: 回写给客户端的 数据(http协议)

    • req:读取到的客户端请求 信息

    type ResponseWriter interface {
       Header() Header            
       Write([]byte) (int, error)    
       WriteHeader(int)            
    }
    type Request struct {
        Method string        // 浏览器请求方法 GET、POST…
        URL *url.URL        // 浏览器请求的访问路径
        ……
        Header Header        // 浏览器请求头部
        Body io.ReadCloser    // 浏览器请求包体
        RemoteAddr string    // 浏览器地址
        ……
        ctx context.Context
    }
    

启动服务

func ListenAndServe(addr string, handler Handler) error
  • addr: 服务器地址结构(IP:port)
  • handler :回调函数。通常传 nil —— 自动调用默认回调函数。DefaultServeMux

html简介

<html>

<head>
<title> 404 not found </title>
</head>


<body>

​       <center>

​           <h1> 404 NOT FOUND </h1>

​       </center>

​       <hr> 

​       不好意思,你所请求的文件,不存在!

</body>

</html>

Go语言实现的 http 客户端

func Get(url string) (resp *Response, err error)
  • 参数:待请求的 url。必须添加 http://

  • 返回值:Response:

    • 服务器回复的应答包。(应答行、应答头、应答包体)

      type Response struct {
          Status     string // e.g. "200 OK"
          StatusCode int    // e.g.  200
          Proto      string // e.g. "HTTP/1.0"
          ……
          Header Header
          Body io.ReadCloser  // 指针类型 —— read、close
          ……
      }

【结论】:在字串中匹配带有匹配参考项的 子串时, 使用 匹配参考项起始(?s:(.*?))匹配参考项结尾


网页爬虫

  • 横向爬取:找寻网页的页与页之间的规律。分页器规律
  • 纵向爬取:找寻一个页面中,条目与条目规律。

爬取流程

  1. 明确 url。 找寻网页分页器规律

http://tieba.baidu.com/f?kw=周杰伦&ie=utf-8&pn=0 --- 1

http://tieba.baidu.com/f?kw=周杰伦&ie=utf-8&pn=50 ---- 2

http://tieba.baidu.com/f?kw=周杰伦&ie=utf-8&pn=100

http://tieba.baidu.com/f?kw=周杰伦&ie=utf-8&pn=150 —— 下一页 + 50

  1. 获取网页数据。http.Get(url)
  2. 过滤有用数据信息。 正则、MustCompile、FindallstringSubmath
  3. 存储、保存数据。

横向规律:

https://movie.douban.com/top250?start=0&filter= ---1

https://movie.douban.com/top250?start=25&filter= ---2

https://movie.douban.com/top250?start=50&filter= ---3

https://movie.douban.com/top250?start=75&filter= ---4 —— 下一页 +25

纵向规律:

  • 电影名称:

  • <img width="100" alt="(电影名称)"

  • 评分:

    <span class="rating_num" property="v:average">(分数)</span>

  • 评价人数:

    <span>(评价人数)人评价</span>

Go语言正则函数

  1. 编译解析正则表达式
regexp

func MustCompile(str string) *Regexp
  • 参数:正则表达式字符串。推荐使用 反引号 ``
  • 返回:编译后,go编译器能识别的 正则表达式 (struct)
  1. 利用正则,从字符串中,提取有用信息。
func (re *Regexp) FindAllStringSubmatch(s string, n int) [][]string
  • 参数1:需要过滤的 源字符串。

  • 参数2:匹配次数。 -1 全部。

  • 返回值:匹配成功的子串

                匹配项中,行、列。
    
                列【0】:带有 匹配项 
    
                列【1】:不带 匹配项 —— 使用!

默认情况下 “.” 能匹配任意一个字符,不能匹配 ‘\n’

(?s:(.*?)):

    ?s:  单行模式。  使 “.” 可以匹配 ‘\n’

    .*?: 匹配 “.” , 越少越好匹配方法。
匹配 从 <div> 到 </div> 中间的数据

<div>xxx</div><div>yyy</div><div>zzz</div><div>qqq</div>
  1        2    3       4     5        6    7        8

越多越好: 1-8
越少越好: 1-2

实现流程:

  1. 提示用户输入爬取起始、终止页start、end

  2. 跟据start、end 循环获取 url 下一页+25

  3. 封装、调用函数 HttpGET()获取一个网页全部数据,返回

    1. resp = http.Get() 获取web服务器的 http应答包
    2. defer resp.Body.Close()
    3. 创建缓冲区buf,循环读取body 存入。
    4. 创建 result 拼接 成 string 返回。
  4. 使用正则筛选数据

    1. 编译解析正则 mustCompile
    2. 提取数据
    3. 重复 12步,分别获取 电影名、评分、评价人数
  5. 保存成文件 Save2file(电影名、评分、评价人数 [][]string)

    1. 创建新文件 os.Create()
    2. 写 标题 : 电影名 \t\t 评分 \t\t 评价人数
    3. 循环 [][]string 依次按标题顺序,写入 数据。
  6. 起并发

    1. 创建 channel -- quit
    2. 封装一个网页的爬取步骤到 SpiderPage 函数中。
    3. go SpiderPage () 并发
    4. 使用 quit 协调主子go程 退出先后顺序(有N次写,有N次读)

图片下载

http://desk.zol.com.cn/bizhi/7407_91896_2.html        --1

http://desk.zol.com.cn/bizhi/7407_91897_2.html        --2

http://desk.zol.com.cn/bizhi/7407_91898_2.html        --3

http://desk.zol.com.cn/bizhi/7407_91899_2.html        --4


<img id="bigImg" src="==url==”

Json

序列化和反序列化

  • 序列化:将数据转化为 字符串(json)类型存储
  • 反序列化:解析 json 字符串,还原回成对应数据类型。
  • 必要性:
    1. 数据发送端与接受端存在 平台差异(32位、64位)。
    2. 本机字节序、网络字节序存在 存储方式差异(大、小端法)

json 语法:

  1. key:value:

    • key:必须是 字符串,建议使用 “ ”
    • value:任意类型:
      • 整型
      • 浮点型
      • bool型
      • null 空类型
      • 对象 {}
      • 数组 []
  2. 对象 {} :

    成员可以是 :key:value 对,对象,数组。 用 “,”隔分。

  3. 数组 []:

    成员可以是 :key:value 对,对象,数组。 用 “,”隔分。

{ "Name":"张三丰", "Age":180, "Marray":false, "家电":[{"剃须刀":["吉列","飞利浦"]}, {"电视机":["三星"]}] }

在线工具:www.json.cn

序列化

enconding/json
func Marshal(v interface{}) ([]byte, error)
  • 参数:待序列化的 数据
  • 返回值:json串

结构体序列化

  • 要求 结构体类型名 和 成员(属性)名 必须大写(包作用域), 才能在 json包中可见。
  • 否则,不报错!!! 但不做序列化转换。

{"Name":"张三","Age":29,"Score":98.9,"Id":1001}

map序列化

  • 定义的map变量,不使用 make 初始化,不能存储数据。

{"Age":19,"Id":9527,"Name":"华安","food":["鸡翅膀","含笑半步癫","一日丧命散"]}

Slice序列化

  • 定义 slice 变量,可以不初始化,直接使用 append 向其中,添加数据。

[{"Age":19,"Id":9527,"Name":"华安","food":["鸡翅膀","含笑半步癫","一日丧命散"]},{"Age":16,"Id":521,"Name":"秋香","food":["鸡翅膀","一日丧命散"],"教师":{"Name":"石榴姐","Age":30,"Addr":"华府"}}]

结构体标签

  • 用途:在序列化时,重新指定 json key 值

  • 语法:使用 反引号 `` 包裹整个结构体标签。 key : value

    • key : json、自定义key
    • value:必须使用 “ ” 包裹 value 值。 ==否则,Tag标签不起作用,且不产生错误!==
      • value中,如果有多个值,使用“,”隔分,==不能随意添加空格==
    type Student5 struct {
        Name  string  `json:"-"`    // "-"作用,不进行序列化.显示结果与小写成员名一致.
        Age   int     `json:"age,omitempty"`    // 序列化时, 忽略0值 / 空值.
        Score float64 `json:"score,string"`        // 序列化时, 转换为 string
        Id    int      `json:"id"`
    }
    输出结果:{"score":"99.9","id":1002}

反序列化

func Unmarshal(data []byte, v interface{}) error
  • 参数1:序列化后生产的json串
  • 参数2:用来传出反序列化后的 原始数据。使用时 传指针
  • 返回值:一旦err有数据,说明反序列化失败。

结构体反序列化

  • 用接收反序列化的 结构体类型,必须与原序列化的结构体类型 严格一致(成员个数、类型)
  • Unmarshal(&struct)

map反序列化

  • 如果接收反序列化结果的 map 没有开辟空间,Unmarshal 函数,帮助自动开辟。

  • 虽然 map 数据类型为“引用”,但使用 Unmarshal函数时,必须要&传参。否则报错。

  • Unmarshal(&map)

slice反序列化

  • Unmarshal(&slice)

网络聊天室

聊天室模块划分

主go程(服务器)

监听客户端连接请求、启动go程(HandleConnet)与客户端通信、创建 Manger go程(管理全局Message)

HandleConnet go程

​ 添加新用户到在线用户列表;组织用户上线消息,写给全局Message;创建 监听用户自带channel的go程(WriteMsgToClient);发送用户聊天消息、查询在线用户、修改用户名、用户下线、超时强踢。

Manager 管理者 go程

​ 监听全局Message上是否有数据(读)。遍历在线用户列表,将读到数据写给用户自带channel。

WriteMsgToClient go程

​ 监听 用户自带channel 上是否有数据可读。读到写给用户。

其他资源

  • 用户结构体 Client { Name, IP+port, channel } 新用户 Name == IP+port
  • 全局 channel (Message)。 所有需要 “广播” 的消息,写入本 channel
  • 全局在线用户列表。onlineMap key:IP+port value:Client

广播用户上线

图示分析

我的golang笔记

实现流程

  1. 创建监听器 net.listen() --- listener

  2. for 循环监听客户端连接 --- listener.Accept() --- conn

  3. 创建全局 Manager go程, 读全局channel , 遍历在线用户

    1. 创建客户端结构体 Client {Name、Addr、C}

    2. 创建全局 channel -- Message

    3. 创建 在线用户列表 onlineMap key:IP+port(string) value : Client

    4. 实现 Manager

      for {

      1. 读全局 <-Message。 无数据, 阻塞; 有数据,继续向下遍历
      2. 遍历 onlineMap。 将 读到的消息,写给 用户自带的 C

      }

  4. 创建、实现、调用子go程 handleConnet(conn)处理客户端事件

    1. defer conn.Close() // 每个客户端结束时,关闭对应socket

    2. 获取客户端地址结构 conn.Remoteaddr().String()

    3. 初始化客户端对象。 Name == Addr == IP + port

    4. 添加新用户 到 全局 在线用户列表

    5. 创建、实现、调用 go程 WriteMsgToClient(clt, conn) 监听用户自带 C 上是否有数据

      for {

      1. 读用户自带 msg := <-clt.C。 无数据, 阻塞; 有数据,继续向下写给客户端
      2. conn.Write(msg)

      }

    6. 组织用户上线消息。loginMsg := "[" + clt.Addr + "]" + clt.Name + ":" + "上线!"

    7. 写给 全局Message 。 广播!!!

    8. 添加测试代码,防止当前go程退出。 for { runtime.GC() }

添加读写锁 —— 保护在线用户列表

  1. 全局位置创建 rwMutex
  2. 在 Manager go程中,遍历在线用户列表前,加读锁
  3. 遍历结束 ,解读锁。
  4. 在 handleConnet go程中,添加新用户 到 全局 在线用户列表 前 ,加 写锁
  5. 添加结束, 解写锁。

广播用户聊天消息

实现流程:

  1. 在 handleConnet go程中,完成上线广播后,创建 匿名go程,用来读取客户端消息。
  2. for 循环读取客户端数据。--- buf
  3. 判断关闭、判断异常
  4. 封装 makeMsg(clt, str)string 组织消息格式。(改写之前用户上线消息)
  5. 使用 makeMsg 组织用户聊天信息。
  6. 写入 全局Message 。 广播!!!

查询在线用户列表

实现流程:

  1. 截取用户信息,去除最后一个字符‘\n’.。 保存成 msg
  2. 读取数据后,判断 用户是否 发送 “who” 指令。如果不是,广播聊天消息。
  3. 变量全局在线用户列表。
  4. 组织在线用户信息
  5. 写给当前用户。不广播!!!conn.Write
  6. 添加读写锁的 读加锁、读解锁。保护共享 全局在线用户列表。

修改用户名

实现流程:

  1. 判断 用户是否 发送 “rename|” 指令。 如果不是,广播聊天消息。
  2. 利用 && 短路运算特性。先判断 len(msg) > 7 再判断 msg[:7] == “rename|”
    • 如果先判断 msg[:7], 会修改msg切片数据。
  3. 使用 split 函数,按 “|” 拆分用户信息,获取新用户名
  4. 更新用户结构体,新名覆盖旧名。
  5. 更新 在线用户列表。更新含有新用户名的用户。
  6. 通知当前用户改名成功。不广播!!!conn.Write。
  7. 添加 写锁、保护在线用户列表。

用户下线:

实现流程:

  1. 在 handleConnet go程中,匿名go程之前。 创建 channel 管理用户下线。isQuit
  2. conn.Read == 0 时,确认用户下线, isQuit <- true。 return 退出当前匿名go程
  3. 在 handleConnet go程结尾的 for 中,添加 select (替换 runtime.GC())
  4. 在 一个 Case 中监听 <-isQuit :
    1. 关闭 用户自带 channel ,促使 WriteMsgToClient go程结束。
    2. 将 WriteMsgToClient go程 内读取用户 channel 的 for 改为 for range 遍历 C
    3. 从在线用户列表中,删除当前用户。delete()
    4. 组织用户下线消息。广播!写给 全局Message 。
    5. return 退出 handleConnet go程
    6. 使用写锁,保护在线用户列表

用户超时强踢

目的:长时间连接服务器,不发送消息的 客户端,踢除,释放服务器资源。

实现流程:

  1. 添加select 的 case 分支,监听 time.After()

  2. 当定时满时,释放用户资源:

    1. 关闭 用户自带 channel ,促使 WriteMsgToClient go程结束。
    2. 将 WriteMsgToClient go程 内读取用户 channel 的 for 改为 for range 遍历 C
    3. 从在线用户列表中,删除当前用户。delete()
    4. 组织用户下线消息。广播!写给 全局Message 。
    5. return 退出 handleConnet go程
    6. 使用写锁,保护在线用户列表
  3. 在 handleConnet go程中,匿名go程之前。 创建 channel 重置定时器。isLive

  4. 添加 select 的 case 分支,监听 <-isLive 。 当isLive上有写入时,定时器会重新计时。

  5. 在用户 “广播聊天”、“查询在线用户who”、“修改用户名 rename” 任意一个分支 之后,添加 isLive <-true

收藏
评论区

相关推荐

关于Golang的那些事(一) -- Node.js和Golang对比
之前一直用Node.js作为开发语言,用了差不多4年的Node.js,涉及前端和后端,最近看到Golang这个新兴之秀挺火的,于是想探究探究一下这门语言,对比了一下他们的Github repo,截止现在Node.js的repo有72.5K星, issue数量是859个,Golang的repo有75.7K星,issue数量是5K个。从趋势来看,Golang来势
golang 中神奇的 slice
声明:本文仅限于简书发布,其他第三方网站均为盗版,原文地址: golang 中神奇的 slice(https://links.jianshu.com/go?tohttps%3A%2F%2Fliqiang.io%2Fpost%2Fimagesliceingolang) 在 golang 中,似乎人们都不太喜欢使用 Linked List,甚至于原
godoc 命令和 golang 代码文档管理
介绍 godoc 是 golang 自带的文档查看器,更多的提供部署服务 go doc 和 godoc 在 golang 1.13 被移除了,可以自行安装 golang.org go1.13 godoc(https://links.jianshu.com/go?tohttps%3A%2F%2Fgolang.org%2Fdoc%2Fg
Mac安装Golang和vscode
Mac第一次安装golang和vscode一起使用,遇到了不少的坑,下面介绍一下正确的安装方式。 1、使用brew安装Golang 如果不知道brew是什么,或怎么安装请看这里 brew官网(https://brew.sh/index_zhcn) brew install golang 安装完成后可以使用
Golang中常用的字符串操作
Golang中常用的字符串操作 一、标准库相关的Package go import( "strings" ) 二、常用字符串操作 1. 判断是否为空字符串 1.1 使用“”进行判断 思路:直接判断是否等于""空字符串,由于Golang中字符串不能为 nil,且为值类型,所以直接与空字符串比较即可。 举例: go
golang 分析调试高阶技巧
layout: post title: “golang 调试高阶技巧” date: 2020603 1:44:09 0800 categories: golang GC 垃圾回收 golang 高阶调试 Golang tools nm compile
Golang duck typing(鸭子类型)的概念
“像鸭子走路,像鸭子叫(长得像鸭子),那么就是鸭子” 描述事物的外部行为而非内部结构 严格说go属于结构化类型系统,类似dock typing 先看一个其他语言中的duck typing : python中的duck typing def download(retriever): return retriever
深入理解 Go Slice
(https://imghelloworld.osscnbeijing.aliyuncs.com/0ce8a8773a658d4b843e5796a0dbf001.png) image 原文地址:深入理解 Go Slice(https://github.com/EDDYCJY/blog/blob/master/golang/pkg/20
golang包循环引用的几种解决方案
golang包循环引用的几种解决方案 发表于2020年11月2日2020年11月3日(https://libuba.com/2020/11/02/golang%e5%8c%85%e5%be%aa%e7%8e%af%e5%bc%95%e7%94%a8%e7%9a%84%e5%87%a0%e7%a7%8d%e8%a7%
我的golang笔记
面向对象思想 面向对象简介 编程思想 与编程语言无关。 C语言、Go中的 结构体 就是后来面向对象编程语言中的类。 面向对象编程:高内聚,低耦合。 特性 继承 —— 匿名字段(实名字段) 封装 —— 方法 多态 —— 接口(interface) Go 语言是典型的面向对象编程语言。 通过程序描述对象 创建类(指定类属性) 类属性:静
我的golang基础
库查询 https://gowalker.org/你应该$HOME/.profile文件增加下面设置。 搭建go的环境 step1:去golang的官网下载go的安装包 windows:go1.9.2.....msi mac:go1.9.2......pkg 双击傻瓜式安装 linux:go1.9.2.linuxamd64.tar.gz 默认到下
now扩展-go的时间工具箱
关于我golang不像C,Java这种高级语言,有丰富的语法糖供开发者很方便的调用。所以这便催生出很多的开源组件,通过使用这些第三方组件能够帮助我们在开发过程中少踩很多的坑。时间处理是所有语言都要面对的一个问题,parse根据字符串转为date类型,tostring()将date类型转为定制化的字符串。在实际使用过程中,parse
go好用的类型转换第三方组件
关于我 通过学习和分享的过程,将自己工作中的问题和技术总结输出,希望菜鸟和老鸟都能通过自己的文章收获新的知识,并付诸实施。 Cast介绍 Cast是什么?Cast是一个库,以一致和简单的方式在不同的go类型之间转换。Cast提供了简单的函数,可以轻松地将数字转换为字符串,将接口转换为bool类型等等。当一个明显的转换是可能的时,Cast会智
Android如何解析json字符串
前言上一篇文章介绍了服务器用Golang如何解析json字符串,今天我们来看看Android客户端是如何解析json字符串的。 正文Golang如何解析post请求中的json字符串(https://www.helloworld.net/p/O917HGeiALU2D)使用java语句如何正确解析json字符串呢?举一个例子,假如我们想从rtc_i
golang - DES加密ECB(模式)
Java默认DES算法使用DES/ECB/PKCS5Padding,而golang认为这种方式是不安全的,所以故意没有提供这种加密方式,那如果我们还是要用到怎么办?下面贴上golang版的DES ECB加密解密代码(默认对密文做了base64处理)。