随笔 - 134,  文章 - 0,  评论 - 0,  阅读 - 21320

GO(四)

面向对象

匿名字段

go支持只提供类型而不写字段名的方式,也就是匿名字段,也称为嵌入字段

package main

import "fmt"

//    go支持只提供类型而不写字段名的方式,也就是匿名字段,也称为嵌入字段

//人
type Person struct {
    name string
    sex  string
    age  int
}

type Student struct {
    Person
    id   int
    addr string
}

func main() {
    // 初始化
    s1 := Student{Person{"5lmh", "man", 20}, 1, "bj"}
    fmt.Println(s1)

    s2 := Student{Person: Person{"5lmh", "man", 20}}
    fmt.Println(s2)

    s3 := Student{Person: Person{name: "5lmh"}}
    fmt.Println(s3)
}

输出结果:

    {{5lmh man 20} 1 bj}
    {{5lmh man 20} 0 }
    {{5lmh  0} 0 }

同名字段的情况

package main

import "fmt"

//人
type Person struct {
    name string
    sex  string
    age  int
}

type Student struct {
    Person
    id   int
    addr string
    //同名字段
    name string
}

func main() {
    var s Student
    // 给自己字段赋值了
    s.name = "5lmh"
    fmt.Println(s)

    // 若给父类同名字段赋值,如下
    s.Person.name = "枯藤"
    fmt.Println(s)
}

输出结果:

    {{  0} 0  5lmh}
    {{枯藤  0} 0  5lmh}

所有的内置类型和自定义类型都是可以作为匿名字段去使用

package main

import "fmt"

//人
type Person struct {
    name string
    sex  string
    age  int
}

// 自定义类型
type mystr string

// 学生
type Student struct {
    Person
    int
    mystr
}

func main() {
    s1 := Student{Person{"5lmh", "man", 18}, 1, "bj"}
    fmt.Println(s1)
}

输出结果:

    {{5lmh man 18} 1 bj}

指针类型匿名字段

package main

import "fmt"

//人
type Person struct {
    name string
    sex  string
    age  int
}

// 学生
type Student struct {
    *Person
    id   int
    addr string
}

func main() {
    s1 := Student{&Person{"5lmh", "man", 18}, 1, "bj"}
    fmt.Println(s1)
    fmt.Println(s1.name)
    fmt.Println(s1.Person.name)
}

输出结果:

    {0xc00005c360 1 bj}
    zs
    zs

Go语言接口

接口(interface)定义了一个对象的行为规范,只定义规范不实现,由具体的对象来实现规范的细节。

接口是一种类型

Go语言提倡面向接口编程。

    接口是一个或多个方法签名的集合。
    任何类型的方法集中只要拥有该接口'对应的全部方法'签名。
    就表示它 "实现" 了该接口,无须在该类型上显式声明实现了哪个接口。
    这称为Structural Typing。
    所谓对应方法,是指有相同名称、参数列表 (不包括参数名) 以及返回值。
    当然,该类型还可以有其他方法。

    接口只有方法声明,没有实现,没有数据字段。
    接口可以匿名嵌入其他接口,或嵌入到结构中。
    对象赋值给接口时,会发生拷贝,而接口内部存储的是指向这个复制品的指针,既无法修改复制品的状态,也无法获取指针。
    只有当接口存储的类型和对象都为nil时,接口才等于nil。
    接口调用不会做receiver的自动转换。
    接口同样支持匿名字段方法。
    接口也可实现类似OOP中的多态。
    空接口可以作为任何类型数据的容器。
    一个类型可实现多个接口。
    接口命名习惯以 er 结尾。

每个接口由数个方法组成,接口的定义格式如下:

    type 接口类型名 interface{
        方法名1( 参数列表1 ) 返回值列表1
        方法名2( 参数列表2 ) 返回值列表2
        …
    }

其中:

    1.接口名:使用type将接口定义为自定义的类型名。Go语言的接口在命名时,一般会在单词后面添加er,如有写操作的接口叫Writer,有字符串功能的接口叫Stringer等。接口名最好要能突出该接口的类型含义。
    2.方法名:当方法名首字母是大写且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问。
    3.参数列表、返回值列表:参数列表和返回值列表中的参数变量名可以省略。

举个例子:

type writer interface{
    Write([]byte) error
}

当你看到这个接口类型的值时,你不知道它是什么,唯一知道的就是可以通过它的Write方法来做一些事情。

Go 语言提供了另外一种数据类型即接口,它把所有的具有共性的方法定义在一起,任何其他类型只要实现了这些方法就是实现了这个接口。

/* 定义接口 */
type interface_name interface {
   method_name1 [return_type]
   method_name2 [return_type]
   method_name3 [return_type]
   ...
   method_namen [return_type]
}

/* 定义结构体 */
type struct_name struct {
   /* variables */
}

/* 实现接口方法 */
func (struct_name_variable struct_name) method_name1() [return_type] {
   /* 方法实现 */
}
...
func (struct_name_variable struct_name) method_namen() [return_type] {
   /* 方法实现*/
}

例子

package main

import (
    "fmt"
)

type Phone interface {
    call()
}

type NokiaPhone struct {
}

func (nokiaPhone NokiaPhone) call() {
    fmt.Println("I am Nokia, I can call you!")
}

type IPhone struct {
}

func (iPhone IPhone) call() {
    fmt.Println("I am iPhone, I can call you!")
}

func main() {
    var phone Phone

    phone = new(NokiaPhone)
    phone.call()

    phone = new(IPhone)
    phone.call()

}

在上面的例子中,我们定义了一个接口Phone,接口里面有一个方法call()。然后我们在main函数里面定义了一个Phone类型变量,并分别为之赋值为NokiaPhone和IPhone。然后调用call()方法,输出结果如下:

I am Nokia, I can call you!
I am iPhone, I can call you!

实现接口的条件

一个对象只要全部实现了接口中的方法,那么就实现了这个接口。换句话说,接口就是一个需要实现的方法列表。

我们来定义一个Sayer接口:

// Sayer 接口
type Sayer interface {
    say()
}

定义dog和cat两个结构体:

type dog struct {}

type cat struct {}

因为Sayer接口里只有一个say方法,所以我们只需要给dog和cat 分别实现say方法就可以实现Sayer接口了。

// dog实现了Sayer接口
func (d dog) say() {
    fmt.Println("汪汪汪")
}

// cat实现了Sayer接口
func (c cat) say() {
    fmt.Println("喵喵喵")
}

接口的实现就是这么简单,只要实现了接口中的所有方法,就实现了这个接口。

接口类型变量

那实现了接口有什么用呢?

接口类型变量能够存储所有实现了该接口的实例。 例如上面的示例中,Sayer类型的变量能够存储dog和cat类型的变量。

func main() {
    var x Sayer // 声明一个Sayer类型的变量x
    a := cat{}  // 实例化一个cat
    b := dog{}  // 实例化一个dog
    x = a       // 可以把cat实例直接赋值给x
    x.say()     // 喵喵喵
    x = b       // 可以把dog实例直接赋值给x
    x.say()     // 汪汪汪
}

类似java的实现关系的写法但是定义和使用分开了

值接收者和指针接收者实现接口的区别

使用值接收者实现接口和使用指针接收者实现接口有什么区别呢?接下来我们通过一个例子看一下其中的区别。

我们有一个Mover接口和一个dog结构体。

type Mover interface {
    move()
}

type dog struct {}

值接收者实现接口

func (d dog) move() {
    fmt.Println("狗会动")
}

此时实现接口的是dog类型:

func main() {
    var x Mover
    var wangcai = dog{} // 旺财是dog类型
    x = wangcai         // x可以接收dog类型
    var fugui = &dog{}  // 富贵是*dog类型
    x = fugui           // x可以接收*dog类型
    x.move()
}

从上面的代码中我们可以发现,使用值接收者实现接口之后,不管是dog结构体还是结构体指针*dog类型的变量都可以赋值给该接口变量。因为Go语言中有对指针类型变量求值的语法糖,dog指针fugui内部会自动求值*fugui

指针接收者实现接口

同样的代码我们再来测试一下使用指针接收者有什么区别:

func (d *dog) move() {
    fmt.Println("狗会动")
}
func main() {
    var x Mover
    var wangcai = dog{} // 旺财是dog类型
    x = wangcai         // x不可以接收dog类型
    var fugui = &dog{}  // 富贵是*dog类型
    x = fugui           // x可以接收*dog类型
}

此时实现Mover接口的是*dog类型,所以不能给x传入dog类型的wangcai,此时x只能存储*dog类型的值

一个类型实现多个接口

一个类型可以同时实现多个接口,而接口间彼此独立,不知道对方的实现。 例如,狗可以叫,也可以动。我们就分别定义Sayer接口和Mover接口,如下: Mover接口。

// Sayer 接口
type Sayer interface {
    say()
}

// Mover 接口
type Mover interface {
    move()
}

dog既可以实现Sayer接口,也可以实现Mover接口。

type dog struct {
    name string
}

// 实现Sayer接口
func (d dog) say() {
    fmt.Printf("%s会叫汪汪汪\n", d.name)
}

// 实现Mover接口
func (d dog) move() {
    fmt.Printf("%s会动\n", d.name)
}

func main() {
    var x Sayer
    var y Mover

    var a = dog{name: "旺财"}
    x = a
    y = a
    x.say()
    y.move()
}

多个类型实现同一接口

Go语言中不同的类型还可以实现同一接口 首先我们定义一个Mover接口,它要求必须由一个move方法。

// Mover 接口
type Mover interface {
    move()
}

例如狗可以动,汽车也可以动,可以使用如下代码实现这个关系:

type dog struct {
    name string
}

type car struct {
    brand string
}

// dog类型实现Mover接口
func (d dog) move() {
    fmt.Printf("%s会跑\n", d.name)
}

// car类型实现Mover接口
func (c car) move() {
    fmt.Printf("%s速度70迈\n", c.brand)
}

这个时候我们在代码中就可以把狗和汽车当成一个会动的物体来处理了,不再需要关注它们具体是什么,只需要调用它们的move方法就可以了。

func main() {
    var x Mover
    var a = dog{name: "旺财"}
    var b = car{brand: "保时捷"}
    x = a
    x.move()
    x = b
    x.move()
}

上面的代码执行结果如下:

    旺财会跑
    保时捷速度70迈

并且一个接口的方法,不一定需要由一个类型完全实现,接口的方法可以通过在类型中嵌入其他类型或者结构体来实现。

// WashingMachine 洗衣机
type WashingMachine interface {
    wash()
    dry()
}

// 甩干器
type dryer struct{}

// 实现WashingMachine接口的dry()方法
func (d dryer) dry() {
    fmt.Println("甩一甩")
}

// 海尔洗衣机
type haier struct {
    dryer //嵌入甩干器
}

// 实现WashingMachine接口的wash()方法
func (h haier) wash() {
    fmt.Println("洗刷刷")
}

接口嵌套

接口与接口间可以通过嵌套创造出新的接口。

// Sayer 接口
type Sayer interface {
    say()
}

// Mover 接口
type Mover interface {
    move()
}

// 接口嵌套
type animal interface {
    Sayer
    Mover
}

嵌套得到的接口的使用与普通接口一样,这里我们让cat实现animal接口:

type cat struct {
    name string
}

func (c cat) say() {
    fmt.Println("喵喵喵")
}

func (c cat) move() {
    fmt.Println("猫会动")
}

func main() {
    var x animal
    x = cat{name: "花花"}
    x.move()
    x.say()
}

空接口的定义

空接口是指没有定义任何方法的接口。因此任何类型都实现了空接口。

空接口类型的变量可以存储任意类型的变量。

func main() {
    // 定义一个空接口x
    var x interface{}
    s := "pprof.cn"
    x = s
    fmt.Printf("type:%T value:%v\n", x, x)
    i := 100
    x = i
    fmt.Printf("type:%T value:%v\n", x, x)
    b := true
    x = b
    fmt.Printf("type:%T value:%v\n", x, x)
}

空接口的应用

空接口作为函数的参数

使用空接口实现可以接收任意类型的函数参数。

// 空接口作为函数参数
func show(a interface{}) {
    fmt.Printf("type:%T value:%v\n", a, a)
}

空接口作为map的值

使用空接口实现可以保存任意值的字典。

// 空接口作为map值
    var studentInfo = make(map[string]interface{})
    studentInfo["name"] = "李白"
    studentInfo["age"] = 18
    studentInfo["married"] = false
    fmt.Println(studentInfo)

类型断言

空接口可以存储任意类型的值,那我们如何获取其存储的具体数据呢?

接口值

一个接口的值(简称接口值)是由一个具体类型和具体类型的值两部分组成的。这两部分分别称为接口的动态类型和动态值。

我们来看一个具体的例子:

var w io.Writer
w = os.Stdout
w = new(bytes.Buffer)
w = nil

分解

想要判断空接口中的值这个时候就可以使用类型断言,其语法格式:

    x.(T)


其中:

    x:表示类型为interface{}的变量
    T:表示断言x可能是的类型。


该语法返回两个参数,第一个参数是x转化为T类型后的变量,第二个值是一个布尔值,若为true则表示断言成功,为false则表示断言失败。

举个例子:

func main() {
    var x interface{}
    x = "pprof.cn"
    v, ok := x.(string)
    if ok {
        fmt.Println(v)
    } else {
        fmt.Println("类型断言失败")
    }
}

上面的示例中如果要断言多次就需要写多个if判断,这个时候我们可以使用switch语句来实现:

func justifyType(x interface{}) {
    switch v := x.(type) {
    case string:
        fmt.Printf("x is a string,value is %v\n", v)
    case int:
        fmt.Printf("x is a int is %v\n", v)
    case bool:
        fmt.Printf("x is a bool is %v\n", v)
    default:
        fmt.Println("unsupport type!")
    }
}

因为空接口可以存储任意类型值的特点,所以空接口在Go语言中的使用十分广泛。

关于接口需要注意的是,只有当有两个或两个以上的具体类型必须以相同的方式进行处理时才需要定义接口。不要为了接口而写接口,那样只会增加不必要的抽象,导致不必要的运行时损耗。

通过接口实现ocp的设计原则

开闭原则(Open-Closed Principle,OCP)

例子

宠物:

type Pet interface{
    eat()
    sleep()
}

Dog狗:

type Dog struct{
    name string
    age int
}

Dog实现接口的方法:

func (this Dog) eat(){
    fmt.Print("dog eat ...")
}

Cat猫:

type Cat struct{
 	name string
    age int
}

Cat实现接口的方法:

func(this Cat)eat(){
    fmt.Println("cat eat ...")
}
func (this Cat)sleep(){
    fmt.Printlv("cat sleep...")
}

Person人:

type Person struct{
    name string
}
func (this Persion)care(pet Pet){
    pet.sleeo()
    pet.eat()
}

测试:

func main (){
    dog:=Dog{}
    cat:=Cat{}
    per:=Persion{}
    per.care(cat)
    per.care(dog)
}

模拟OOP的属性和方法

为每一个函数添加一个结构体,接受者。函数变成了一个方法。

反射

pair结构

变量:

  • type
    • static type----int、 string
    • concrete type---interface
  • value

内置的pair的详细

pair:

  • type:
  • value:

例子:

接口和类型之间的调用

package main

import "fmt"

type Reader interface {
	ReaderBook()
}
type Writer interface {
	WriterBook()
}

// 具体类型
type Book struct {
}

func (this *Book) ReaderBook() {
	fmt.Println("Read a book")
}
func (this *Book) WriterBook() {
	fmt.Println("Read a book")
}
func main() {
    //pair<type:Book,value:book{}的地址
	b := &Book{}
    //pair<type:,value:>
	var r Reader
    //pair<type:Book,value:book{}的地址>
	r = b
	r.ReaderBook()
    var w Writer
    //pair<type:Book,value:book{}地址>
    w=r.(Writer)//类型断言
    w.WriterBook()

}

reflect包

  • ValueOf用来获取输入参数中接口中的1数据的值,如果接口为空则返回0
func valueOf(i interface{})Value{...}
  • TypeOf用来动态的获取输入参数接口中的值的类型,如果接口为空则返回nil
func TypeOf(i interface{})Type{...}

例子:

package main

import (
	"fmt"
	"reflect"
)

type User struct {
	Id   int
	Name string
	Age  int
}

func reflectNum(arg interface{}) {
	fmt.Println("type:", reflect.TypeOf(arg))
	fmt.Println("value:", reflect.ValueOf(arg))
}
func DoFiledAndMethod(input interface{}) {
	//获得input的type
	inputType := reflect.TypeOf(input)
	fmt.Println("input的type:", inputType)
	//获得input的value
	inputValue := reflect.ValueOf(input)
	fmt.Println("input的value:", inputValue)
	//通过type获得input中的字段
	for i := 0; i < inputType.NumField(); i++ {
		field := inputType.Field(i)
		value := inputValue.Field(i).Interface()
		fmt.Printf("%s:%v=%v\n", field.Name, field.Type, value)

	}
	//通过type获得里面的方法
	for i := 0; i < inputType.NumMethod(); i++ {
		m := inputType.Method(i)
		fmt.Printf("%s:%v\n", m.Name, m.Type)

	}
}
func main() {
	reflectNum("sss")
	reflectNum(123)
	user := User{1, "hjz", 20}
	DoFiledAndMethod(user)
}

[Running] go run "d:\桌面\GOlearn\test\main.go"
type: string
value: sss
type: int
value: 123
input的type: main.User
input的value: {1 hjz 20}
Id:int=1
Name:string=hjz
Age:int=20

[Done] exited with code=0 in 1.831 seconds

结构体标签Tag

依据反射们可以为结构体中添加一些标签,来让不同的包中使用不同的数据类型。

例子:

package main

import (
	"fmt"
	"reflect"
)

type User struct {
	Id   int    `info:"name" doc:"ID"`
	Name string `info:"sex"`
	Age  int
}

func FindTag(val interface{}) {
	//当前结构体的所有元素
	t := reflect.TypeOf(val).Elem()
	for i := 0; i < t.NumField(); i++ {
		tagstring := t.Field(i).Tag.Get("info")
		tagdoc := t.Field(i).Tag.Get("doc")
		fmt.Println("info:", tagstring)
		fmt.Println("doc:", tagdoc)
	}
}
func main() {
	r := &User{}
	FindTag(r)
}

[Running] go run "d:\桌面\GOlearn\test\main.go"
info: name
doc: ID
info: sex
doc: 
info: 
doc: 

[Done] exited with code=0 in 1.722 seconds

结构体标签在json中的应用

结构体变成json--json.Marshal()

json.Marshal(xx)

例子:

package main

import (
	"encoding/json"
	"fmt"
)

type Movie struct {
	Title  string   `json:"title"`
	Year   int      `json:"year"`
	Price  int      `json:"rmb"`
	Actors []string `json:"actors"`
}

func main() {
	movie := Movie{"喜剧之王", 2000, 10, []string{"周星驰", "张柏芝"}}
	//编码json的过程 结构体编程json
	jsonStr, err := json.Marshal(movie)
	if err != nil {
		fmt.Println("json marshal error", err)
		return
	}
	fmt.Printf("jsonStr =%s\n", jsonStr)

}
[Running] go run "d:\桌面\GOlearn\test\main.go"
jsonStr ={"title":"喜剧之王","year":2000,"rmb":10,"actors":["周星驰","张柏芝"]}

[Done] exited with code=0 in 3.571 seconds

json变成结构体--json.Unmarshal(xx,xx)

package main

import (
	"encoding/json"
	"fmt"
)

type Movie struct {
	Title  string   `json:"title"`
	Year   int      `json:"year"`
	Price  int      `json:"rmb"`
	Actors []string `json:"actors"`
}

func main() {
	movie := Movie{"喜剧之王", 2000, 10, []string{"周星驰", "张柏芝"}}
	//编码json的过程 结构体编程json
	jsonStr, err := json.Marshal(movie)
	if err != nil {
		fmt.Println("json marshal error", err)
		return
	}
	fmt.Printf("jsonStr =%s\n", jsonStr)
	//json变成结构体
	myMovie := Movie{}
	err = json.Unmarshal(jsonStr, &myMovie)
	if err != nil {
		fmt.Println("json unmarshal error", err)
		return
	}
	fmt.Print(myMovie)
}

[Running] go run "d:\桌面\GOlearn\test\main.go"
jsonStr ={"title":"喜剧之王","year":2000,"rmb":10,"actors":["周星驰","张柏芝"]}
{喜剧之王 2000 10 [周星驰 张柏芝]}
[Done] exited with code=0 in 2.184 seconds

协程

进程、线程的数量越多,切换的成本就越大,也就越浪费

多线程 伴随着同步竞争(资源,锁...)

高cpu调度、高内存占用

  • 进程占用内存 虚拟内存4GB(32bit OS)
  • 线程占用内存 越4MB

goroutine

CPU视角:

  • 用户线程(co-routine)协程
  • 内核线程(thread)

CPU视角:m-n

  • 协程 n (co-routine)
  • 协程调度器(轮询)
  • 线程(thread)

GoLang对于协程的处理:

协程(co-routine)-->Groutine 几KB---大量

灵活调度:调度器

早起的调度器:

G----goroutine 协程

M---thread 线程

全局go协程队列:

  • 任务goroutine n

有多个线程

每一次执行都会有执行锁和放回锁

缺点:

  1. 创建、销毁、调度G都需要每个M获得锁,这就形成了激烈的竞争
  2. M转移G会造成延迟和额外的系统负载在原来的G执行中又新产生一个协程,但是Go会将他放到新的M线程上
  3. 系统调用(CPU在M之间的切换)导致频繁的线程阻塞和取消阻塞操作增加了系统开销

GMP

G--goroutine 协程

P-----processor处理器

M----thread线程


  • 全局队列
  • GOMAXPROCS个-------P
    • 每一p内部有G的P的本地队列
  • 内核线程
  • 操作系统调度器
  • 硬件CPU核心

每一次运行的并行的协程就是GOMAXPROCS的数量

放到P上面才能运行,创建一个新的G优先放到P的本地队列中,如果满了放到全局队列中

调度器的设计策略

  • 复用线程
    • work stealing机制 偷取 执行时,如果新的线程本地队列没有执行的协程,就偷取额外的P的协程
    • hand off机制 握手 当执行协程出现阻塞时,首先创建/唤醒一个线程thread,原来的线程还是阻塞协程G,但是原来的P会调度到新唤醒的线程上继续执行新的协程
  • 利用并行
    • GOMAXPROCS限定的个数=CPU核数/2
  • 抢占
    • 原来 co-routine C 主动释放CPU,别的新的co-routine才能执行
    • 现在goroutine G 最多运行10ms
  • 全局G队列
    • 全局队列 ----入队和出队--锁--比较慢
    • 执行时会先去本地的队列找协程,没有去找别的P,最后去全局队列

网络编程

互联网的核心是一系列协议,总称为”互联网协议”(Internet Protocol Suite),正是这一些协议规定了电脑如何连接和组网。我们理解了这些协议,就理解了互联网的原理。由于这些协议太过庞大和复杂,没有办法在这里一概而全,只能介绍一下我们日常开发中接触较多的几个协议。

互联网的逻辑实现被分为好几层。每一层都有自己的功能,就像建筑物一样,每一层都靠下一层支持。用户接触到的只是最上面的那一层,根本不会感觉到下面的几层。要理解互联网就需要自下而上理解每一层的实现的功能。

分层模型

物理层

我们的电脑要与外界互联网通信,需要先把电脑连接网络,我们可以用双绞线、光纤、无线电波等方式。这就叫做”实物理层”,它就是把电脑连接起来的物理手段。它主要规定了网络的一些电气特性,作用是负责传送0和1的电信号。

数据链路层

单纯的0和1没有任何意义,所以我们使用者会为其赋予一些特定的含义,规定解读电信号的方式:例如:多少个电信号算一组?每个信号位有何意义?这就是”数据链接层”的功能,它在”物理层”的上方,确定了物理层传输的0和1的分组方式及代表的意义。早期的时候,每家公司都有自己的电信号分组方式。逐渐地,一种叫做”以太网”(Ethernet)的协议,占据了主导地位。

以太网规定,一组电信号构成一个数据包,叫做”帧”(Frame)。每一帧分成两个部分:标头(Head)和数据(Data)。其中”标头”包含数据包的一些说明项,比如发送者、接受者、数据类型等等;”数据”则是数据包的具体内容。”标头”的长度,固定为18字节。”数据”的长度,最短为46字节,最长为1500字节。因此,整个”帧”最短为64字节,最长为1518字节。如果数据很长,就必须分割成多个帧进行发送。

那么,发送者和接受者是如何标识呢?以太网规定,连入网络的所有设备都必须具有”网卡”接口。数据包必须是从一块网卡,传送到另一块网卡。网卡的地址,就是数据包的发送地址和接收地址,这叫做MAC地址。每块网卡出厂的时候,都有一个全世界独一无二的MAC地址,长度是48个二进制位,通常用12个十六进制数表示。前6个十六进制数是厂商编号,后6个是该厂商的网卡流水号。有了MAC地址,就可以定位网卡和数据包的路径了。

我们会通过ARP协议来获取接受方的MAC地址,有了MAC地址之后,如何把数据准确的发送给接收方呢?其实这里以太网采用了一种很”原始”的方式,它不是把数据包准确送到接收方,而是向本网络内所有计算机都发送,让每台计算机读取这个包的”标头”,找到接收方的MAC地址,然后与自身的MAC地址相比较,如果两者相同,就接受这个包,做进一步处理,否则就丢弃这个包。这种发送方式就叫做”广播”(broadcasting)。

网络层

按照以太网协议的规则我们可以依靠MAC地址来向外发送数据。理论上依靠MAC地址,你电脑的网卡就可以找到身在世界另一个角落的某台电脑的网卡了,但是这种做法有一个重大缺陷就是以太网采用广播方式发送数据包,所有成员人手一”包”,不仅效率低,而且发送的数据只能局限在发送者所在的子网络。也就是说如果两台计算机不在同一个子网络,广播是传不过去的。这种设计是合理且必要的,因为如果互联网上每一台计算机都会收到互联网上收发的所有数据包,那是不现实的。

因此,必须找到一种方法区分哪些MAC地址属于同一个子网络,哪些不是。如果是同一个子网络,就采用广播方式发送,否则就采用”路由”方式发送。这就导致了”网络层”的诞生。它的作用是引进一套新的地址,使得我们能够区分不同的计算机是否属于同一个子网络。这套地址就叫做”网络地址”,简称”网址”。

“网络层”出现以后,每台计算机有了两种地址,一种是MAC地址,另一种是网络地址。两种地址之间没有任何联系,MAC地址是绑定在网卡上的网络地址则是网络管理员分配的。网络地址帮助我们确定计算机所在的子网络,MAC地址则将数据包送到该子网络中的目标网卡。因此,从逻辑上可以推断,必定是先处理网络地址,然后再处理MAC地址。

规定网络地址的协议,叫做IP协议。它所定义的地址,就被称为IP地址。目前,广泛采用的是IP协议第四版,简称IPv4。IPv4这个版本规定,网络地址由32个二进制位组成,我们通常习惯用分成四段的十进制数表示IP地址,从0.0.0.0一直到255.255.255.255。

根据IP协议发送的数据,就叫做IP数据包。IP数据包也分为”标头”和”数据”两个部分:”标头”部分主要包括版本、长度、IP地址等信息,”数据”部分则是IP数据包的具体内容。IP数据包的”标头”部分的长度为20到60字节,整个数据包的总长度最大为65535字节。

传输层

有了MAC地址和IP地址,我们已经可以在互联网上任意两台主机上建立通信。但问题是同一台主机上会有许多程序都需要用网络收发数据,比如QQ和浏览器这两个程序都需要连接互联网并收发数据,我们如何区分某个数据包到底是归哪个程序的呢?也就是说,我们还需要一个参数,表示这个数据包到底供哪个程序(进程)使用。这个参数就叫做”端口”(port),它其实是每一个使用网卡的程序的编号。每个数据包都发到主机的特定端口,所以不同的程序就能取到自己所需要的数据。

“端口”是0到65535之间的一个整数,正好16个二进制位。0到1023的端口被系统占用,用户只能选用大于1023的端口。有了IP和端口我们就能实现唯一确定互联网上一个程序,进而实现网络间的程序通信。

我们必须在数据包中加入端口信息,这就需要新的协议。最简单的实现叫做UDP协议,它的格式几乎就是在数据前面,加上端口号。UDP数据包,也是由”标头”和”数据”两部分组成:”标头”部分主要定义了发出端口和接收端口,”数据”部分就是具体的内容。UDP数据包非常简单,”标头”部分一共只有8个字节,总长度不超过65,535字节,正好放进一个IP数据包。

UDP协议的优点是比较简单,容易实现,但是缺点是可靠性较差,一旦数据包发出,无法知道对方是否收到。为了解决这个问题,提高网络可靠性,TCP协议就诞生了。TCP协议能够确保数据不会遗失。它的缺点是过程复杂、实现困难、消耗较多的资源。TCP数据包没有长度限制,理论上可以无限长,但是为了保证网络的效率,通常TCP数据包的长度不会超过IP数据包的长度,以确保单个TCP数据包不必再分割。

应用层

应用程序收到”传输层”的数据,接下来就要对数据进行解包。由于互联网是开放架构,数据来源五花八门,必须事先规定好通信的数据格式,否则接收方根本无法获得真正发送的数据内容。”应用层”的作用就是规定应用程序使用的数据格式,例如我们TCP协议之上常见的Email、HTTP、FTP等协议,这些协议就组成了互联网协议的应用层。

如下图所示,发送方的HTTP数据经过互联网的传输过程中会依次添加各层协议的标头信息,接收方收到数据包之后再依次根据协议解包得到数据。

应用层

socket编程

Socket是BSD UNIX的进程通信机制,通常也称作”套接字”,用于描述IP地址和端口,是一个通信链的句柄。Socket可以理解为TCP/IP网络的API,它定义了许多函数或例程,程序员可以用它们来开发TCP/IP网络上的应用程序。电脑上运行的应用程序通常通过”套接字”向网络发出请求或者应答网络请求。

socket图解

  • Socket又称“套接字”,应用程序通常通过“套接字”向网络发出请求或者应答网络请求
  • 常用的Socket类型有两种:流式Socket和数据报式Socket,流式是一种面向连接的Socket,针对于面向连接的TCP服务应用,数据报式Socket是一种无连接的Socket,针对于无连接的UDP服务应用
  • TCP:比较靠谱,面向连接,比较慢
  • UDP:不是太靠谱,比较快

举个例子:TCP就像货到付款的快递,送到家还必须见到你人才算一整套流程。UDP就像某快递快递柜一扔就走管你收到收不到,一般直播用UDP

实现TCP通信

TCP协议

TCP/IP(Transmission Control Protocol/Internet Protocol) 即传输控制协议/网间协议,是一种面向连接(连接导向)的、可靠的、基于字节流的传输层(Transport layer)通信协议,因为是面向连接的协议,数据像水流一样传输,会存在黏包问题。

TCP服务端

一个TCP服务端可以同时连接很多个客户端,例如世界各地的用户使用自己电脑上的浏览器访问淘宝网。因为Go语言中创建多个goroutine实现并发非常方便和高效,所以我们可以每建立一次链接就创建一个goroutine去处理。

TCP服务端程序的处理流程:

    1.监听端口
    2.接收客户端请求建立链接
    3.创建goroutine处理链接。


我们使用Go语言的net包实现的TCP服务端代码如下:

// tcp/server/main.go

// TCP server端

// 处理函数
func process(conn net.Conn) {
    defer conn.Close() // 关闭连接
    for {
        reader := bufio.NewReader(conn)
        var buf [128]byte
        n, err := reader.Read(buf[:]) // 读取数据
        if err != nil {
            fmt.Println("read from client failed, err:", err)
            break
        }
        recvStr := string(buf[:n])
        fmt.Println("收到client端发来的数据:", recvStr)
        conn.Write([]byte(recvStr)) // 发送数据
    }
}

func main() {
    listen, err := net.Listen("tcp", "127.0.0.1:20000")
    if err != nil {
        fmt.Println("listen failed, err:", err)
        return
    }
    for {
        conn, err := listen.Accept() // 建立连接
        if err != nil {
            fmt.Println("accept failed, err:", err)
            continue
        }
        go process(conn) // 启动一个goroutine处理连接
    }
}

将上面的代码保存之后编译成server或server.exe可执行文件。

TCP客户端

一个TCP客户端进行TCP通信的流程如下:

    1.建立与服务端的链接
    2.进行数据收发
    3.关闭链接


使用Go语言的net包实现的TCP客户端代码如下:

// tcp/client/main.go

// 客户端
func main() {
    conn, err := net.Dial("tcp", "127.0.0.1:20000")
    if err != nil {
        fmt.Println("err :", err)
        return
    }
    defer conn.Close() // 关闭连接
    inputReader := bufio.NewReader(os.Stdin)
    for {
        input, _ := inputReader.ReadString('\n') // 读取用户输入
        inputInfo := strings.Trim(input, "\r\n")
        if strings.ToUpper(inputInfo) == "Q" { // 如果输入q就退出
            return
        }
        _, err = conn.Write([]byte(inputInfo)) // 发送数据
        if err != nil {
            return
        }
        buf := [512]byte{}
        n, err := conn.Read(buf[:])
        if err != nil {
            fmt.Println("recv failed, err:", err)
            return
        }
        fmt.Println(string(buf[:n]))
    }
}

将上面的代码编译成client或client.exe可执行文件,先启动server端再启动client端,在client端输入任意内容回车之后就能够在server端看到client端发送的数据,从而实现TCP通信。

UDP协议

UDP协议(User Datagram Protocol)中文名称是用户数据报协议,是OSI(Open System Interconnection,开放式系统互联)参考模型中一种无连接的传输层协议,不需要建立连接就能直接进行数据发送和接收,属于不可靠的、没有时序的通信,但是UDP协议的实时性比较好,通常用于视频直播相关领域。

UDP服务端

使用Go语言的net包实现的UDP服务端代码如下:

// UDP/server/main.go

// UDP server端
func main() {
    listen, err := net.ListenUDP("udp", &net.UDPAddr{
        IP:   net.IPv4(0, 0, 0, 0),
        Port: 30000,
    })
    if err != nil {
        fmt.Println("listen failed, err:", err)
        return
    }
    defer listen.Close()
    for {
        var data [1024]byte
        n, addr, err := listen.ReadFromUDP(data[:]) // 接收数据
        if err != nil {
            fmt.Println("read udp failed, err:", err)
            continue
        }
        fmt.Printf("data:%v addr:%v count:%v\n", string(data[:n]), addr, n)
        _, err = listen.WriteToUDP(data[:n], addr) // 发送数据
        if err != nil {
            fmt.Println("write to udp failed, err:", err)
            continue
        }
    }
}

UDP客户端

使用Go语言的net包实现的UDP客户端代码如下:

// UDP 客户端
func main() {
    socket, err := net.DialUDP("udp", nil, &net.UDPAddr{
        IP:   net.IPv4(0, 0, 0, 0),
        Port: 30000,
    })
    if err != nil {
        fmt.Println("连接服务端失败,err:", err)
        return
    }
    defer socket.Close()
    sendData := []byte("Hello server")
    _, err = socket.Write(sendData) // 发送数据
    if err != nil {
        fmt.Println("发送数据失败,err:", err)
        return
    }
    data := make([]byte, 4096)
    n, remoteAddr, err := socket.ReadFromUDP(data) // 接收数据
    if err != nil {
        fmt.Println("接收数据失败,err:", err)
        return
    }
    fmt.Printf("recv:%v addr:%v count:%v\n", string(data[:n]), remoteAddr, n)
}

TCP黏包

服务端代码如下:

// socket_stick/server/main.go

func process(conn net.Conn) {
    defer conn.Close()
    reader := bufio.NewReader(conn)
    var buf [1024]byte
    for {
        n, err := reader.Read(buf[:])
        if err == io.EOF {
            break
        }
        if err != nil {
            fmt.Println("read from client failed, err:", err)
            break
        }
        recvStr := string(buf[:n])
        fmt.Println("收到client发来的数据:", recvStr)
    }
}

func main() {

    listen, err := net.Listen("tcp", "127.0.0.1:30000")
    if err != nil {
        fmt.Println("listen failed, err:", err)
        return
    }
    defer listen.Close()
    for {
        conn, err := listen.Accept()
        if err != nil {
            fmt.Println("accept failed, err:", err)
            continue
        }
        go process(conn)
    }
}

客户端代码如下:

// socket_stick/client/main.go

func main() {
    conn, err := net.Dial("tcp", "127.0.0.1:30000")
    if err != nil {
        fmt.Println("dial failed, err", err)
        return
    }
    defer conn.Close()
    for i := 0; i < 20; i++ {
        msg := `Hello, Hello. How are you?`
        conn.Write([]byte(msg))
    }
}

将上面的代码保存后,分别编译。先启动服务端再启动客户端,可以看到服务端输出结果如下:

收到client发来的数据: Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?
收到client发来的数据: Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?
收到client发来的数据: Hello, Hello. How are you?Hello, Hello. How are you?
收到client发来的数据: Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?
收到client发来的数据: Hello, Hello. How are you?Hello, Hello. How are you?

客户端分10次发送的数据,在服务端并没有成功的输出10次,而是多条数据“粘”到了一起。

1.1.1. 为什么会出现粘包

主要原因就是tcp数据传递模式是流模式,在保持长连接的时候可以进行多次的收和发。

“粘包”可发生在发送端也可发生在接收端:

    1.由Nagle算法造成的发送端的粘包:Nagle算法是一种改善网络传输效率的算法。简单来说就是当我们提交一段数据给TCP发送时,TCP并不立刻发送此段数据,而是等待一小段时间看看在等待期间是否还有要发送的数据,若有则会一次把这两段数据发送出去。
    2.接收端接收不及时造成的接收端粘包:TCP会把接收到的数据存在自己的缓冲区中,然后通知应用层取数据。当应用层由于某些原因不能及时的把TCP的数据取出来,就会造成TCP缓冲区中存放了几段数据。


1.1.2. 解决办法

出现”粘包”的关键在于接收方不确定将要传输的数据包的大小,因此我们可以对数据包进行封包和拆包的操作。

封包:封包就是给一段数据加上包头,这样一来数据包就分为包头和包体两部分内容了(过滤非法包时封包会加入”包尾”内容)。包头部分的长度是固定的,并且它存储了包体的长度,根据包头长度固定以及包头中含有包体长度的变量就能正确的拆分出一个完整的数据包。

我们可以自己定义一个协议,比如数据包的前4个字节为包头,里面存储的是发送的数据的长度。

// socket_stick/proto/proto.go
package proto

import (
    "bufio"
    "bytes"
    "encoding/binary"
)

// Encode 将消息编码
func Encode(message string) ([]byte, error) {
    // 读取消息的长度,转换成int32类型(占4个字节)
    var length = int32(len(message))
    var pkg = new(bytes.Buffer)
    // 写入消息头
    err := binary.Write(pkg, binary.LittleEndian, length)
    if err != nil {
        return nil, err
    }
    // 写入消息实体
    err = binary.Write(pkg, binary.LittleEndian, []byte(message))
    if err != nil {
        return nil, err
    }
    return pkg.Bytes(), nil
}

// Decode 解码消息
func Decode(reader *bufio.Reader) (string, error) {
    // 读取消息的长度
    lengthByte, _ := reader.Peek(4) // 读取前4个字节的数据
    lengthBuff := bytes.NewBuffer(lengthByte)
    var length int32
    err := binary.Read(lengthBuff, binary.LittleEndian, &length)
    if err != nil {
        return "", err
    }
    // Buffered返回缓冲中现有的可读取的字节数。
    if int32(reader.Buffered()) < length+4 {
        return "", err
    }

    // 读取真正的消息数据
    pack := make([]byte, int(4+length))
    _, err = reader.Read(pack)
    if err != nil {
        return "", err
    }
    return string(pack[4:]), nil
}

接下来在服务端和客户端分别使用上面定义的proto包的Decode和Encode函数处理数据。

服务端代码如下:

// socket_stick/server2/main.go

func process(conn net.Conn) {
    defer conn.Close()
    reader := bufio.NewReader(conn)
    for {
        msg, err := proto.Decode(reader)
        if err == io.EOF {
            return
        }
        if err != nil {
            fmt.Println("decode msg failed, err:", err)
            return
        }
        fmt.Println("收到client发来的数据:", msg)
    }
}

func main() {

    listen, err := net.Listen("tcp", "127.0.0.1:30000")
    if err != nil {
        fmt.Println("listen failed, err:", err)
        return
    }
    defer listen.Close()
    for {
        conn, err := listen.Accept()
        if err != nil {
            fmt.Println("accept failed, err:", err)
            continue
        }
        go process(conn)
    }
}

客户端代码如下:

// socket_stick/client2/main.go

func main() {
    conn, err := net.Dial("tcp", "127.0.0.1:30000")
    if err != nil {
        fmt.Println("dial failed, err", err)
        return
    }
    defer conn.Close()
    for i := 0; i < 20; i++ {
        msg := `Hello, Hello. How are you?`
        data, err := proto.Encode(msg)
        if err != nil {
            fmt.Println("encode msg failed, err:", err)
            return
        }
        conn.Write(data)
    }
}

http编程

例子:

服务端:

package main

import (
	"fmt"
	"net"
	"strings"
)

// 业务逻辑
func HandlerServer(conn net.Conn) {
	//获得客户端的网络地址
	addr := conn.RemoteAddr().String()
	fmt.Printf("addr: %v\n", addr)
	//创建读缓存区
	buf := make([]byte, 1024)
	for {
		//读取用户数据
		n, err := conn.Read(buf)
		if err != nil {
			fmt.Printf("err: %v\n", err)
			return
		}
		fmt.Printf("string(buf[:n]): %v\n", string(buf[:n]))
		//断开连接
		if string(buf[:n-2]) == "exit" {
			fmt.Println(addr, "is exit")
			return
		}
		//把数据转化为大写给用户发送
		conn.Write([]byte(strings.ToUpper(string(buf[:n]))))
	}
}

// 服务端
func main() {
	//监听
	Listener, err := net.Listen("tcp", "127.0.0.1:8000")
	if err != nil {
		fmt.Printf("err: %v\n", err)
		return
	}
	defer Listener.Close()
	//接收多个用户
	for {
		conn, err2 := Listener.Accept()
		if err2 != nil {
			fmt.Printf("err2: %v\n", err2)
			continue
		}
		defer conn.Close()
		//业务处理
		go HandlerServer(conn)
	}
}

客户端:

package main

import (
	"fmt"
	"net"
	"os"
)

//客户端

func main() {
	//拨号
	conn, err := net.Dial("tcp", "127.0.0.1:8000")
	if err != nil {
		fmt.Printf("err: %v\n", err)
		return
	}
	defer conn.Close()

	//接收服务器的数据
	go func() {
		str := make([]byte, 1024)
		for {
			//从键盘输入内容-给服务器发送内容
			n, err3 := os.Stdin.Read(str)
			if err3 != nil {
				fmt.Printf("err3: %v\n", err3)
				continue
			}
			// fmt.Printf("string(str[:n]): %v\n", string(str[:n]))
			//发送信息到服务器
			conn.Write(str[:n])
			if string(str[:n]) == "exit" {
				return
			}
		}
	}()
	buf := make([]byte, 1024)
	for {
		n, err2 := conn.Read(buf)
		if err2 != nil {
			fmt.Printf("err: %v\n", err)
			continue
		}
		fmt.Printf("string(buf[:n]): %v\n", string(buf[:n]))
	}
}

web的工作流程

web服务器的工作原理可以简单地归纳为

  • 客户端通过TCP/IP协议建立到服务器的TCP连接
  • 客户端向服务器发送HTTP协议请求包,请求到服务器里的资源文档
  • 服务器向客户机发送HTTP协议应答包,如果请求的资源包含有动态语言的内容,那么服务器会调用动态语言的解释引擎负责处理“动态内容”,并将处理得到的数据返回给客户端
  • 客户机与服务器断开。由客户端解释HTML文档,在客户端屏幕上渲染图形结果

HTTP协议

  • 超文本传输协议(HTTP,HyperText Transfer Protocol)是互联网上应用最为广泛的一种网络协议,它详细规定了浏览器和万维网服务器之间互相通信的规则,通过因特网传送万维网文档的数据传送协议
  • HTTP协议通常承载于TCP协议之上

HTTP服务端

package main

import (
    "fmt"
    "net/http"
)

func main() {
    //http://127.0.0.1:8000/go
    // 单独写回调函数
    http.HandleFunc("/go", myHandler)
    //http.HandleFunc("/ungo",myHandler2 )
    // addr:监听的地址
    // handler:回调函数
    http.ListenAndServe("127.0.0.1:8000", nil)
}

// handler函数
func myHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Println(r.RemoteAddr, "连接成功")
    // 请求方式:GET POST DELETE PUT UPDATE
    fmt.Println("method:", r.Method)
    // /go
    fmt.Println("url:", r.URL.Path)
    fmt.Println("header:", r.Header)
    fmt.Println("body:", r.Body)
    // 回复
    w.Write([]byte("www.5lmh.com"))
}

1.1.4. HTTP服务端

package main

import (
    "fmt"
    "io"
    "net/http"
)

func main() {
    //resp, _ := http.Get("http://www.baidu.com")
    //fmt.Println(resp)
    resp, _ := http.Get("http://127.0.0.1:8000/go")
    defer resp.Body.Close()
    // 200 OK
    fmt.Println(resp.Status)
    fmt.Println(resp.Header)

    buf := make([]byte, 1024)
    for {
        // 接收服务端信息
        n, err := resp.Body.Read(buf)
        if err != nil && err != io.EOF {
            fmt.Println(err)
            return
        } else {
            fmt.Println("读取完毕")
            res := string(buf[:n])
            fmt.Println(res)
            break
        }
    }
}

WebSocket编程

webSocket是什么

  • WebSocket是一种在单个TCP连接上进行全双工通信的协议
  • WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据
  • 在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输
  • 需要安装第三方包:
    • cmd中:go get -u -v github.com/gorilla/websocket

举个聊天室的小例子

在同一级目录下新建四个go文件connection.go|data.go|hub.go|server.go

运行

    go run server.go hub.go data.go connection.go

运行之后执行local.html文件

server.go文件代码

package main

import (
    "fmt"
    "net/http"

    "github.com/gorilla/mux"
)

func main() {
    router := mux.NewRouter()
    go h.run()
    router.HandleFunc("/ws", myws)
    if err := http.ListenAndServe("127.0.0.1:8080", router); err != nil {
        fmt.Println("err:", err)
    }
}

hub.go文件代码

package main

import "encoding/json"

var h = hub{
    c: make(map[*connection]bool),
    u: make(chan *connection),
    b: make(chan []byte),
    r: make(chan *connection),
}

type hub struct {
    c map[*connection]bool
    b chan []byte
    r chan *connection
    u chan *connection
}

func (h *hub) run() {
    for {
        select {
        case c := <-h.r:
            h.c[c] = true
            c.data.Ip = c.ws.RemoteAddr().String()
            c.data.Type = "handshake"
            c.data.UserList = user_list
            data_b, _ := json.Marshal(c.data)
            c.sc <- data_b
        case c := <-h.u:
            if _, ok := h.c[c]; ok {
                delete(h.c, c)
                close(c.sc)
            }
        case data := <-h.b:
            for c := range h.c {
                select {
                case c.sc <- data:
                default:
                    delete(h.c, c)
                    close(c.sc)
                }
            }
        }
    }
}

data.go文件代码

package main

type Data struct {
    Ip       string   `json:"ip"`
    User     string   `json:"user"`
    From     string   `json:"from"`
    Type     string   `json:"type"`
    Content  string   `json:"content"`
    UserList []string `json:"user_list"`
}

connection.go文件代码

package main

import (
    "encoding/json"
    "fmt"
    "net/http"

    "github.com/gorilla/websocket"
)

type connection struct {
    ws   *websocket.Conn
    sc   chan []byte
    data *Data
}

var wu = &websocket.Upgrader{ReadBufferSize: 512,
    WriteBufferSize: 512, CheckOrigin: func(r *http.Request) bool { return true }}

func myws(w http.ResponseWriter, r *http.Request) {
    ws, err := wu.Upgrade(w, r, nil)
    if err != nil {
        return
    }
    c := &connection{sc: make(chan []byte, 256), ws: ws, data: &Data{}}
    h.r <- c
    go c.writer()
    c.reader()
    defer func() {
        c.data.Type = "logout"
        user_list = del(user_list, c.data.User)
        c.data.UserList = user_list
        c.data.Content = c.data.User
        data_b, _ := json.Marshal(c.data)
        h.b <- data_b
        h.r <- c
    }()
}

func (c *connection) writer() {
    for message := range c.sc {
        c.ws.WriteMessage(websocket.TextMessage, message)
    }
    c.ws.Close()
}

var user_list = []string{}

func (c *connection) reader() {
    for {
        _, message, err := c.ws.ReadMessage()
        if err != nil {
            h.r <- c
            break
        }
        json.Unmarshal(message, &c.data)
        switch c.data.Type {
        case "login":
            c.data.User = c.data.Content
            c.data.From = c.data.User
            user_list = append(user_list, c.data.User)
            c.data.UserList = user_list
            data_b, _ := json.Marshal(c.data)
            h.b <- data_b
        case "user":
            c.data.Type = "user"
            data_b, _ := json.Marshal(c.data)
            h.b <- data_b
        case "logout":
            c.data.Type = "logout"
            user_list = del(user_list, c.data.User)
            data_b, _ := json.Marshal(c.data)
            h.b <- data_b
            h.r <- c
        default:
            fmt.Print("========default================")
        }
    }
}

func del(slice []string, user string) []string {
    count := len(slice)
    if count == 0 {
        return slice
    }
    if count == 1 && slice[0] == user {
        return []string{}
    }
    var n_slice = []string{}
    for i := range slice {
        if slice[i] == user && i == count {
            return slice[:count]
        } else if slice[i] == user {
            n_slice = append(slice[:i], slice[i+1:]...)
            break
        }
    }
    fmt.Println(n_slice)
    return n_slice
}

local.html文件代码

<!DOCTYPE html>
<html>
<head>
    <title></title>
    <meta http-equiv="content-type" content="text/html;charset=utf-8">
    <style>
        p {
            text-align: left;
            padding-left: 20px;
        }
    </style>
</head>
<body>
<div style="width: 800px;height: 600px;margin: 30px auto;text-align: center">
    <h1>www.5lmh.comy演示聊天室</h1>
    <div style="width: 800px;border: 1px solid gray;height: 300px;">
        <div style="width: 200px;height: 300px;float: left;text-align: left;">
            <p><span>当前在线:</span><span id="user_num">0</span></p>
            <div id="user_list" style="overflow: auto;">
            </div>
        </div>
        <div id="msg_list" style="width: 598px;border:  1px solid gray; height: 300px;overflow: scroll;float: left;">
        </div>
    </div>
    <br>
    <textarea id="msg_box" rows="6" cols="50" onkeydown="confirm(event)"></textarea><br>
    <input type="button" value="发送" onclick="send()">
</div>
</body>
</html>
<script type="text/javascript">
    var uname = prompt('请输入用户名', 'user' + uuid(8, 16));
    var ws = new WebSocket("ws://127.0.0.1:8080/ws");
    ws.onopen = function () {
        var data = "系统消息:建立连接成功";
        listMsg(data);
    };
    ws.onmessage = function (e) {
        var msg = JSON.parse(e.data);
        var sender, user_name, name_list, change_type;
        switch (msg.type) {
            case 'system':
                sender = '系统消息: ';
                break;
            case 'user':
                sender = msg.from + ': ';
                break;
            case 'handshake':
                var user_info = {'type': 'login', 'content': uname};
                sendMsg(user_info);
                return;
            case 'login':
            case 'logout':
                user_name = msg.content;
                name_list = msg.user_list;
                change_type = msg.type;
                dealUser(user_name, change_type, name_list);
                return;
        }
        var data = sender + msg.content;
        listMsg(data);
    };
    ws.onerror = function () {
        var data = "系统消息 : 出错了,请退出重试.";
        listMsg(data);
    };
    function confirm(event) {
        var key_num = event.keyCode;
        if (13 == key_num) {
            send();
        } else {
            return false;
        }
    }
    function send() {
        var msg_box = document.getElementById("msg_box");
        var content = msg_box.value;
        var reg = new RegExp("\r\n", "g");
        content = content.replace(reg, "");
        var msg = {'content': content.trim(), 'type': 'user'};
        sendMsg(msg);
        msg_box.value = '';
    }
    function listMsg(data) {
        var msg_list = document.getElementById("msg_list");
        var msg = document.createElement("p");
        msg.innerHTML = data;
        msg_list.appendChild(msg);
        msg_list.scrollTop = msg_list.scrollHeight;
    }
    function dealUser(user_name, type, name_list) {
        var user_list = document.getElementById("user_list");
        var user_num = document.getElementById("user_num");
        while(user_list.hasChildNodes()) {
            user_list.removeChild(user_list.firstChild);
        }
        for (var index in name_list) {
            var user = document.createElement("p");
            user.innerHTML = name_list[index];
            user_list.appendChild(user);
        }
        user_num.innerHTML = name_list.length;
        user_list.scrollTop = user_list.scrollHeight;
        var change = type == 'login' ? '上线' : '下线';
        var data = '系统消息: ' + user_name + ' 已' + change;
        listMsg(data);
    }
    function sendMsg(msg) {
        var data = JSON.stringify(msg);
        ws.send(data);
    }
    function uuid(len, radix) {
        var chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split('');
        var uuid = [], i;
        radix = radix || chars.length;
        if (len) {
            for (i = 0; i < len; i++) uuid[i] = chars[0 | Math.random() * radix];
        } else {
            var r;
            uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-';
            uuid[14] = '4';
            for (i = 0; i < 36; i++) {
                if (!uuid[i]) {
                    r = 0 | Math.random() * 16;
                    uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r];
                }
            }
        }
        return uuid.join('');
    }
</script>

例子

TCP -服务器-客户端

服务端:

package main

import (
	"fmt"
	"net"
	"strings"
)

// 业务逻辑
func HandlerServer(conn net.Conn) {
	//获得客户端的网络地址
	addr := conn.RemoteAddr().String()
	fmt.Printf("addr: %v\n", addr)
	//创建读缓存区
	buf := make([]byte, 1024)
	for {
		//读取用户数据
		n, err := conn.Read(buf)
		if err != nil {
			fmt.Printf("err: %v\n", err)
			return
		}
		fmt.Printf("string(buf[:n]): %v\n", string(buf[:n]))
		//断开连接
		if string(buf[:n-2]) == "exit" {
			fmt.Println(addr, "is exit")
			return
		}
		//把数据转化为大写给用户发送
		conn.Write([]byte(strings.ToUpper(string(buf[:n]))))
	}
}

// 服务端
func main() {
	//监听
	Listener, err := net.Listen("tcp", "127.0.0.1:8000")
	if err != nil {
		fmt.Printf("err: %v\n", err)
		return
	}
	defer Listener.Close()
	//接收多个用户
	for {
		conn, err2 := Listener.Accept()
		if err2 != nil {
			fmt.Printf("err2: %v\n", err2)
			continue
		}
		defer conn.Close()
		//业务处理
		go HandlerServer(conn)
	}
}

客户端:

package main

import (
	"fmt"
	"net"
	"os"
)

//客户端

func main() {
	//拨号
	conn, err := net.Dial("tcp", "127.0.0.1:8000")
	if err != nil {
		fmt.Printf("err: %v\n", err)
		return
	}
	defer conn.Close()

	//接收服务器的数据
	go func() {
		str := make([]byte, 1024)
		for {
			//从键盘输入内容-给服务器发送内容
			n, err3 := os.Stdin.Read(str)
			if err3 != nil {
				fmt.Printf("err3: %v\n", err3)
				continue
			}
			// fmt.Printf("string(str[:n]): %v\n", string(str[:n]))
			//发送信息到服务器
			conn.Write(str[:n])
			if string(str[:n]) == "exit" {
				return
			}
		}
	}()
	buf := make([]byte, 1024)
	for {
		n, err2 := conn.Read(buf)
		if err2 != nil {
			fmt.Printf("err: %v\n", err)
			continue
		}
		fmt.Printf("string(buf[:n]): %v\n", string(buf[:n]))
	}
}

Go语言并发编程

前言

进程和线程

 A. 进程是程序在操作系统中的一次执行过程,系统进行资源分配和调度的一个独立单位。
 B. 线程是进程的一个执行实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。
C.一个进程可以创建和撤销多个线程;同一个进程中的多个线程之间可以并发执行。

并发和并行

    A. 多线程程序在一个核的cpu上运行,就是并发。
    B. 多线程程序在多个核的cpu上运行,就是并行。

并发

并发

并行

并行

协程和线程

协程:独立的栈空间,共享堆空间,调度由用户自己控制,本质上有点类似于用户级线程,这些用户级线程的调度也是自己实现的。
线程:一个线程上可以跑多个协程,协程是轻量级的线程。

goroutine 只是由官方实现的超级"线程池"。

每个实力4~5KB的栈内存占用和由于实现机制而大幅减少的创建和销毁开销是go高并发的根本原因。

并发不是并行:

并发主要由切换时间片来实现"同时"运行,并行则是直接利用多核实现多线程的运行,go可以设置使用核数,以发挥多核计算机的能力。

goroutine 奉行通过通信来共享内存,而不是共享内存来通信。

Go 语言支持并发,我们只需要通过 go 关键字来开启 goroutine 即可。

Goroutine

在java/c++中我们要实现并发编程的时候,我们通常需要自己维护一个线程池,并且需要自己去包装一个又一个的任务,同时需要自己去调度线程执行任务并维护上下文切换,这一切通常会耗费程序员大量的心智。那么能不能有一种机制,程序员只需要定义很多个任务,让系统去帮助我们把这些任务分配到CPU上实现并发执行呢?

Go语言中的goroutine就是这样一种机制,goroutine的概念类似于线程,但 goroutine是由Go的运行时(runtime)调度和管理的。Go程序会智能地将 goroutine 中的任务合理地分配给每个CPU。Go语言之所以被称为现代化的编程语言,就是因为它在语言层面已经内置了调度和上下文切换的机制。

在Go语言编程中你不需要去自己写进程、线程、协程,你的技能包里只有一个技能–goroutine,当你需要让某个任务并发执行的时候,你只需要把这个任务包装成一个函数,开启一个goroutine去执行这个函数就可以了,就是这么简单粗暴。

goroutine 是轻量级线程,goroutine 的调度是由 Golang 运行时进行管理的。

goroutine 语法格式:

go 函数名( 参数列表 )

例如:

go f(x, y, z)

开启一个新的 goroutine:

f(x, y, z)

Go 允许使用 go 语句开启一个新的运行期线程, 即 goroutine,以一个不同的、新创建的 goroutine 来执行一个函数。 同一个程序中的所有 goroutine 共享同一个地址空间。

例子

package main

import (
        "fmt"
        "time"
)

func say(s string) {
        for i := 0; i < 5; i++ {
                time.Sleep(100 * time.Millisecond)
                fmt.Println(s)
        }
}

func main() {
        go say("world")
        say("hello")
}

执行以上代码,你会看到输出的 hello 和 world 是没有固定先后顺序。因为它们是两个 goroutine 在执行:

world
hello
hello
world
world
hello
hello
world
world
hello

例子

package main
import(
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"time"
)
func responseSize(url string){
    fmt.Println("Step:",url)
    respomse,err := http.Get(url)
    if err != nil{
        log.Fatal(err)
    }
    fmt.Println("Step2:",url)
    defer response.Body.Close()
    
    fmt.Println("Step:",url)
    bodt,er := ioutil.ReadAll(response.Body)
    if err!= nil{
        log.Fatal(err)
    }
    fmt.Println("Step4:",len(body))
}
func main(){
    go responseSize("https://www.duoke360.com")
    go responseSize("https://baidu.com")
    go responseSize("https://jd.com")
    time.Sleep(10*time.Second)
}

通道(channel)

通道(channel)是用来传递数据的一个数据结构。

  • 共享数据
  • 在任何给定时间只有一个goroutine可以访问数据项,因此不会发生数据竞争。
  • 无缓存--同步 有缓存--异步

通道可用于两个 goroutine 之间通过传递一个指定类型的值来同步运行和通讯。操作符 <- 用于指定通道的方向,发送或接收。如果未指定方向,则为双向通道。

ch <- v    // 把 v 发送到通道 ch
v := <-ch  // 从 ch 接收数据
           // 并把值赋给 v

声明一个通道很简单,我们使用chan关键字即可,通道在使用前必须先创建:

ch := make(chan int)

注意:默认情况下,通道是不带缓冲区的。发送端发送数据,同时必须有接收端相应的接收数据。

以下实例通过两个 goroutine 来计算数字之和,在 goroutine 完成计算后,它会计算两个结果的和:

特性

  1. 对于同一个通道,发送操作之间是互斥的,接收操作之间也是互斥的。
  2. 发送操作和接收操作中对元素值的处理都是不可分割的。
  3. 发送操作在完全之前会被阻塞。接收操作也是如此。

例子

package main

import (
	"fmt"
	"math/rand"
	"time"
)

var values = make(chan int)

func send() {
	rand.Seed(time.Now().UnixNano())
	value := rand.Intn(10)
	fmt.Printf("send:%v\n", value)
	values <- value
}
func main() {
	defer close(values)
	go send()
	fmt.Printf("wait...")
	value := <-values
	fmt.Printf("value: %v\n", value)
	fmt.Println("end...")
}

[Running] go run "d:\桌面\GOlearn\test\main.go"
wait...send:2
value: 2
end...

[Done] exited with code=0 in 2.984 seconds

通道缓冲区

  • 无缓冲区--同步通道
  • 带缓存区--异步通道

通道可以设置缓冲区,通过 make 的第二个参数指定缓冲区大小:

ch := make(chan int, 100)

带缓冲区的通道允许发送端的数据发送和接收端的数据获取处于异步状态,就是说发送端发送的数据可以放在缓冲区里面,可以等待接收端去获取数据,而不是立刻需要接收端去获取数据。

不过由于缓冲区的大小是有限的,所以还是必须有接收端来接收数据的,否则缓冲区一满,数据发送端就无法再发送数据了。

注意:如果通道不带缓冲,发送方会阻塞直到接收方从通道中接收了值。如果通道带缓冲,发送方则会阻塞直到发送的值被拷贝到缓冲区内;如果缓冲区已满,则意味着需要等待直到某个接收方获取到一个值。接收方在有值可以接收之前会一直阻塞。

package main

import "fmt"

func main() {
    // 这里我们定义了一个可以存储整数类型的带缓冲通道
        // 缓冲区大小为2
        ch := make(chan int, 2)

        // 因为 ch 是带缓冲的通道,我们可以同时发送两个数据
        // 而不用立刻需要去同步读取数据
        ch <- 1
        ch <- 2

        // 获取这两个数据
        fmt.Println(<-ch)
        fmt.Println(<-ch)
}

执行输出结果为:

1
2

Go 遍历通道与关闭通道

Go 通过 range 关键字来实现遍历读取到的数据,类似于与数组或切片。格式如下:

v, ok := <-ch

如果通道接收不到数据后 ok 就为 false,这时通道就可以使用 close() 函数来关闭。

package main

import (
        "fmt"
)

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)
        // range 函数遍历每个从通道接收到的数据,因为 c 在发送完 10 个
        // 数据之后就关闭了通道,所以这里我们 range 函数在接收到 10 个数据
        // 之后就结束了。如果上面的 c 通道不关闭,那么 range 函数就不
        // 会结束,从而在接收第 11 个数据的时候就阻塞了。
        for i := range c {
                fmt.Println(i)
        }
}

输出

  0
  1
  1
  2
  3
  5
  8
  13
  21
  34

WaitGroup实现同步

var wg sync.WaitGroup
func hello(i int){
    // 和这个一样 defer wg.Add(-1)
    defer wg.Done()//goroutine结束就登记-1
    fmt.Println("hello Goroutine",i)
}
func main(){
    for i:=0;i<10;i++{
        wg.Add(1)//启动一个goroutine就登记+1
        go hello(i)
    }
    wg.Wait()//等待所有登记的goriutine都结束
}

runtime包

runtime.Gosched()

让出当前CPU时间片,重新等待安排任务

package main

import (
	"fmt"
	"runtime"
)

func show(s string) {
	for i := 0; i < 2; i++ {
		fmt.Println(s)
	}
}
func main() {
	go show("java")
	//主协程
	for i := 0; i < 2; i++ {
		//切一下,再次分配任务
		runtime.Gosched() //注释调查看结果
		fmt.Println("golang")
	}
}

[Running] go run "d:\桌面\GOlearn\test\main.go"
java
java
golang
golang

[Done] exited with code=0 in 2.345 seconds

注释调就可能出现go的协程没有执行

[Running] go run "d:\桌面\GOlearn\test\main.go"
golang
golang
java

[Done] exited with code=0 in 3.914 seconds

runtime.Goexit()

退出当前协程

package main

import (
	"fmt"
	"runtime"
	"time"
)

func show() {
	for i := 0; i < 10; i++ {
		if i >= 5 {
			runtime.Goexit()
		}
		fmt.Printf("i: %v\n", i)
	}
}
func main() {
	go show()
	time.Sleep(time.Second)
}

[Running] go run "d:\桌面\GOlearn\test\main.go"
i: 0
i: 1
i: 2
i: 3
i: 4

[Done] exited with code=0 in 3.108 seconds

runtime.GOMAXPROCS

CPU的核心数。1.5之前是默认使用1,现在默认使用最多的。

package main

import (
	"fmt"
	"runtime"
	"time"
)

func a() {
	for i := 0; i < 10; i++ {
		fmt.Println("A:", i)
	}
}
func b() {
	for i := 0; i < 10; i++ {
		fmt.Println("B:", i)
	}
}
func main() {
	fmt.Printf("runtime.NumCPU(): %v\n", runtime.NumCPU())
	runtime.GOMAXPROCS(2)
	go a()
	go b()
	time.Sleep(time.Second)
}

[Running] go run "d:\桌面\GOlearn\test\main.go"
runtime.NumCPU(): 12
A: 0
B: 0
B: 1
B: 2
B: 3
B: 4
A: 1
A: 2
A: 3
A: 4
A: 5
A: 6
A: 7
A: 8
A: 9
B: 5
B: 6
B: 7
B: 8
B: 9

[Done] exited with code=0 in 3.729 seconds

当最大进程数设置成1就变成了单线程执行了。

[Running] go run "d:\桌面\GOlearn\test\main.go"
runtime.NumCPU(): 12
B: 0
B: 1
B: 2
B: 3
B: 4
B: 5
B: 6
B: 7
B: 8
B: 9
A: 0
A: 1
A: 2
A: 3
A: 4
A: 5
A: 6
A: 7
A: 8
A: 9

[Done] exited with code=0 in 2.991 seconds

Mutex互斥锁

除了使用channel实现同步之外,还可以使用Mutex互斥锁的方式实现同步

package main

import (
	"fmt"
	"sync"
	"time"
)

var m int = 100
var lock sync.Mutex
var wt sync.WaitGroup

func add() {
	defer wt.Done()
	lock.Lock()
	m += 1
	time.Sleep(time.Millisecond * 10)
	lock.Unlock()
}
func sub() {
	defer wt.Done()
	lock.Lock()
	time.Sleep(time.Millisecond * 2)
	m -= 1
	lock.Unlock()
}
func main() {
	for i := 0; i < 100; i++ {
		go add()
		wt.Add(1)
		go sub()
		wt.Add(1)
	}
	wt.Wait()
	fmt.Printf("m: %v\n", m)
}

[Running] go run "d:\桌面\GOlearn\test\main.go"
m: 100

[Done] exited with code=0 in 6.623 seconds

channel的遍历

方法一:for循环+if判断

package main
import "fmt"
func main(){
    c:=make(chan int)
    go func (){
        for i := 0;i<10;i++{
            c<-i
        }
        close(c)
    }()
    for {
        if data,ok := <-c;ok{
            fmt.Printf("data:%v\n",data)
        }else{
            break
        }
    }
}

方法二:for range

package main
import "fmt"
func main (){
    c:= make (chan int)
    go func (){
        for i:=0;i<10;i++{
            c<-i
        }
        close(c)
    }()
    for v:=range c{
        fmt.Printf("v:%v\n",v)
    }
}

select switch

Select 语句具有以下特点。

  • 可处理一个或多个 channel 的发送/接收操作。
  • 如果多个 case 同时满足,select 会随机选择一个执行。
  • 对于没有 case 的 select 会一直阻塞,可用于阻塞 main 函数,防止退出。

分析

1.select是Go的一个控制结构,类似于switch语句,用于处理异常IO操作。select会监听case语句中的channel的读写操作,当case中channel读写操作为非阻塞状态(即能读写)时,将会触发的动作。

select中的case语句必须是一个channel操作

select中的default子句总是可运行的

2.如果有多个case都可以运行,select会随机公平的选出一个执行,其他不会执行

3.如果没有可以运行的case语句,且有default语句,那么就会执行default中的动作

4.如果没有可以运行的case语句,且没有default语句,select将阻塞,直到某个case通信可以运行

例子:

package main

import (
	"fmt"
	"time"
)

var chanInt = make(chan int)
var chanStr = make(chan string)

func main() {
	go func() {
		chanInt <- 100
		chanStr <- "hello"
		close(chanInt)
		close(chanStr)
	}()
	for {
		select {
		case r := <-chanInt:
			fmt.Printf("chanInt:%v\n", r)
		case r := <-chanStr:
			fmt.Printf("chanStr:%v\n", r)
		default:
			fmt.Println("default...")
		}
		time.Sleep(time.Second)
	}
}

[Running] go run "d:\桌面\GOlearn\test\main.go"
default...
chanInt:100
chanStr:hello
chanStr:
chanInt:0
chanStr:
chanStr:
chanInt:0

[Done] exited with code=1 in 8.597 seconds

Timer

Timer顾名思义,就是定时器的意思,可以实现一些定时操作,器的内部也是通过channel实现的。

例子:

package main 
import(
	"fmt"
    "time"
)
func main(){
    timer1 := time.NewTimer(timer.second*2)
    t1 := time.Now()
    fmt.Printf("t1:%v\n",t1)
    t2 := <- timer.C
    fmt.Printf("t2:%v\n",t2)
    
    //如果只是想单纯的等待的话,可以使用time.Sleep来实现
    timer2 :=time.NewTimer(timer.Second*2)
    <-timer2.C
    fmt.Println("2s后")
    
    timer.Sleep(timer.Second*2)
    <-timer2.C
    fmt.Println("再一次2s后")
    
    <-timer.After(timer.Second*2)//time.After函数的返回
    fmt.Println("再再一次2s后")
    
    timer3 := time.NewTimer(time.Second)
    go func(){
        <-timer3.C
        fmt.Println("timer 3 expried")
    }()
    
    stop := timer3.stop()//停止定时器
    //阻止timer事件的发生,当该函数执行后,timer计时器停止,相应的事件不在执行
    if stop {
        fmt.Println("Timer 3 stoped")
    }
    fmt.Println("before")
    timer4 := timer.NewTimer(time.Second*5)//原来设置5s
    tmer4.Reset(time.Second*1//重新设置时间,即修改NewTimer的时间
    <-timer4.C
}

这里的timer.C是一个通道channel,当要读取通道中的值时,由于创建时: time.NewTimer(timer.second*2)这是阻塞的--实现等待操作

type Timer struct{
    C <-chan Time
    r runtimerTimer
}

Ticker

Timer只执行一次,Ticker可以周期性执行

例子

package main

import (
	"fmt"
	"time"
)

func main() {
	ticker := time.NewTicker(time.Second)
	counter := 1
	for _ = range ticker.C {
		fmt.Println("ticker...")
		counter++
		if counter >= 5 {
			break
		}
	}
	ticker.Stop()
}

package main

import (
	"fmt"
	"time"
)

func main() {
	ticker := time.NewTicker(time.Second * 2)
	chanInt := make(chan int)
	go func() {
		for _ = range ticker.C {
			select {
			case chanInt <- 1:
			case chanInt <- 2:
			case chanInt <- 3:
			}
		}
	}()
	sum := 0
	for v := range chanInt {
		fmt.Printf("接收:%v\n", sum)
		sum += v
		if sum >= 10 {
			fmt.Printf("sum:%v\n", sum)
			break
		}
	}
}

原子变量

之前-sync.Mutex 加锁的方式

package main

import (
	"fmt"
	"sync"
	"time"
)

var m int = 100
var lock sync.Mutex
var wt sync.WaitGroup

func add() {
	defer wt.Done()
	lock.Lock()
	m += 1
	time.Sleep(time.Millisecond * 10)
	lock.Unlock()
}
func sub() {
	defer wt.Done()
	lock.Lock()
	time.Sleep(time.Millisecond * 2)
	m -= 1
	lock.Unlock()
}
func main() {
	for i := 0; i < 100; i++ {
		go add()
		wt.Add(1)
		go sub()
		wt.Add(1)
	}
	wt.Wait()
	fmt.Printf("m: %v\n", m)
}

改进后 sync.atmoic 原子操作 --CompareAndSwap

package main

import (
	"fmt"
	"sync/atomic"
	"time"
)

var i int32 = 100

func add() {
	//原子操作,内部  cas compare and swap   比较并且交换 old new
	atomic.AddInt32(&i, 1)
}
func sub() {
	atomic.AddInt32(&i, -1)
}
func main() {
	for i := 0; i < 10; i++ {
		go add()
		go sub()
	}
	time.Sleep(time.Second * 3)
	fmt.Printf("i: %v\n", i)
}

[Running] go run "d:\桌面\GOlearn\test\tempCodeRunnerFile.go"
i: 100

[Done] exited with code=0 in 4.139 seconds

原子操作-atomic

atomic提供的原子操作能够确保任一时刻只有一个goroutine对变量进行操作,善用atomic能够避免程序中出现大量的锁操作。

atmoic常见有:

  • 增减
  • 载入 read
  • 比较并交换cas
  • 交换
  • 存储 write

下面将分别介绍这些操作:

增删操作-Add

atomic包中提供了如下以Add为前缀的增减操作:

- func AddInt32(addr *int32,delta int32)
- func AddInt64(addr *int64,delta int64)
- func AddUint32(addr *uint32,delta uint32)
- func AddUint64(addr *uint64,delta uint64)
- func AddUintptr(addr*uintptr,delta uintptr)

载入操作-Load--读

此类操作的前缀为Load

- func LoadInt32(addr *int32,delta int32)
- func LoadInt64(addr *int64,delta int64)
- func LoadPointer(add *unsafe.Pointer)(val unsafe.Pointer)
- func LoadUint32(addr *uint32,delta uint32)
- func LoadUint64(addr *uint64,delta uint64)
- func LoadUintptr(addr*uintptr,delta uintptr)

载入操作能够保证原子的度变量的值,当读取的时候,任何其他CPU操作都无法对该变量进行读写,其实机制受到底层硬件的支持

比较并交换-cas

该操作简称CAS(Compare And Swap)。这类操作的前缀为CompareAndSwap

- func CompareAndSwapInt32(addr *int32,delta int32)(swapped bool)
- func CompareAndSwapInt64(addr *int64,delta int64)(swapped bool)
- func CompareAndSwapPointer(add *unsafe.Pointer)(val unsafe.Pointer)(swapped bool)
- func CompareAndSwapUint32(addr *uint32,delta uint32)(swapped bool)
- func CompareAndSwapUint64(addr *uint64,delta uint64)(swapped bool)
- func CompareAndSwapUintptr(addr*uintptr,delta uintptr)(swapped bool)

该操作在进行交换前首先确保变量的值并未被更改,仍然保持参数old所记录的值,满足此前提下才进行交换操作。CAS的做法类似操作数据库时常见的乐观锁机制

交换-swap

此类操的前缀为Swap

- func SwapInt32(addr *int32,new iny32)(old int32)
- func SwapInt64(addr *int64,new int64)(old int64)
- func SwapPointer(addr *unsafe.Pointer,new unsafe.Pointer)(old unsafe.Pointer)
- func SwapUint32(addr *uint32,new uint32)(old uint32)
- func SwapUint64(addr *uint64,new uint64)(old uint64)
- func SwapUintptr(addr *uintptr, new uintptr)(old uintptr)

相比较于CAS,明显此类操作更为暴力直接,并不管变量的旧值是否被改变,直接赋予新值然后返回背替换的值

存储-store--写

此类操作的前缀为Store:

- func StoreInt32(addr *int32,val int32)
- func StoreInt64(addr *int64,val int64)
- func StorePointer(add *unsafe.Pointer,val unsafe.Pointer)
- func StoreUint32(addr *uint32,val uint32 )
- func StoreUint32(addr *uint64,val uint64)
- func StoreUint64(addr *uintptr,val uintptr)

此类操作确保了写变量的原子性,避免其他操作读到了修改变量过程中的脏数据

例子:

package main

import (
	"fmt"
	"sync/atomic"
)

func test_add_sub() {
	var i int32 = 100
	// 增加操作
	atomic.AddInt32(&i, 1)
	fmt.Printf("i: %v\n", i)
	//减少操作
	atomic.AddInt32(&i, -1)
	fmt.Printf("i: %v\n", i)
}
func test_read_write() {
	var i int32 = 100
	// 读操作
	p := atomic.LoadInt32(&i)
	fmt.Printf("p: %v\n", p)
	// 写操作
	atomic.StoreInt32(&i, 200)
	fmt.Printf("i: %v\n", i)
}
func test_cas() {
	var i int32 = 100
	//比较后修改
	b := atomic.CompareAndSwapInt32(&i, i, 200) //修改的返回bool
	fmt.Printf("b: %v\n", b)
	fmt.Printf("i: %v\n", i)
}
func main() {

}

posted on   Steam残酷  阅读(50)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 零经验选手,Compose 一天开发一款小游戏!
· 一起来玩mcp_server_sqlite,让AI帮你做增删改查!!
点击右上角即可分享
微信分享提示