Go 语言入门指南:基础语法和常用特性解析 (下) | 青训营

284 阅读22分钟

go语言支持模块化的开发理念,在go语言中使用包来支持代码模块化和代码的复用性。一个包由多个go文件组成。

包的定义

我们可以根据需求自定义包,我们可以把包理解为一个存放go文件的文件夹。在该文件夹下的所有文件都必须在第一行声明该文件所属的包。

一个文件夹里的文件只能属于一个包内,同一个包的文件不能在多个包下。并且包名为main的包是应用程序的入口,通过编译这种包会获得一个可执行文件,而编译不包含main的包不会获得可执行文件。

package 包名

标识符可见性

在一个包下声明的标识符都属于同一命名空间,在不同包下的声明的标识符不属于同一空间。如果想在不同包下使用包内部的标识符需要带上包名前缀。例如fmt.Println()fmt包下的Println()函数。

如果想要让包内部的标识符能够被外部的包使用,我们需要将标识符首字母大写。在go语言中,首字母大写表示对外可见,首字母小写表示对外不可见。

package demo

//由于首字母小写,只能在当前包内使用
var private1 = 100

//首字母大写,可以在其他包中使用
const Public1 = 20

//首字母小写,只能在当前包下使用
type person struct{
    name string
    age int
}

//首字母大写,可以在其他包使用
type Student struct{
    Name string //可在保外访问的字段
    age int //只能在当前包访问的字段
}

//首字母大写,可以在其他包下使用
func Add(x, y int) int {
    return x+y
}

//首字母小写,只能在当前包下使用
func sub(x, y int) int {
    return x-y
}

包的引用

如果我们需要在当前包下引用其他包的内容,需要使用import进行引用。并且import通常放在package的下面。

package demo

import 包名 路径

import "fmt"
import "net/http"
import "strings"

批量引用

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

包起别名

import f "fmt"

func main() {
    f.Println("hello") //hello
}

当我们需要对一些文件进行初始化时,我们可以将初始化代码写在单独的一个包下,然后使用匿名引入的方式,对初始化代码所在包进行引入,匿名引入的包中的init函数将会被自动执行并且只执行一遍。

import _ "..."

init初始化

func init(){
    /...
}

我们无法主动调用init函数,当init所在包被执行时,它将按照声明顺序自动执行。

package main

import "fmt"

func init(){
    fmt.Println("init函数")
    sayHi()
}

func sayHi(){
    fmt.Println("hello")
}

func main(){
    fmt.Println("main函数")
}
//输出结果:
//init函数
//hello
//main函数

接口

接口是一种抽象的类型。接口相对于之前的具体类型,更像是一种约定,约定了一种类型有哪些方法。

接口类型

接口类型的定义

每个接口类型都由任意个方法签名组成。

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

//例如Writer方法的Write方法
type Writer interface {
    Write([]byte) error
}

接口实现的条件

接口就是规定的一个需要实现的方法列表。

//Singer接口
type Singer interface {
    Sing()
}

type dog struct{}

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

实现接口的原因

在我们的日常生活中,不管是猫还是狗,它们都有相同的行为,例如吃饭、睡觉等。那么为了把这些相同的行为方法一同处理,我们就可以使用到接口。我们只需要约定一个EatingSleeping,那么当我们需要这个方法时,只需要调用这个方法即可。并且这个方法不仅限于某个结构体独有的方法,所有结构体都可以去实现这个抽象方法。

type Eating interface {
   eat()
}

type Sleeping interface {
   sleep()
}

type cat struct {
   name string
}

type dog struct {
   name string
}

func (c cat) eat() {
   fmt.Printf("%v正在吃东西\n", c.name)
}

func (d dog) eat() {
   fmt.Printf("%v正在吃东西\n", d.name)
}

func (c cat) sleep() {
   fmt.Printf("%v正在睡觉\n", c.name)
}

func (d dog) sleep() {
   fmt.Printf("%v正在睡觉\n", d.name)
}

//我们可以直接通过接口类型进行调用,
//所有实现了eat方法的都会被作为Eating类型来处理
func eatMethod(e Eating) {
   e.eat()
}
//我们可以直接通过接口类型进行调用,
//所有实现了sleep方法的都会被作为Sleeping类型来处理
func sleepMethod(s Sleeping) {
   s.sleep()
}

func main() {
   c := cat{"小猫"}
   d := dog{"小狗"}
   eatMethod(c) //小猫正在吃东西
   eatMethod(d) //小狗正在吃东西
   sleepMethod(c) //小猫正在睡觉
   sleepMethod(d) //小狗正在睡觉

   var e Eating
   e = c
   eatMethod(e) //小猫正在吃东西
   e = d
   eatMethod(e) //小狗正在吃东西
}

类型和接口的关系

一个类型实现多个接口

我们拿狗举例,狗不仅能吃,还能睡觉,那么我们定义EatingSleeping两个接口,狗既能实现Eating接口,又能实现Sleeping接口。

type Eating interface {
   eat()
}

type Sleeping interface {
   sleep()
}

type dog struct {
   name string
}

func (d dog) eat() {
   fmt.Printf("%v正在吃东西\n", d.name)
}

func (d dog) sleep() {
   fmt.Printf("%v正在睡觉\n", d.name)
}

func main() {
    d := dog{"小白"}
    var e Eating = d
    var s Sleeping = d
    e.eat() //小白正在吃东西
    s.sleep() //小白正在睡觉
}

多个类型实现同一接口

不同类型也有相同行为的存在,那么此时,就存在多个类型实现同一接口的情况。

type Sleeping interface {
   sleep()
}

type cat struct {
   name string
}

type dog struct {
   name string
}

func (c cat) sleep() {
   fmt.Printf("%v正在睡觉\n", c.name)
}

func (d dog) sleep() {
   fmt.Printf("%v正在睡觉\n", d.name)
}

func main() {
   c := cat{"小猫"}
   d := dog{"小狗"}
   var s Sleeping
   s = c
   s.sleep() //小猫正在睡觉
   s = d
   s.sleep() //小狗正在睡觉
}

接口组合

接口和接口之间可以互相嵌套接口从而形成新的接口类型。例如go标准库中的io源码就有接口组合的实例。

// src/io/io.go

type Reader interface {
	Read(p []byte) (n int, err error)
}

type Writer interface {
	Write(p []byte) (n int, err error)
}

type Closer interface {
	Close() error
}

// ReadWriter 是组合Reader接口和Writer接口形成的新接口类型
type ReadWriter interface {
	Reader
	Writer
}

// ReadCloser 是组合Reader接口和Closer接口形成的新接口类型
type ReadCloser interface {
	Reader
	Closer
}

// WriteCloser 是组合Writer接口和Closer接口形成的新接口类型
type WriteCloser interface {
	Writer
	Closer
}

空接口

空接口就是一个没有任何方法的接口类型。也正因如此,任何类型都可以视为实现了空接口。所以空接口的变量可以用于存储任意类型的值。

func show(i interface{}) {
   fmt.Printf("类型: %T, 值: %v\n", i, i)
}

func main() {
   var i interface {}
   i = 10
   show(i) //类型: int, 值: 10
   i = "str"
   show(i) //类型: string, 值: str
   i = true
   show(i) //类型: bool, 值: true

   //空接口也可以作为map的值
   m := make(map[string]interface{})
   m["str1"] = 11
   m["str2"] = "str"
   m["str3"] = true
   fmt.Println(m) //map[str1:11 str2:str str3:true]
}

错误处理

go语言中跟其他语言不太一样,go语言中没有异常,只有错误,它将错误当成一种值来进行处理。

Error接口

go语言中有一个error接口来表示错误类型。并且error中只有Error一个方法。

type error interface{
    Error() string
}

//当一个函数或者方法需要返回错误时,我们通常把错误作为最后一个返回。
func Open(name string) (*File, error) {
    return OpenFile(name, O_RDONLY, 0)
}

//error是一个接口类型,因此它的默认零值是nil。我们也通常把错误于nil进行判断。
func main() {
file, err := os.Open(test.go")
    if err != nil {
        fmt.Println("open failed, err:", err)
        return
    }
}

自定义错误

我们通常可以根据需求自定义error。通常可以直接使用errors.New()来自定义一个错误。

func New(test string) error

func cal(n int) error {
    if n < 10 {
        err := errors.New("num is to small")
        return  err
    }
    return nil
}

func main(){
    n := 5
    fmt.Println(cal(n)) //num is to small
}

fmt.Errorf

当我们需要对格式化的错误描述信息时,我们可以使用fmt.Errorf

fmt.Errorf("err: %v\n", err)

//为了保证函数调用的错误链不丢失,我们可以使用%w
fmt.Errorf("err: %w\n", err)

反射

首先,我们需要先了解一下什么是反射。

反射是指在程序运行期间,对程序本身进行访问和修改的行为。程序在编译期间,变量会被转换成内存地址,变量名不会被编译器写到可执行部分。在运行程序时,程序也无法获取自身的信息。

通过反射我们可以写出很多灵活性极高的代码,但是这里并不推荐过多使用反射。反射产生的类型错误往往只会在真正运行时才会panic,并且反射的代码通常难以理解,而且反射的性能较低,基于反射实现的代码通常要比正常代码运行速度慢很多。

reflect包

在go语言的反射机制中,任何接口都是由具体类型和具体类型的值组成的。而go语言反射的相关功能都由其内置的reflect包提供,任意接口值在反射中都可以理解为由reflect.Typereflect.Value两部分组成。

TypeOf

go语言中,我们可以使用内嵌的reflect.TypeOf()来获得任意值的类型对象,程序可以通过类型对象来访问任意值的类型信息。

func reflectType(x interface{}) {
    v := reflect.TypeOf(x)
    fmt.Printf("%v\n", v)
}
func main() {
    var a float64 = 3.14
    reflectType(a) //float64
    var b int = 100
    reflectType(b) //int
}

反射中关于类型划分为两种:类型(Type)种类(Kind)。因为在Go语言中我们可以使用type关键字构造很多自定义类型,而种类(Kind)就是指底层的类型,但是在反射中,当我们需要区分指针、结构体等类型时,就会用到种类(Kind)。 举个例子,我们定义了两个指针类型和两个结构体类型,通过反射查看它们的类型和种类。

type myInt int64

func reflectType(x interface{}) {
   t := reflect.TypeOf(x)
   fmt.Printf("type:%v kind:%v\n", t.Name(), t.Kind())
}

func main() {
   var a *float32 // 指针
   var b myInt    // 自定义类型
   var c rune     // 类型别名
   reflectType(a) // type: kind:ptr
   reflectType(b) // type:myInt kind:int64
   reflectType(c) // type:int32 kind:int32

   type person struct {
      name string
      age  int
   }
   type book struct{ title string }
   var d = person{
      name: "张三",
      age:  18,
   }
   var e = book{title: "《跟博笙一起学习Go》"}
   reflectType(d) // type:person kind:struct
   reflectType(e) // type:book kind:struct
}

ValueOf

go语言中的reflect.ValueOf()返回的是reflect.Value类型,包括原始值的值信息。reflect.Value与原始值之间可以相互转换。

我们可以通过反射获取值。

func reflectValue(x interface{}) {
    v := reflect.ValueOf(x)
    k := v.Kind()
    switch k {
    case reflect.Int64:
        // v.Int()从反射中获取整型的原始值,然后通过int64()强制类型转换
        fmt.Printf("type is int64, value is %d\n", int64(v.Int()))
    case reflect.Float32:
        // v.Float()从反射中获取浮点型的原始值,然后通过float32()强制类型转换
        fmt.Printf("type is float32, value is %f\n", float32(v.Float()))
    case reflect.Float64:
        // v.Float()从反射中获取浮点型的原始值,然后通过float64()强制类型转换
        fmt.Printf("type is float64, value is %f\n", float64(v.Float()))
    }
}
func main() {
    var a float32 = 3.14
    var b int64 = 100
    reflectValue(a) 
    // type is float32, value is 3.140000
    reflectValue(b) 
    // type is int64, value is 100
    // 将int类型的原始值转换为reflect.Value类型
    c := reflect.ValueOf(10)
    fmt.Printf("type c :%T\n", c) 
    // type c :reflect.Value
}

我们还可以通过反射设置变量的值。

func reflectSetValue1(x interface{}) {
    v := reflect.ValueOf(x)
    if v.Kind() == reflect.Int64 {
        v.SetInt(200) //这里修改的是副本,reflect包会引发panic
    }
}
func reflectSetValue2(x interface{}) {
    v := reflect.ValueOf(x)
    // 反射中我们可以使用 Elem()方法获取指针对应的值
    if v.Elem().Kind() == reflect.Int64 {
        v.Elem().SetInt(200)
    }
}
func main() {
    var a int64 = 100
    reflectSetValue2(&a)
    fmt.Println(a) //200
    reflectSetValue1(a) 
    //panic: reflect: reflect.Value.SetInt using unaddressable value
}

结构体反射

任意值通过reflect.TypeOf()来获得反射对象的信息后,如果返回的类型是结构体,我们可以通过反射值对象reflect.TypeNumField()Field()方法来获得结构体成员的信息。

结构体相关方法

方法说明
Field(i int) StructField根据索引,返回索引对应的结构体字段的信息。
NumField() int返回结构体成员字段数量。
FieldByName(name string) (StructField, bool)根据给定字符串返回字符串对应的结构体字段的信息。
FieldByIndex(index []int) StructField多层成员访问时,根据 []int 提供的每个结构体的字段索引,返回字段的信息。
FieldByNameFunc(match func(string) bool) (StructField,bool)根据传入的匹配函数匹配需要的字段。
NumMethod() int返回该类型的方法集中方法的数目
Method(int) Method返回该类型方法集中的第i个方法
MethodByName(string)(Method, bool)根据方法名返回该类型方法集中的方法

StructField类型

StructField类型用来描述结构体中的字段的信息。

type StructField struct {
    name       string    //字段名字
    pkgPath    string    //字段的包路径
    type       Type      //字段类型
    tag        StructTag //字段标签
    offset     uintptr   //字段在结构体中的字节偏移量
    index      []int     //用于Type.FieldByIndex时的索引切片
    Anonymous  bool      //是否为匿名字段
}

并发

基本概念

首先,我们需要先了解一下关于并发编程的几个基础概念。

  • 串行:环环相扣,一个连着一个进行。
  • 并发:同一时间段内执行多个任务(一个处理器同时处理多个任务)。
  • 并发:同一时刻执行多个任务(多个处理器或者是多核的处理器同时处理多个不同的任务。)。
  • 进程(process):程序在操作系统中的一次执行过程,系统进行资源分配和调度的一个独立单位。
  • 线程(thread):操作系统基于进程开启的轻量级进程,是操作系统调度执行的最小单位。
  • 协程(coroutine):非操作系统提供而是由用户自行创建和控制的用户态 “线程” ,比线程更轻量级。

goroutine

go语言中执行并发的核心就是goroutine,每个goroutine都会占用一个非常小的栈。goroutine是go程序中最基本的并发执行单元,每个go程序都至少会包含一个goroutine用于main函数中,当go程序启动时它就会自动创建并启动。

go关键字

当我们在go语言中,想要创建一个goroutine非常简单,只需要在函数或者方法的调用前加上go关键字即可。

go foo() //创建一个goroutine运行函数foo

//匿名函数也可以通过go关键字创建goroutine来执行
go func(){
    ...
}()
func hello(){
    fmt.Println("hello")
}

func main(){
    go hello()
    fmt.Println("hi")
}
//输出结果:
//hi

上述代码中,我们会发现,通过goroutine的函数并未执行,这是为什么呢?因为我们的main方法执行完毕,那么其他所有goroutine不论执行到哪一步都会随之一起关闭。我们可以理解为我们在打一个Boss,这个Boss有很多分身,当我们击败Boss时,它的分身也会随之消失。

当然,我们可以对上述代码做些修改,使其全部执行完毕。我们这里使用的是time.Sleep()函数来使main函数在此停留一秒钟。

func hello(){
    fmt.Println("hello")
}

func main(){
    go hello()
    fmt.Println("hi")
    time.Sleep("time.Second")
}
//输出结果:
//hi
//hello

我们对上述代码加以修改后会发现,结果有时候不一定准确,还会存在hello()函数未执行的情况。这里我们还可以使用sync包下的WaitGroup来进行优化。

var wg sync.WaitGroup

func hello(){
    fmt.Println("hello")
    wg.Done() //结束一个goroutine
}

func main(){
    wg.Add(1) //手动添加一个goroutine去执行
    go hello()
    fmt.Println("hi")
    wg.Wait() //阻塞等待全部goroutine完成
}

当然,我们还可以启动多个goroutine。

var wg sync.WaitGroup

func hello(i int){
    fmt.Println("hello", i)
    defer wg.Done() //结束一个goroutine
}

func main(){
    for i:=0; i<5; i++ {
        wg.Add(1) //手动添加一个goroutine去执行
        go hello(i)
    }
    wg.Wait() //阻塞等待全部goroutine完成
}
//输出结果:
//4
//1
//2
//3
//0

这里我们会发现每次输出的结果都不是固定的,都是随机的,这是因为goroutine是并发执行的,因此goroutine的调度也是随机的。

channel

go语言采用的并发模型是CSP(communicating sequential process),提倡通过通信共享内存而不是通过内存共享而实现通道。

go语言中的channel就是goroutine的连接通道,使得goroutine发送特定值到另一个goroutine中。

go语言的channel是一种特殊的类型.我们可以将其理解为一个队列,遵循着先入先出的规则。每一个通道都有一个具体类型的导管,当我们在声明这条通道时也需要为其指定元素类型。

channel声明

var 变量名称 chan 元素类型

var c1 chan int    //声明一条传递整数的通道
var c2 chan string //声明一条传递字符串的通道
var c3 chan []int  //声明一条传递int切片的通道

channel零值

当我们声明通道类型后,并未对其进行初始化,则它的默认零值为nil

var ch chan int
fmt.Println(ch) //nil

初始化channel

我们可以使用make对通道进行初始化。

make(chan 元素类型, [缓冲大小])

c1 := make(chan int)
c2 := make(chan bool, 1)

channel操作

通道拥有发送、接收和关闭三种操作。发送和接收都需要使用<-符号来完成。

这里我们需要注意,通道值是可以被垃圾回收机制回收掉的。通道通常由发送方进行关闭操作,并且当接收方明确等待通道关闭的信号时才需要关闭操作。并且对于关闭的通道,我们不能再对其发送值,否则会导致panic;进行接收则会一直获取值直到通道为空为止。当对一个关闭且没有值的通道进行执行接收操作则会得到对应类型的零值。关闭一个已经关闭的通道会导致panic。

ch := make(chan int)

//发送
ch <- 10   //将10发送到ch中

//接收
x := <- ch     //从ch中接收值并赋值给x
<-ch           //从ch中接收值,忽略结果
fmt.Println(x) //10

//关闭
close(ch)

无缓冲通道

无缓冲通道也叫做阻塞的通道。

func main() {
   ch := make(chan int)
   ch <- 11
   fmt.Println("发送成功")
}
//输出结果:
//fatal error: all goroutines are asleep - deadlock!

我们可以看见,这段代码在执行的时候会引起deadlock的错误,deadlock表示goroutine都被挂起导致死锁。我们可以理解为,无缓冲的通道只有在接收方能够接收值的时候才可以发送,否则就会一直处于等待发送的阶段。同样的,当我们对一个无缓冲通道执行接收操作时,如果没有向通道发送值的操作时也会导致阻塞。我们可以通过增加一个接收方来解决上述问题。

func receive(ch chan int) {
    res := <-ch
    fmt.Println("接收成功", res)
}

func main() {
    ch := make(chan int)
    go receive(ch)
    ch <- 10
    fmt.Println("发送成功")
}
//输出结果:
//发送成功
//接收成功 10

我们可以看见,使用无缓冲通道可以使发送和接收的goroutine同步化,因此无缓冲通道也称为同步通道

有缓存通道

有缓存通道,顾名思义,就是我们在初始化时给它一个容量,只要通道内的元素小于等于该通道的容量时,它就不会引起错误,而那些发送接收的行为也会一直在通道内等待,等待另一个通道去接收或者发送。

func main() {
    ch := make(chan int, 1)
    ch <- 11
    fmt.Println("发送成功")
}
//输出结果:
//发送成功

单向通道

go语言中的通道如果我们只想让它进行接收或者发送,那么该怎么做呢?go语言提供了单向通道来处理我们这种需求。

<- chan int //只能接收通道
chan <- int //只能发送通道

// send 接收通道
// 返回值为一个接收通道
func send() <-chan int {
   ch := make(chan int, 2)
   // 创建一个新的goroutine执行发送数据的任务
   go func() {
      for i := 0; i < 5; i++ {
         if i%2 == 1 {
            ch <- i
         }
      }
      close(ch) // 任务完成后关闭通道
   }()

   return ch
}

// receive 接收通道
// 参数为一个接收通道
func receive(ch <-chan int) int {
   sum := 0
   for v := range ch {
      sum += v
   }
   return sum
}

func main() {
   ch2 := send()

   res2 := receive(ch2)
   fmt.Println(res2) // 4
}

select多路复用

当我们需要对多条通道进行处理时,我们可以使用select来完成对通道的处理。select的使用方式与switch非常类似,它们都有case分支和一个default默认分支。每个case都对应一个接收或者发送过程,select会一直等待,直到它的某个case完成对应的操作。select可以一次处理多个channel的发送和接收操作。如果同时有多个case被满足,那么select会随机选择一个case去执行。

select {
   case <-ch1:
      ...
   case data := <-ch2:
      ...
   case ch3<-data:
      ...
   default:
      默认
}

func main() {
   ch := make(chan int, 2)
   for i:=1;i<=10;i++ {
      select {
      case x:= <-ch:
         fmt.Println(x)
      case ch <- i:
      }
   }
   //输出结果(随机):
   //1
   //3
   //5
   //6
   //9
}

并发安全和锁

通过goroutine的学习后大家会发现,经常会出现多个goroutine处理同一个数据的情况发生。

var x int
var wg sync.WaitGroup // 等待组

// add 对全局变量x执行5000次加1操作
func add() {
   for i := 0; i < 5000; i++ {
      x++
   }
   wg.Done()
}

func main() {
   wg.Add(2)

   go add()
   go add()

   wg.Wait()
   fmt.Println(x)
}

通过上述代码我们可以发现,x的结果会出现6187,3476等情况,这就是因为两个goroutine同时对一个数据进行处理时,就会引起这个情况。我们换一种理解,因为goroutine是并发处理,那么当第一个goroutine执行第3000次的时候,第二个goroutine正好在这个时候执行第2500次,而当前x的值是5500,那么它们同时对5500自增,第一个goroutine自增后的值是5501,第二个goroutine自增后的值也是5501。因此就会导致结果与预期结果不符。

互斥锁

go语言中提供了一种互斥锁,常用于控制共享资源的访问。它能保证在同一时间只有一个goroutine在访问共享资源。

sync.Mutex

方法名功能
func (m *Mutex) Lock()获取互斥锁
func (m *Mutex) Unlock()释放互斥锁
//多个goroutine并发操作全局变量x
//Mutex 互斥锁

var x int
var wg sync.WaitGroup

//互斥锁
//同一时刻,只有一个goroutine可以访问资源
var lock sync.Mutex

func add() {
    for i := 0; i < 50000; i++ {
        //上锁
        lock.Lock()
        x++
        //解锁
        lock.Unlock()
    }
    wg.Done()
}

func main() {

    wg.Add(2)

    go add()
    go add()

    wg.Wait()

    fmt.Println(x)
    //输出结果:
    //100000
}

读写互斥锁

当我们在使用互斥锁的时候,我们会发现,互斥锁使我们的效率有所降低。当我们去读取一个资源的时候,没有必要为其增加互斥锁,只有当我们需要修改一个资源的时候,为了避免重复操作,我们才需要增加互斥锁。那么这个时候,我们就需要用到读写锁。

sync.RWMutex

方法名功能
func (rw *RWMutex) Lock()获取写锁
func (rw *RWMutex) UnLock()释放写锁
func (rw *RWMutex) RLock()获取读锁
func (rw *RWMutex) RUnLock()释放读锁
func (rw *RWMutex) RLock()返回一个实现Locker接口的读写锁
//RWMutex 读写锁,常用于读操作远大于写操作时
var x int
var wg sync.WaitGroup
var rwm sync.RWMutex
var lock sync.Mutex

func read() {
    rwm.RLock()
    time.Sleep(time.Millisecond)
    rwm.RUnlock()
    fmt.Println(time.Millisecond, "read")
    wg.Done()
}

func write() {
    rwm.Lock()
    x++
    time.Sleep(time.Millisecond * 10)
    rwm.Unlock()
    fmt.Println(time.Millisecond*10, "write", x)
    wg.Done()
}

func main() {
    start := time.Now()
    
    for i := 0; i < 10; i++ {
        wg.Add(1)
        read()
    }

    for i := 0; i < 10; i++ {
        wg.Add(1)
        write()
    }
    
    fmt.Println(time.Now().Sub(start))
} 

sync.WaitGroup

go语言中的sync.WaitGroup可以用于实现并发任务的同步操作。

方法名功能
func (wg * WaitGroup) Add(num int)计数器+num
(wg *WaitGroup) Done()计数器-1
(wg *WaitGroup) Wait()阻塞直到全部计数器归0

sync.Once

当我们需要某些操作在高并发的场景下必须执行一次时,我们就可以使用到sync.Once。例如配置文件的加载。

func (o *Once) Do(f func())

网络编程

互联网的核心是一系列协议,总称为”互联网协议”(Internet Protocol Suite),正是这一些协议规定了电脑如何连接和组网。我们理解了这些协议,就理解了互联网的原理。

socket编程

Socket是应用层与TCP/IP协议组信的中间软件抽象层。

TCP通信

TCP协议

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

TCP服务端

TCP服务端的处理流程为:监听端口,接收客户端请求建立链接,创建goroutine处理链接。

//tcp server
//服务端

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

func main() {

   listen, err := net.Listen("tcp", "127.0.0.1:15000")
   if err != nil {
      fmt.Println("listen failed, err:", err)
      return
   }
   for {
      coon, err := listen.Accept() //建立连接,接收数据
      if err != nil {
         fmt.Println("accept failed, err:", err)
         return
      }
      go process(coon) //启动一个goroutine处理连接
   }
}

TCP客户端

TCP客户端进行TCP通信流程:建立与服务端的链接,进行数据收发,关闭链接。

//tcp client
//客户端

func main() {
   coon, err := net.Dial("tcp", "127.0.0.1:15000")
   if err != nil {
      fmt.Println("err:", err)
      return
   }
   defer coon.Close() //关闭连接
   reader := bufio.NewReader(os.Stdin)
   for {
      input, _ := reader.ReadString('\n') //读取用户输入
      inputInfo := strings.TrimSpace(input)
      if strings.ToUpper(inputInfo) == "EXIT" { //如果输入exit就退出
         return
      }
      _, err := coon.Write([]byte(inputInfo)) //发送数据
      if err != nil {
         return
      }
      bytes := [512]byte{}
      read, err := coon.Read(bytes[:])
      if err != nil {
         fmt.Println("read failed, err:", err)
         return
      }
      fmt.Println(string(bytes[:read]))
   }
}

小结

作为go语言的最后一篇语法篇,我们可以从本文中学习到包、接口、错误处理、反射、并发和网络编程。希望小伙伴们在观看本文后能够有所收获。go的基础语法也就到此结束了,接下来我们也将踏入go的进一步学习当中。光会语法是远远不够的,我们还需要学习go的框架,和一些常用工具等。最后就祝愿小伙伴们学业有成,愿我们顶峰相见!

码字不易,如果您看到了这里,听我说谢谢您。

如果您觉得本文还不错,还请留下您小小的赞。

如果您看了本文有所感受,还请留下您宝贵的评论。