go

概述

  1. go能够更好地支持并发编程。

  2. go语言编译快、执行快、易于开发,具有一部分动态语言的特性,同时是面向对象的语言,很容易上手。

    Java编译快,执行速度一般。

    python这种动态语言虽然快速编译,但是执行速度一般。

    编译速度执行速度
    Java一般一般
    python
    c++
    go

    动态语言编译速度快,正是因为简单,编写简单,编译快,但是执行就慢,因为离底层远,需要翻译成可执行的机器码的时间就长,可以这么理解。

    Java是半编译半解释型语言,为了保证执行效率和响应速度。

    动态语言和脚本语言编译速度快

  3. 如果main包的源代码没有包含main函数,则会引发构建错误。

byte short int long float double char boolean String
    

JVM里,一个栈帧对应着一个方法调用。

  1. 指针 = 引用 = 内存地址

  2. 想要修改切片或者数组里面的值,要通过下标即索引来修改,通过for range循环。

    不要通过for range循环的第二个值value来进行修改,这个value只是拷贝,是修改不了数组或者切片里的值的。

  3. 切片是引用类型,切片的底层指向一个数组。

  4. func main() {
    
    	t := new(T) // t 是一个指向T的指针。
    	t.a = 5
    	t.b = 8
    }
    
    type T struct {
    	a int
    	b int
    }
    
  5. 内嵌类型是父类型

    外层类型是子类型

    内嵌是父类型,相当于继承了内嵌类型的所有属性和方法,类似于super()

  6. 在Go中,应用程序并发处理的部分被称作goroutines(协程)

    协程工作在相同的地址空间中,所以共享内存的方式一定是同步的(必须同步,不然会产生并发安全问题,因为协程共享内存)。

    Go使用channels来同步协程。

  7. 协程粒度更小,更轻量级

可以使用少量的操作系统线程就能拥有任意多个提供服务的协程。

  1. 协程可以运行在多个操作系统之间,也可以运行在线程之内。

  2. 协程是通过使用关键字go调用执行一个函数或者方法来实现的。

    这样会在当前的计算过程中开始一个同时进行的函数,在相同的地址空间中分配了独立的栈。

  3. 一个处理器,单核,仍然可以执行多个线程,只不过是通过处理器调度,交替执行,会进行线程上下文的切换,这就叫做并发,多个线程竞争处理器,处理器此时对于这多个线程来说叫做资源,处理器单核交替执行这多个线程,便是并发场景。

    处理器调度有几种方式,短作业优先,优先级别,先来先得等。

    并行是真正的多核处理器,一个核心负责一个线程,多个线程分别执行任务,他们同时执行,这是并行。

    并发,单核处理多个线程,如果调度方式得当,给外界的感觉就好像多个线程在并行执行一样,仍然有很好的并行执行效果。(实际上是交替执行,即并发。)

  4. 使用GOMAXPROCS

    在gc编译器下,必须设置GOMAXPROCS为一个大于默认值1的数值来允许运行时支持使用多于1个的操作系统线程,所有的协程都会共享同一个线程,除非将GOMAXPROCS设置为1个大于1的数值。

  5. 当main()函数返回的时候,程序退出:它不会等待任何其他非main协程的结束,这就是为什么在服务器程序中,每一个请求都会启动一个协程来处理,server()函数必须保持运行状态。

  6. 协程是独立的处理单元,代码逻辑必须独立于协程调用的顺序。

  7. 协程可以使用共享变量来通信,但是不提倡这样做,因为这种方式给所有的共享内存的多线程都带来了困难。

    GO有一种特殊的类型即通道--channel,这是一个可以发送类型化数据的管道,由其负责协程之间的通信,从而避开所有由共享内存导致的陷阱。

  8. 如果两个协程需要通信,必须给他们同一个通道作为参数才可以,这个通道必须是已初始化的。

  9. 对于同一个通道,发送操作,在接收者准备好之前是阻塞的,如果通道中的数据无人接收,就无法再给通道传入其他数据,新的输入无法在通道非空的情况下传入。

  10. 一个无缓冲通道只能包含1个元素。

    给通道提供缓存,可以在扩展的make命令中设置它的容量。

    buf := 100
    ch1 := make(chan string, buf)
    

    buf是通道可以同时容纳的元素个数。

    在缓冲满载之前,给一个带缓冲的通道发送数据是不会阻塞的,而从通道读取数据也不会阻塞,直到缓冲空了。

  11. 元素会按照发送的顺序被接收。如果缓冲容量是0或者未设置,通信仅在收发双方准备好的情况下才可以成功。

  12. golang,Exit函数可以让当前程序以给出的状体码code退出,一般来说,状态码0表示成功,非0表示出错,程序会立刻终止,并且defer的函数不会执行。

  13. golang遍历list

    for iter := listHaiCoder.Front(); iter != nil; iter = iter.Next() {
    		fmt.Println(iter.Value)
    	}
    
  14. golang将空接口类型interface{}转换成string类型

    func Strval(value interface{}) string {
    	var key string
    	if value == nil {
    		return key
    	}
    
    	switch value.(type) {
    	case float64:
    		ft := value.(float64)
    		key = strconv.FormatFloat(ft, 'f', -1, 64)
    	case float32:
    		ft := value.(float32)
    		key = strconv.FormatFloat(float64(ft), 'f', -1, 64)
    	case int:
    		it := value.(int)
    		key = strconv.Itoa(it)
    	case uint:
    		it := value.(uint)
    		key = strconv.Itoa(int(it))
    	case int8:
    		it := value.(int8)
    		key = strconv.Itoa(int(it))
    	case uint8:
    		it := value.(uint8)
    		key = strconv.Itoa(int(it))
    	case int16:
    		it := value.(int16)
    		key = strconv.Itoa(int(it))
    	case uint16:
    		it := value.(uint16)
    		key = strconv.Itoa(int(it))
    	case int32:
    		it := value.(int32)
    		key = strconv.Itoa(int(it))
    	case uint32:
    		it := value.(uint32)
    		key = strconv.Itoa(int(it))
    	case int64:
    		it := value.(int64)
    		key = strconv.FormatInt(it, 10)
    	case uint64:
    		it := value.(uint64)
    		key = strconv.FormatUint(it, 10)
    	case string:
    		key = value.(string)
    	case []byte:
    		key = string(value.([]byte))
    	default:
    		newValue, _ := json.Marshal(value)
    		key = string(newValue)
    	}
    
    	return key
    }
    

golang语言特性

  1. GOROOT表示源码包所在路径,即golang安装路径。

    这个用于配置环境变量。

  2. GOPATH,开发者Go项目默认路径,

    但是我们不可能只有一个Go项目,所以不推荐将GOPATH配置到环境变量,而是通过go modules管理项目,或者在控制台终端对每一个go项目进行单独的GOPATH配置。

  3. go语言有极简单的部署方式

    • 可以直接编译成机器码

      机器码是能够被CPU识别的二进制指令。

      字节码也是二进制,但是并不是机器码,并不能够被CPU识别,所以在JVM的执行引擎部分,还需要JIT即时编译器进行二次编译。

    • 不依赖于其他库

      最终生成的可执行程序,是一个静态的可执行文本文件

    • 直接运行

  4. golang是静态语言,动态语言如JavaScript,python是没有编译器的,是解释器,解释执行,所以效率可能不高。

    Java是半编译半解释型语言。

  5. 静态语言有编译器,我们通过go build指令进行编译,如果当前代码有问题,编译期间就能够排除很多问题。

  6. golang的并发是语言层面的并发。

    很多语言支持并发,但是是通过外层的包装,一层又一层来达到并发,但是golang是原生支持并发,从原始的语法就是天生支持并发的。

    golang能够充分利用多核,切换成本很低(通过协程实现并发,通过channel进行通信),尽量地提高cpu的并发的利用率。

  7. golang优势

    • runtime系统调度机制
    • 高效的gc
    • 丰富的标准库
  8. go编译指令

    go build xxx.go
    

    编译之后的可执行文件执行指令

    //直接通过 ./xxx(linux)或者 .\xxx.exe(windows)
    
  9. golang所有Exception都用Error来处理

  10. go run xxx.go //这是既包含了编译,也包含了运行!!
    我们也可以分步执行,就是先go build,再通过./xxx执行可执行文件
    
  11. golang中的语句,加分号和不加分号都可以,建议不加,编译器会自动给我们加。

golang语法

基础语法

  1. // 方法一:声明一个变量,默认赋初始值。
    var a int 
    // 方式二:在初始化的时候,可以省去数据类型,通过值自动匹配当前的变量的数据类型
    var c = 100
    fmt.Printf("type of c = %T\n", c)
    // 方式三:省去var关键字,直接自动匹配(常用)
    e := 100
    //这种声明方式,只能用在函数体内来声明。
    
  2. const

    //const来定义枚举类型
    const (
    	// 可以在const中添加关键字 iota,每行的iota都会累加1
    	// 第一行的iota默认是0
    	//BEIJING = 0
    	//SHANGHAI = 1
    	//SHENZHEN = 2
    	BEIJING = iota
    	SHANGHAI
    	SHENZHEN
    )
    
    func main() {
    	fmt.Println(BEIJING)
    	fmt.Println(SHANGHAI)
    	fmt.Println(SHENZHEN)
    }
    

    iota只能配合const()一起使用,iota只有在const进行累加效果

  3. 在golang中有两种定义变量的方式,分别为“常量赋值”,“自动推导类型常量”

    • 常量赋值语法格式:

      const 常量名称 数据类型 = 值

    • 自动推导类型常量:

      const 常量名称 = 值

  4. golang中使用常量注意事项:

    • 常量一经定义不可修改,比如再次赋值是不允许的

    • 不可以获取常量的内存地址

      在go中在变量前使用“&”可以获取变量的内存地址

    • 常量和变量在不同的内存存储区域:

      • 常量存储在数据区下面的常量区
      • 变量在栈区进行存储,但是在go中将堆和栈进行统一管理,称为虚拟内存区域
    • 为了将常量和变量有所区分,一般实际开发中,建议将常量的名称的所有字母大写。

  5. init函数与import导包

    image-20220727101615334

    init()函数执行的时机要早于main函数

    做变量的初始化,就可以在init()函数中进行操作

  6. golang的每个文件都属于一个包,go是以包的形式管理文件和项目结构的。

  7. 在导入包的时候,在导的包前面加下划线,表示导入这个包,但是不使用这个包,golang语法比较严格,不这么做的话,编译会报错。

    导入这个包,但是不使用这个包,说明只会执行这个包内部的init()方法。

    比较严格的意思是说,导入的包必须使用,不然会成为多余的代码。

    在导入的包前面可以定义包的别名。

  8. 值传递就是值的拷贝

    引用传递可以理解为地址的拷贝,传递的引用,参数为地址值,指向同一个内存地址的内容。

    指针和引用传递可以理解为同一个意思。

    image-20220727105416076

    *p = 10表示改变p这个指针(引用)所指向的地址空间的值(内容)。

    changeValue(&a)传的值是指针,为什么说golang只有值传递,其实这里也是值传递,为什么又说传的是指针相当于引用传递,这里&a表示指针即内存地址值,传&a表示指针的拷贝,即内存地址值的拷贝,就是值的拷贝。p就是地址值即&a,*p就表示p这个地址值所指向内存中的内容。

    p的类型是*int,说明存储的是地址值,即指针,这是一个意思。

    p作为changeValue函数的形参,相当于是已经声明了并做了初始化,如下。

    image-20220727110325089

    image-20220727110613817

  9. defer用来表示一个函数在执行最后,在结束之前要执行的语句。

    在同一个函数内,defer可以写多个。

    写在后面的defer先执行。

    image-20220727112542873

    return如果和defer出现在同一个函数中,return的语句要比defer的语句先执行。

数组及切片

  1. 定义数组的时候一定要指定数组的长度,数组是定长的,在物理内存空间上是连续的,这是数据结构的知识。在其他语言中也都是一样的。

    数组和链表是物理内存空间上真实存在的结构。

    而栈和队列则是受限线性表,是逻辑结构。

  2. 数组的长度也是数组变量的一部分

    不同长度的数组是不同的数据类型。

  3. 我们想在调用函数的时候传递数组参数时,应该写动态数组即切片,这和Java中传递数组这个引用类型变量的方式更加贴合,更加接近。

    在golang中,数组是值类型,并不是引用类型,而切片是引用类型,传递切片,能够指向底层的数组,这和Java中的方式更相近。

  4. 通过切片传递参数,是引用传递,而不是值传递。

    但是golang只有值传递,没有引用传递,所以这里实际上是传递的是地址值的拷贝!!所以可以理解为引用传递,不必过分纠结这点,切片作为数组引用指向底层的数组。

    传递切片的方式,可以修改切片中的内容,这就是因为引用传递。

    切片是动态数组是指可以通过append给切片添加数据,从而实现扩容。

    在golang中用切片更多,list都用得较少。

  5. 固定长度的数组在传参的时候是严格匹配数组类型的。

  6. 切片在传参上是引用传递,而且不同长度的动态数组,在传参时形参是一致的。

  7. 声明slice有多种方式。推荐用make的方式

    slice := make([]int, 3, 5)
    

    第二个参数表示切片长度,第三个长度表示容量。

    image-20220727125745938

  8. 当切片的长度等于切片的容量了,说明切片已满了。

    此时若追加元素至切片,golang会为切片开辟空间,开辟的空间大小和之前切片的容量一样。

    image-20220727130045041

    image-20220727130159380

  9. 切片的而长度和容量不同,长度表示左指针至右指针之间的距离,容量表示左指针至底层数组末尾的距离。

    切片的扩容机制:append的时候,如果长度增加后超过容量,则将容量扩充一倍(即扩至2倍)

  10. 切片的截取是左闭右开。

  11. //切片的截取:截取后的切片仍然和截取之前的切片是同一个引用,指向同一个底层数组,是同一个内存地址
    s := []int{1, 2, 3}
    s1 := s[0 : 2]
    //若此时修改s1,会发现s也被修改了,说明截取前后的两个切片是同一个引用
    

    image-20220727130805936

  12. 如果想让切片截取前后的切片分别指向各自的引用,不指向同一个引用,则用copy函数。

    s2 := make([]int, 3)
    copy(s2,s)
    

    将s中的值,依次拷贝到s2中,但是s2和s是指向不同地址。

map

  1. key-value键值对形式。

  2. 声明map也推荐用make的方式

    因为用make的方式能够使声明的map在底层有实际的内存空间,相当于已经初始化,这样增加了程序的健壮性,不容易出错。相当于代码规范。

    var myMap map[string]string

    这种方式仅仅是声明,并没有实际的内存空间。

    var myMap map[string]string
    if myMap == nil {
        fmt.Println("空map")
    }// 肯定会打印,因为myMap仅仅是声明,没有在底层分配实际的内存空间。
    
  3. 这和声明slice一样,如果直接采用var slice []int的方式声明,slice为nil,为后续出现空指针异常埋下隐患,声明map也不要通过这种方式,都通过make的方式来避免问题。

  4. 如果给map添加键值对的时候,数量达到了最初声明map时的容量,后续继续添加,会自动扩容,和slice是一样的。

    map里面的内容是无序的。

    推荐的声明map的方式:

    myMap := make(map[string]string)// 会自动初始化分配容量
    
    	mymap := make(map[string]string)
    	var mymap1 map[string]string
    	fmt.Println(mymap == nil)  // false
    	fmt.Println(mymap1 == nil) // true
    

    自动初始化map,且为map分配空间,避免空指针异常。

  5. 传递map,是引用传递,map是引用类型。

结构体struct

  1. type myint int,这是声明一种新的数据类型myint,是int的一个别名。

  2. 我们通过type这个关键字来定义结构体。

    把多种基本的数据类型组合在一起,变成一种复合的数据类型。相当于Java的类

  3. 给结构体定义方法的时候,要用结构体指针。

    image-20220727145125068

    golang中的结构体是值类型,要用指针,才能修改结构体中的“成员变量”。

  4. 如果类名首字母大写,表示其他包也能够访问。

    如果说类的属性首字母大写,表示该属性对外是能够访问的,否则只能够包的内部访问呢,相当于是private和public

  5. 示例

    type Human struct {
    	name string
    	sex  string
    }
    
    func (this *Human) Eat() {
    	fmt.Println(this.name, "eat")
    }
    func (this *Human) Walk() {
    	fmt.Println(this.name, "walk")
    }
    func main() {
    	//这种方式不能声明全局变量。
    	t := new(Human)
    	t.name = "zhangsan"
    	t.sex = "female"
    	t.Eat()
    	t.Walk()
    }
    
  6. 继承示例:

    type superMan struct {
        Human //superMan继承了父类Human的方法和属性
        level int
    }
    // 可以重定义父类的方法,也可以写子类自己的新方法。
    
  7. 接口里定义的所有方法必须被某个类全部实现,才能说这个类实现了此接口。

    接口里只有方法的定义,没有方法的具体实现。

    // 本质是一个指针
    type AnimalInterface interface {
       Sleep()
       GetColor() string
       GetType() string
    }
    
    type Cat struct {
       color string
    }
    
    func (this *Cat) Sleep() {
       fmt.Println("zzzzzzzzz.....")
    }
    func (this *Cat) GetColor() string {
       return this.color
    }
    func (this *Cat) GetType() string {
       return "Cat"
    }
    
    func main() {
    	var animal AnimalInterface
    	animal = new(Cat)
    	animal.Sleep()
    }
    

    左边是接口,右边是具体的类的对象,实例化对象。

    左边是抽象,右边是具体。把具体赋值给抽象,之后都通过抽象的接口来调用方法,而方法具体怎么实现,有多种形态,即依据接口指向哪个实例化对象。

  8. 空接口 interface{}

    int、string、float32、float64、struct....都实现了interface{}

  9. interface{}该如何区分此时引用的底层数据类型是什么?

    给interface{}提供类型断言的机制

    value, ok := arg.(string)
    

反射

  1. reflect包

    ValueOf用来获取输入参数接口中的数据的值,如果接口为空,则返回0

    TypeOf,用来动态获取输入参数接口中的类型,如果接口为空则返回nil

  2. 反射可以对一个已知变量,动态获取变量的value和type

json和结构体的转换

  1. type Movie struct {
    	Title  string   `json:"title"`
    	Year   int      `json:"year"`
    	Price  int      `json:"rmb"`
    	Actors []string `json:"actors"`
    }
    
    func main() {
    	movie := new(Movie)
    	movie.Title = "喜剧之王"
    	movie.Year = 2000
    	movie.Price = 10
    	movie.Actors = []string{"xingye", "zhangbozhi"}
    	jsonStr, err := json.Marshal(*movie)
    	if err != nil {
    		log.Println(err)
    	}
    	fmt.Printf("jsonStr = %s\n", string(jsonStr))
    	myMovie := new(Movie)
    	err = json.Unmarshal(jsonStr, myMovie)
    	fmt.Println(*myMovie)
    }
    
  2. 结构体标签应用:

    • json编解码
    • orm映射关系

golang高阶

goroutine

  1. 多线程/多进程,解决了CPU串行执行线程,而线程阻塞导致的CPU利用率低,吞吐量低的问题。

    单核CPU也能执行多线程,通过cpu的调度机制,时间片,先来先得,短作业优先等等,这在宏观上是并行,微观上是穿行,这就是并发,并没有真正的并行。

    多个核心,每个核心同时执行线程,这才是真正的并行。

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

  3. 把一个线程切分为用户线程和内核线程,用户线程保护业务层面的并发效果。

    实际上内核线程就是线程,用户线程就是go语言中协程的概念。

  4. 一个进程是一个运行在自己内存地址空间的独立执行体,一个进程由一个或多个操作系统线程组成,这些线程其实是共享同一个内存地址空间的。

    多线程,以便让用户或CPU不必等待,增加性能,提高CPU的吞吐量,一个并发程序可以在一个处理器或者内核上使用多个线程来执行任务,这就是并发。

    在Go中,应用程序并发处理的部分被称作协程。

  5. 协程工作在相同的地址空间中,所以共享内存的方式一定是同步的,这个可以通过sync包来实现。

  6. 协程是根据一个或多个线程的可用性,映射在他们之上的。

    image-20220805114256574

  7. 协程之间的通信方式通过管道,协程工作在相同的内存中,共享内存,这很危险,对于共享变量会产生并发安全问题,协程通过管道进行通信而不通过共享内存的方式进行通信。

    image-20220805114736596

  8. 使用协程进行并发,性能高的原因就是避免了经常进行线程之间的切换,降低了线程切换的成本,线程的上下文切换是需要耗费资源的,CPU时间,保存上下文等。所以在并发编程中,并不是说线程越多越好,当CPU为单核,多个线程并发执行时(微观上实际上是串行),线程之间的切换会耗费资源,虽然比进程之间的切换成本低。因为进程之间的切换,涉及到虚拟地址与物理地址之间的映射,涉及到查询cache等。

  9. image-20220805115145009

    一个P同一时刻只能去执行一个协程

    所以一个程序在某一时刻能并行执行的协程的最大数量就是GOMAXPROCS的值,就是P的数量。

    P是处理器,M是内核线程,也就是线程,G是协程。

    可以理解为协程映射在内核线程上执行。

  10. Go协程的调度器的设计策略

    • 复用线程

      • work stealing

        image-20220805115739642

        当G1正在被执行,而M2空闲,要把M2利用上,而M2的P的本地队列是没有协程的,就会去从M1的队列中去偷取(从队列的尾部偷取)。

      • hand off

        如果阻塞,阻塞的是协程和线程,不是P和CPU!!

        P去执行其他协程。

    • 利用并行

      GOMAXPROCS限定P的个数,这决定了并行执行的协程的最大数量

    • 抢占

      如果有其他协程在等待运行的话,当前协程只能最多执行10ms,就会被其他协程抢占CPU。所有协程平均分配CPU时间片。

    • 全局G队列

  11. main是主goroutine即主协程,其他协程是子协程。

    在Java中,main函数入口是线程,还有异常处理线程、垃圾回收线程。

    主协程也就是main结束之后,其他协程不管有没有执行完都会结束。

  12. 两个协程之间要进行通信 用channel机制,不要通过全局变量!

    而且协程之间是并行执行的,并行执行的最大协程数量由GOMAXPROCS决定,协程之间并行执行,那么子协程返回值不会被主协程即main拿到,他们之间是并行关系。

channel

  1. 作用:实现协程之间的通信。

    image-20220805154323775

  2. channel是一种数据类型,也是协程之间的通信机制。

  3. channel的定义方式

    • make(chan Type) // 等价于make(chan Type 0)
    • make(chan Type, capacity)
  4. channel的简单使用

    channel <- value // 发送value到channel
    // 从channel读数据有以下三种方式
    <- channel // 接收并将其丢弃
    x := <- channel // 从channel接收数据,并赋值给x
    x, ok := <- channel // 功能同上,同时检查channel是否关闭或者是否为空
    
  5. 无缓冲channel

    channel阻塞

    默认情况下,通信是同步且无缓冲的。

    在有接收者接收数据之前,向通道发送数据的协程会阻塞。因为一个无缓冲的通道在没有空间来保存数据的时候,必须要有一个接收者准备好接收通道的数据。所以通道的发送/接收操作在对方准备好之前是阻塞的。

    • 对于同一个通道,发送操作,在接收者准备好之前,也就是接收者接收数据之前,是阻塞的。因为如果channel中的数据无人接收,就无法再给通道传入其他数据,所以发送操作会等待通道再次变为可用状态,就是通道的值被接收的时候。
    • 对于同一个通道,接收操作是阻塞的,直到发送者发送了数据给通道。因为通道中没有数据,接收操作就会阻塞,知道通道中有数据,也就是发送者发送了数据。
  6. 带缓冲channel

    一个无缓冲通道只能包含 1个元素

    带缓冲通道是可以理解为消息队列,或者说消费者生产者模式。

    在缓冲存满数据之前,给一个带缓冲的通道发送数据是不会阻塞的,数据会存在缓冲区;从通道读数据也不会阻塞,直到缓冲区空。

    如果容量大于0,通道就是异步的了,缓冲满载之前,发送数据不阻塞,缓冲变空之前,接收数据不阻塞,元素会按照发送数据的顺序被接收。

    如果容量是0或者未设置,就是不带缓冲的通道,通信仅在收发双方准备好的情况下才可以成功。

    消息队列本质是个队列,这里的带缓冲通道,也很像队列。

  7. 关闭channel

    close是内置函数,可以关闭一个channel

    func main() {
    	c := make(chan int)
    
    	go func() {
    		for i := 0; i < 5; i++ {
    			c <- i
    		}
    		// close可以关闭一个channel
    		close(c)
    	}()
    
    	for {
    		// ok如果为true表示channel没有关闭,如果为false,表示channel已经关闭
    		// 每次在读数据之前会判断ok,也就是通道是否关闭
    		if data, ok := <-c; ok {
    			fmt.Println(data)
    		} else {
    			break
    		}
    	}
    	fmt.Println("main finished..")
    }
    
    • channel不像文件一样需要经常去关闭,只有当你确实没有任何发送数据了,才去关闭channel
    • 关闭channel后,无法向channel再发送数据
    • 关闭channel后,可以继续从channel接收数据
    • 对于nil channel,无论收发都会阻塞(这就是无缓冲channel,收发都会阻塞,直到对方发送或接收)
  8. channel与range

    for data := range {
        fmt.Println(data)
    }
    
  9. channel与select

    select可以完成监控多个channel的状态。

    每个case 表示监控每个channel

    如以下代码,select在尝试监控chan1和chan2

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

go modules模块管理

什么是go modules

  1. go modules是go语言的依赖解决方案。

    go modules目前集成在go的工具链中,只要安装了go,也就可以使用go modules了,而go modules的出现也解决了在go 1.11之前的几个常见争议问题:

    • go语言长久以来的依赖管理问题
    • 淘汰现有的go path使用模式
    • 统一社区中的其他依赖管理工具
  2. go modules的目的之一就是淘汰gopath,那么gopath是什么?

    gopath是表示当前golang全部项目所在路径。

    goroot是go语言源码包所在路径,对比JDK。

  3. go path路径下有三个文件夹

    • bin

      包含所有go代码已经编译过的可执行程序

    • pkg

      存放自定义包的目录

    • src

      存放项目源文件的目录

  4. gopath的弊端

    go get下载第三放库的时候,因为无法指定版本号,无版本控制概念。

  5. 使用go modules就可以不使用gopath

    用go modules来控制依赖。

  6. go mod命令

    命令作用
    go mod init生成go.mod文件
    go mod download下载go.mod文件中指明的所有依赖
    go mod tidy整理现有的依赖
    go mod graph查看现有的依赖结构
    go mod edit编辑go.mod文件
    go mod vendor导出项目所有的依赖到vendor目录
    go mod verify检验一个模块是否被篡改过
    go mod why查看为什么需要依赖某模块
  7. go mod 环境变量--GO111MODULE

    可以通过 go env命令查看

    go语言提供了GO111MODULE这个环境变量来作为go modules的开关,其允许设置以下参数:

    • auto : 只要项目包含了go.mod文件的话启用go modules
    • on:启用go modules,推荐设置,将会是未来版本中的默认值。
    • off:禁用go modules,不推荐设置。

    命令:

    $ go env -w GO111MODULE=ON
    
  8. GOPROXY

    这个环境变量主要是用于设置Go模块代理,其作用是用于使Go在后续拉取模块版本时直接通过镜像站点来快速拉取。

    $ go env -w GOPROXY=https://goproxy.cn,direct
    
  9. GOSUMDB

    用来校验拉取的第三方库是否是完整的,如果设置了GOPROXY,这个就不用设置了。

  10. GONOPROXY、GONOSUMDB、GOPRIVATE

    设置GOPRIVATE即可,他们表示一个意思,表示第三方库是私有仓库。

  11. 初始化项目

    • 任意文件夹创建项目

    • 创建go.mod文件 init

    • 在该项目编写源代码

      如果源代码中依赖某个库,我们可以手动download即用go get,也可以自动download

Last Updated:
Contributors: 陈杨