目录

学习路线

Golang学习路线图 - 来源:topgoer.com

基础扫盲

这里扫盲的意思是:列出一些我们觉得是golang关键的和独有的基础知识,其他语言相通的知识或容易理解的基础知识,不再赘述。

针对已经初步认识golang基础的童鞋,再次回顾并进一步认识转化

  • GOPATH

    在进行Go语言开发的时候,我们的代码总是会保存在$GOPATH/src目录下。在工程经过go build、go install或go get等指令后,会将下载的第三方包源代码文件放在$GOPATH/src目录下, 产生的二进制可执行文件放在 $GOPATH/bin目录下,生成的中间缓存文件会被保存在 $GOPATH/pkg 下。

    如果我们使用版本管理工具(Version Control System,VCS。常用如Git)来管理我们的项目代码时,我们只需要添加$GOPATH/src目录的源代码即可。bin 和 pkg 目录的内容无需版本控制。

  • vscode go扩展安装

    vscode go扩展安装

  • 可见性

    1)声明在函数内部,是函数的本地值,类似private

    2)声明在函数外部,是对当前包可见(包内所有.go文件都可见)的全局值,类似protect

    3)声明在函数外部且首字母大写是所有包可见的全局值,类似public

  • 变量声明方式

    var(声明变量), const(声明常量), type(声明类型) ,func(声明函数)

    Go的程序是保存在多个.go文件中,文件的第一行就是package XXX声明,用来说明该文件属于哪个包(package),package声明下来就是import声明,再下来是类型,变量,常量,函数的声明

  • 引用类型

    除了开发语言常有的值类型(boolean、string、int、array、struct等),golang的引用类型有slice序列序列数组、map映射、channel管道

    注意:数组array和结构体struct都是值类型

  • init函数

    package初始化必须执行

  • main函数

    默认入口函数

  • 运算符

    算数、关系、逻辑、赋值、位

    有取余运算符,没有取整运算符,需要通过math包实现取整:math.Ceil(f),math.Floor(f)

  • 下划线 _ 特殊标识

    基础含义是:忽略结果

    比如:import _ “helo”,仅仅是执行helo的init函数,而不需要导入整个helo包

    比如:f,_ := os.open(file),忽略返回的第二个变量,这里可以理解为占位符

  • 变量声明

    var 变量名 变量类型

    var name string = "9ong"
    
    var (
        a string
        b int
        c bool
    )
    
  • 初始化类型推导

    //这里没有声明变量类型,但编译器会根据右边的值推导变量的类型完成初始化
    var name = '9ong'
    
  • 短变量声明:=

    := 这个符号标识短变量声明,在golang中常用,其他语言少见有这样的标识符

  • 整型

    int8,8位,1个字节(byte)

    整型分为以下两个大类: 按长度分为:int8、int16、int32、int64,对应的无符号整型:uint8、uint16、uint32、uint64

    其中,uint8就是我们熟知的byte型,int16对应C语言中的short型,int64对应C语言中的long型。

    默认的int,根据系统字长,可能是4或8个字节,即32位或63位。同理uint

  • 复数

    complex64(实部与虚部各32位) complex128

  • 字符串

    默认UTF-8编码

  • 反引号支持多行字符串

    s1 := `第一行
    第二行
    第三行
    `
    fmt.Println(s1)
    
  • 字符串常用操作

    • 长度:len(str)
    • 拼接:+ 和 , pirntln(“xxx”,“dd"+"44”)
    • 分割:str.Split()
    • 包含:str.Contains()
    • join:str.Join()
    • 子串位置:str.Index()
    • 前缀/后缀:str.HasPrefix() str.HasSuffix()
  • rune类型

    用来处理unicode,比如中文、日文等,比如我们经常计算字符串长度,汉字在len函数计算时,默认是按照byte计算的,而UTF-8编码的汉字往往是3~4个byte(int32),所以len计算出来就不准确(php、python这些语言中也都存在类似的问题,也都通过额外参数或是转换类型后才能正确处理汉字字符串长度)

    在golang中可以通过range将带有中文的字符串转换成rune类型,再处理

  • 强制类型转换

    golang中没有隐式类型转换,需要手动强制换换,比如Sqrt函数接受的参数必须是float64,需要通过float64(a)转换整型变量a

  • 数组

    golang的数组有点特殊。

    在声明定义的时候,长度就固定了,而且值类型必须一致。数组是值类型,不是引用类型,就是说赋值和传参都是复制整个数组另开内存空间。

    元素item使用{}包起来

    var a [4]int = [4]int{1,2,3} //不足4个元素,用int默认值0补足
    var b = [...]int{1,2,3,4} //[...]表示初始化时确定长度
    var c = [...]int{0:1,1:2,2:3} //支持索引
    var dd = [...][2]int{{1,2,3},{3,2,1}} //多维数组    
    
  • 数组传参

    前面说数组是值类型的,传参时默认也是值类型传参;

    当然也可以通过&来实现数组引用传参,

    package main
    
    import "fmt"
    
    func sumArr(a [5]int) int {
        var sum int = 0
        for i := 0; i < len(a); i++ {
            sum += a[i]
        }
        return sum
    }
    func printArr(arr *[5]int) {
        arr[0] = 10
        for i, v := range arr {
            fmt.Println(i, v)
        }
    }
    
    func main() {        
        var arr1 [5]int
        sum := sumArr(arr1) //值类型
        printArr(&arr1) //引用类型
        fmt.Println(arr1)
        arr2 := [...]int{2, 4, 6, 8, 10}
        printArr(&arr2)
        fmt.Println(arr2)
    }
    

    是不是觉得golang的数组也太麻烦了,php的数组简单,正是因为php的解释型和动态性,使得php数组强大且编码简单,在性能上比不上golang在编译阶段就确定类型和长度的数组执行效率。

  • slice切片

    slice表面上是数组的子集,但slice其实是引用类型,通过内部指针和相关属性引用数组片段实现

    slice的语法和python的列表切片语法是类似的

    var a = [10]int{1,2,3,4,5}
    var slice1 = a[1:3]
    var slice2 = [:]
    

    slice同样适合使用len、cap等数组可使用的函数方法,但受限于数组,比如长度不可能超出原数组

    更多切片操作:

    s[n]    切片s中的索引位置为n的元素
    s[:]
    s[low:]
    s[:high]
    s[low:high]
    s[low:high:max] 从切片s的索引位置low到high获得切片,len=high-low,cap=max-low
    len(s)  切片长度
    cap(s)  切片容量
    

    注意:切片采用的是左闭右开方式,即包含索引low但不包含索引high的元素

  • make

    make是用于内存分配的,区别于new,它只用于slice、map以及chan的内存创建,而且它返回的类型就是这三个类型本身,而不是他们的指针类型,因为这三种类型就是引用类型,所以就没有必要返回他们的指针了

    make函数是无可替代的,我们在使用slice、map以及channel的时候,都需要使用make进行初始化,然后才可以对它们进行操作。

    make可以创建切片

    slice := make([]int,0,5) //创建切片:类型int、初始长度0、容量5
    
  • append

    切片追加

    var a = []int{1, 2, 3}
    fmt.Printf("slice a : %v\n", a)
    var b = []int{4, 5, 6}
    fmt.Printf("slice b : %v\n", b)
    c := append(a, b...)
    fmt.Printf("slice c : %v\n", c)
    d := append(c, 7)
    fmt.Printf("slice d : %v\n", d)
    e := append(d, 8, 9, 10)
    fmt.Printf("slice e : %v\n", e)
    
    

    注意:如果append后超出原切片的cap容量,将复制数据并分配一个新数组(重新分配地址,即切片与原数组已经不再引用同一个地址)

  • copy

    切片复制,函数 copy 在两个 slice 间复制数据,复制长度以 len 小的为准。两个 slice 可指向同一底层数组,允许元素区间重叠。

  • 字符串与切片

    string在底层是一个byte的数组,也支持切片操作

    对于静态编译语言,string定义后是不可变的,要修改字符,需要转换成[]byte(str)或[]rune(str)处理后,再强制转换成string。rune用于处理中文字符串,前面已经提及

    对于php动态脚本语言,string变量运行时是动态可变的,牺牲性能与空间来不麻烦php开发人员

  • 指针

    指针,从学编程开始,很多地方都在强调指针多重要,也不好理解,有常用到指针的语言通常都是比较难以精通的语言

    指针是什么?指针就是内存地址。

    在golang中,不能进行指针偏移和运算,属于安全性指针

    两个符号:

    • & :变量取址
    • * :指针取值

    三个概念:指针地址、指针类型、指针取值

    • 指针地址:指针,变量的内存地址
    • 指针类型:每个变量类型都对应一个指针类型,比如*string,*[4]int
    • 指针取值:通过内存地址取得地址存储的值(也是变量的值,因为变量的内存地址就是指针)
func main() {
    //指针取值
    a := 10
    b := &a // 取变量a的地址,将指针保存到b中
    fmt.Printf("type of b:%T\n", b)
    c := *b // 指针取值(根据指针去内存取值)
    fmt.Printf("type of c:%T\n", c)
    fmt.Printf("value of c:%v\n", c)
}
  • map

    map是一种无序的基于key-value的数据结构,Go语言中的map是引用类型,必须初始化才能使用

    • 定义map

      语法:make(map[KeyType]ValueType, [cap])

      func main() {
          scoreMap := make(map[string]int, 8){
              "小红":99
          }
          scoreMap["张三"] = 90
          scoreMap["小明"] = 100
          fmt.Println(scoreMap)
          fmt.Println(scoreMap["小明"])
          fmt.Printf("type of a:%T\n", scoreMap)
      }
      

      看了这个示例,有php开发者可能又要说php数组了,php数组可以是列表、对象、结构体,动态混合可调整,还能互相转换,不考虑编译/解释、空间效率问题,让php开发者会觉得很轻松。强类型语言就是要求变量定义的时候尽量明确类型、大小,因为在编译的时候,尽量要知道变量的类型及要分配的空间大小,减少运行时的检查与转换,提高运行时的效率。

    • 判断键值对是否存在

      func main() {
          scoreMap := make(map[string]int)
          scoreMap["小红"] = 90
          scoreMap["小明"] = 100
          // 如果key存在ok为true,v为对应的值;不存在ok为false,v为值类型的零值
          v, ok := scoreMap["小红"]
          if ok {
              fmt.Println(v)
          } else {
              fmt.Println("找不到")
          }
      }
      
    • 遍历

      采用迭代器range进行遍历,map遍历是无序的

    • 删除

      delete(map,key)

流程

  • for循环

    for i, n := 0, len(s); i < n; i++ { // 常见的 for 循环,支持初始化语句。
        println(s[i])
    }
    
  • range

    Golang range类似迭代器操作,返回 (索引, 值) 或 (键, 值)。

    类似于java迭代器,类似php的foreach迭代循环,类似于python的xrange,不需要开发者操心是否需要迭代器,甚至开发者都不知道迭代器这个概念。

    golang中range 格式可以对 slice、map、数组、字符串等进行迭代循环。格式如下:

    for key, value := range oldMap {
        newMap[key] = value
    }
    
  • switch

    golang的switch要注意一个break问题,case中不需要加break,匹配执行完后会直接终止,不需要等break

    
    switch var1 {
    case val1:
        ...
    case val2:
        ...
    default:
        ...
    }   
    

    虽然不需要break,但也可以实现case匹配多个值:

    switch marks {
      case 90: grade = "A"
      case 80: grade = "B"
      case 50,60,70 : grade = "C"
      default: grade = "D"  
    }    
    

    另外有个特殊TypeSwitch的语法:

    switch x.(type){
    case type:
       statement(s)      
    case type:
       statement(s)
    /* 你可以定义任意个数的case */
    default: /* 可选 */
       statement(s)
    }
    
  • break/continue

    continue、break配合标签(label)可用于多层循环跳出

    func SelectTest() {
    i := 0
    Loop:
        for {
            select {
            case <-time.After(time.Second * time.Duration(2)):
                i++
                if i == 5 {
                    fmt.Println("跳出for循环")
                    break Loop
                    //goto Loop2
                }
            }
            fmt.Println("for循环内 i=", i)
        }
    //Loop2:
        fmt.Println("for循环外")
    
    }
      
    
  • select

    select是golang一个很有意思的控制结构

    简单说:select 语句类似于 switch 语句,但是select会随机执行一个可运行的case。如果没有case可运行,它将阻塞,直到有case可运行。

    相当于操作系统的处理器管理进程,select(调度)一直检查哪个case(进程)处于就绪状态(得到资源从等待状态恢复),就执行哪个case

    如果其中的任意一个语句可以继续执行(即没有被阻塞),那么就从那些可以执行的语句中任意选择一条来使用。 如果没有任意一条语句可以执行(即所有的通道都被阻塞),那么有两种可能的情况:

    • 如果给出了default语句,那么就会执行default的流程,同时程序的执行会从select语句后的语句中恢复。
    • 如果没有default语句,那么select语句将被阻塞,直到至少有一个case可以进行下去。

    select的case语句必须是IO操作

    • 超时判断场景

      //比如在下面的场景中,使用全局resChan来接受response,如果时间超过3S,resChan中还没有数据返回,则第二条case将执行
      var resChan = make(chan int)
      // do request
      func test() {
          select {
          case data := <-resChan:
              doData(data)
          case <-time.After(time.Second * 3):
              //如果超过3秒,上一个case还未满足,将会执行本case,意味着已经超时(time下的定时器本质上也是channel实现)
              fmt.Println("request time out")
          }
      }
      
      func doData(data int) {
          //...
      }
      
    • 判断channel是否阻塞

      //在某些情况下是存在不希望channel缓存满了的需求的,可以用如下方法判断
      ch := make (chan int, 5)
      //...
      data=0
      select {
      case ch <- data:
      default:
          //一旦case不满足,就会马上进入default,也意味着case中的channel是阻塞的
          //做相应操作,比如丢弃data。视需求而定
      }
      
      

函数

  • 函数支持当做参数传递

  • 参数

    golang中默认是值传参,参数的改变不影响外部变量

    也支持引用传参,参数的改变影响外部变量(共用同一个地址)

    如何实现引用传参,注意&与*符号的使用:

    &:变量取址

    *:指针取值

    package main
    
    import (
        "fmt"
    )
    
    /* 定义相互交换值的函数 ,需要使得swap函数外的a、b值交换,swap实际上通过地址交换实现a、b的值交换*/
    func swap(x, y *int) {
        var temp int
    
        temp = *x /* 保存 x 的值 */
        *x = *y   /* 将 y 值赋给 x */
        *y = temp /* 将 temp 值赋给 y*/
    
    }
    
    func main() {
        var a, b int = 1, 2
        /*
            调用 swap() 函数
            &a 指向 a 指针,a 变量的地址
            &b 指向 b 指针,b 变量的地址
        */
        swap(&a, &b)
    
        fmt.Println(a, b)
    }
    
  • 支持可变形参

    语法:(args …Type)

    func test(s string, n ...int) string {
        var x int
        for _, i := range n {
            x += i
        }
    
        return fmt.Sprintf(s, x)
    }
    
  • 返回值

    • 下划线标识符用来忽略函数的某个返回值,前面你有介绍过

    • 裸返回

      我们更愿意叫他 缺省返回,有其他语言经验的开发者,就很容易从缺省值理解这个缺省返回,缺省返回即没有参数的return语句返回所有定义的返回变量的当前值

      func calc(a, b int) (sum int, avg int) {
          sum = a + b
          avg = (a + b) / 2
      
          return
      }
      
      func main() {
          var a, b int = 1, 2
          c := add(a, b)
          sum, avg := calc(a, b)
          fmt.Println(a, b, c, sum, avg)
      }
      
    • 不支持使用容器接受多返回值,只能用多个变量或_

  • defer延迟调用

      1. 关键字 defer 用于注册延迟调用。
      1. 这些调用直到 return 前才被执。因此,可以用来做资源清理。
      1. 多个defer语句,按先进后出的方式执行。可以理解成defer就是把操作注册到栈里,在return前从栈中取出依次执行,栈是先进后出的
      1. defer语句中的变量,在defer声明时就决定了

    defer的使用场景,一般有文件句柄关闭、资源释放,类似java/php中的类解析方法,在类销毁前执行,defer在一般在return前执行

    前面函数多个返回值,我们可以用defer通过闭包读取和修改

    defer 闭包,在renturn前一步执行

    package main
    
    func add(x, y int) (z int) {
        defer func() {
            z += 100
        }()
    
        z = x + y
        return z + 10 
        //执行顺序:z=x+y -> z+10 -> (defer z+100) -> return
    }
    
    func main() {
        println(add(1, 2))  //113
    }
    
  • defer与return

    package main
    
    import "fmt"
    
    func foo() (i int) {
    
        i = 0
        defer func() {
            fmt.Println(i)
        }()
    
        return 2
    }
    
    func main() {
        foo() //2
    }
    
    // 输出 2  ,因为return 2,这句中的2就已经先把2赋值给返回变量i了,而defer闭包都是取得当前变量最新值,也就是2,所以Println(i),就是输出2
    
  • 匿名函数

    有过js、php、python经验的童鞋,对匿名函数会比较熟悉

    func main() {
        getSqrt := func(a float64) float64 {
            return math.Sqrt(a)
        }
        fmt.Println(getSqrt(4))
    }   
    
  • 闭包

    有过js经验的童鞋,也会很熟悉

    golang的闭包也是类似的

    package main
    
    import "fmt"
    
    // 返回2个函数类型的返回值
    func test01(base int) (func(int) int, func(int) int) {
        // 定义2个函数,并返回
        // 相加
        add := func(i int) int {
            base += i
            return base
        }
        // 相减
        sub := func(i int) int {
            base -= i
            return base
        }
        // 返回
        return add, sub
    }
    
    func main() {
        f1, f2 := test01(10)
        // base一直是没有消
        fmt.Println(f1(1), f2(2))
        // 此时base是9
        fmt.Println(f1(3), f2(4))
    }
    

    延迟调用参数在注册时求值或复制,可用指针或闭包 “延迟” 读取。也就是说,只有闭包被执行时,才复制变量参数值(当前最新值,而不是定义时的值)

    和js类似,变量的当前值取的不是定义时的值,而是运行时的当前值

  • 递归

    所有语言的递归都类似

    • 子问题须与原始问题为同样的事,且更为简单。
    • 不能无限制地调用本身,至少有1个出口,化简为非递归状况处理。
  • 异常处理

    关键词:panicrecoverdefer

    异常和错误要有所区分,异常一般是指关键流程不可修复的错误,错误是指普通错误,甚至可以用于业务逻辑错误

    golang中抛出异常:panic函数

    在defer中通recover函数捕获异常得到err信息

    func test() {
        defer func() {
            if err := recover(); err != nil {
                println(err.(string)) // 将 interface{} 转型为具体类型。
            }
        }()
        if true{//这里可以根据需要条件判断是否抛出异常
            panic("panic error!")
        }
    }
    

    基于golang的异常函数panic、recover,我们可以稍微改装实现类似try的异常处理(当然golang中没有try关键字)

    以下示例,相当于封装了Try函数的实现,只是类似(还是java、js、php等语言的throw、try…catch…finally比较顺手,毕竟先入为主了哈)

    package main
    
    import "fmt"
    
    //Try函数定义了两个参数,第一个参数用于执行逻辑函数,第二个参数用于接收处理异常的函数
    func Try(fun func(), handler func(interface{})) {
        defer func() {
            if err := recover(); err != nil {
                handler(err)
            }
        }()
        fun()
    }
    
    func main() {
        Try(func() {
            panic("test panic")
        }, func(err interface{}) {
            fmt.Println(err)
        })
    }    
    

    这里新手可能会问golang是不是面向对象的语言,严格来说不是,官网自己也说了yes and no,可以有面向对象的编程思维,支持面向过程结构化编程,其实是不是面向对象不紧要,紧要的是能很好的处理好事情,php虽然现在也算是面向对象语言,但php开发者大部分没有面向对象的思想的,况且php也像golang一样借鉴了c、js、java等语言

    后面我们也会了解下golang中面向对象的一些基础知识

结构体

Go语言中没有“类”的概念,也没有“类”的继承等面向对象的概念。Go语言中通过结构体的内嵌再配合接口比面向对象具有更高的扩展性和灵活性。

  • type of

    获取变量的类型,类似js的语法type of

    var b int
    fmt.Printf("type of b:%T\n", b) //type of b:int
    
  • 类型别名

    语法:type alias=Type

    type rune = int32 //比如前面我们提到的rune就是int32的别名
    type myInt = int  //我们可以定义myInt的别名,具有int的特征
    
  • 类型定义

    语法:type NewType Type

    type NewInt int
    
  • struct自定义

    golang参考了很多c语言的语法,struct基本类似于c的struct

    type MyStruct struct{
        name string
        age int
    }
    
  • struct实例化

    虽然没有类(class),但golang中struct也需要实例化,才会分配内存空间。

    struct,我们可以理解成是一个抽象的数据结构,实例化就是使用数据通过这个数据结构进行具象化。如果有面向对象的经验,可以参考类和对象的关系

    匿名结构体,可以视为使用最原始的结构体。

    js的函数是函数,又可以算是结构体,也能像类,java、php有严格的类和函数区分,go的结构体代替了类,区别于函数

  • struct初始化

    
    type person struct {
        name string
        city string
        age  int8
    }
    
    func main(){
        p := person{
            name: "9ong.com",
            city: "广州",            
        }
        fmt.Printf("p=%#v\n", p) //p=main.person{name:"9ong.com", city:"广州", age:0}
    }
    
  • struct构造函数

    func newPerson(name, city string, age int8) *person {
        return &person{
            name: name,
            city: city,
            age:  age,
        }
    }
    
  • 嵌套结构体

    这很好理解,结构体中属性也是个结构体(类中的属性也是个类)

  • 结构体继承

    //Animal 动物
    type Animal struct {
        name string
    }
    
    func (a *Animal) move() {
        fmt.Printf("%s会动!\n", a.name)
    }
    
    //Dog 狗
    type Dog struct {
        Feet    int8
        *Animal //通过嵌套匿名结构体实现继承 指针类型
    }
    
    func (d *Dog) wang() {
        fmt.Printf("%s会汪汪汪~\n", d.name)
    }
    
    func main() {
        d1 := &Dog{
            Feet: 4,
            Animal: &Animal{ //注意嵌套的是结构体指针
                name: "旺财",
            },
        }
        d1.wang() //旺财会汪汪汪~
        d1.move() //旺财会动!
    }
    

    有点别扭诡异的写法,不自然(面向对象语言在趋近自然语言上还是有很多可取之处的)

  • 属性公开与私有

    结构体中字段大写开头表示可公开访问,小写表示私有(仅在定义当前结构体的包中可访问)。

    简单粗暴

  • 结构体与JSON

    在现代语言中,json是一种人类容易阅读和编写的轻量级的数据交换格式

    
    data,err1 := json.Marshal(s)
    
    //json转换成struct,需要定义并指定struct的结构
    type Class struct{
        title:"xxx"
        name:"xxxx"
    }        
    c1 = &Class{}
    err2 := json.Unmarshal([]byte(str),c1)
    

面向对象

方法

  • 方法

    方法区别于函数,在其他面向对象语言中,方法一般是指类中的方法块,只有类能调用,函数一般是单独结构块,从属于文件,可以单独调用。

    在golang中,函数也是文件中的单独可调用的代码块,而方法需要结合结构体struct一起使用,从属于结构体,但不定义在结构体中,属于半隐式定义方法

    语法:

    func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) {
        函数体
    }
    

    示例

    //Person 结构体
    type Person struct {
        name string
        age  int8
    }
    
    //NewPerson 构造函数 返回结构体的指针类型
    func NewPerson(name string, age int8) *Person {
        return &Person{
            name: name,
            age:  age,
        }
    }
    
    //Dream Person做梦的方法
    func (p Person) Dream() {
        fmt.Printf("%s的梦想是传递golang!\n", p.name)
    }
    
    // SetAge 设置p的年龄
    // 使用指针接收者
    func (p *Person) SetAge(newAge int8) {
        p.age = newAge
    }        
    
    func main() {
        p1 := NewPerson("goer", 25)
        p1.Dream()
        p1.SetAge(30)  //SetAge定义时使用了*Person指针类型接收者
        fmt.Println(p1.age) // 30
    }
    

    注意:指针类型接收者和值类型接收者,指针类型接收者就是引用类型接收者,类似于js中的this,php的$this引用,方法中修改了相应值,会影响外部结构体属性值

    什么时候使用指针类型接收者:

    • 1.需要修改接收者中的值
    • 2.接收者是拷贝代价比较大的大对象
    • 3.保证一致性,如果有某个方法使用了指针接收者,那么其他的方法也应该使用指针接收者。
  • 任意类型添加方法

    类型与js的prototype原型

    看示例更清楚:

    //MyInt 以int为原型定义自定义MyInt类型
    type MyInt int
    
    //为MyInt添加一个Say的方法
    func (m MyInt) Say() {
        fmt.Println("Hi, I am int.")
    }
    func main() {
        var m1 MyInt
        m1.Say() //Hi,I am int.
        m1 = 99
        fmt.Printf("%#v  %T\n", m1, m1) //99 main.MyInt
    }
    

接口

golang面向对象的知识并不多,但我们重点关注下接口

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

在Go语言中接口(interface)是一种类型,一种抽象的类型。记住是一种类型。

空接口类型的变量可以存储任意类型的变量。相当于java中的Object,所有对象都继承于Object。golang中interface{}表示任意类型都可以传进来,空接口都能处理。

接口教程很简单,新手会用,但不知道为什么哪里用,以下有两个很经典的示例,一个实现多态,一个实现反射:

  • 多态

    有面向对象经验的童鞋,知道封装、继承、多态,有助于阅读

    
    package main
    
    import "fmt"
    
    type Filter interface {
        About() string
        Process([]int) []int
    }
    
    // A UniqueFilter will remove duplicated numbers.
    type UniqueFilter struct{}
    func (uf UniqueFilter) About() string {
        return "remove diplicated numbers"
    }
    func (uf UniqueFilter) Process(inputs []int) []int {
        var outs = make([]int, 0, len(inputs))
        var pusheds = make(map[int]bool)
        for _, n := range inputs {
            if !pusheds[n] {
                pusheds[n] = true
                outs = append(outs, n)
            }
        }
        return outs
    }
    
    // A MultipleFilter will only keep numbers which are
    // multiples of the MultipleFilter as an integer.
    type MultipleFilter int
    func (mf MultipleFilter) About() string {
        return fmt.Sprintf("keep multiples of %v", mf)
    }
    func (mf MultipleFilter) Process(inputs []int) []int {
        var outs = make([]int, 0, len(inputs))
        for _, n := range inputs {
            if n % int(mf) == 0 {
                outs = append(outs, n)
            }
        }
        return outs
    }
    
    // With the help of polymorphism, only one "filteAndPrint"
    // function is needed.
    func filteAndPrint(fltr Filter, unfiltered []int) []int {
        // Call the methods of "fltr" will call the methods
        // of the value boxed in "fltr" acctually.
        filtered := fltr.Process(unfiltered)
        fmt.Println(fltr.About() + ":\n\t", filtered)
        return filtered
    }
    
    func main() {
        numbers := []int{12, 7, 21, 12, 12, 26, 25, 21, 30}
        fmt.Println("before filtering:\n\t", numbers)
    
        // Three non-interface values are boxed into three Filter
        // interface slice element values.
        filters := []Filter{
            UniqueFilter{},
            MultipleFilter(2),
            MultipleFilter(3),
        }
    
        // Each slice element will be assigned to the local variable
        // "fltr" (of interface type Filter) one by one. The value
        // boxed in each element will also be copied to "fltr".
        for _, fltr := range filters {
            numbers = filteAndPrint(fltr, numbers)
        }
    }
    
    
  • 反射

    反射就是动态的获取对象/结构体的信息 golang反射理解

    反射也可以理解成多态的一种实现方式,只不过对象/结构体是代码本身,比如golang中的其他类型实现空接口类型(空接口类型可以接受任意参数

    反射只是概念上高级了些,实际不复杂,也很自然的事。为什么编程语言要有反射呢?,这里说到如果静态语言没有反射的话,编译之后,类型都是固定的,将难以实现一些运行时的接受动态类型的函数等,比如Println(a),PrintLn是允许打印任何类型的变量的,有了反射,实现起来就轻松多了

    
    package main
    
    import "fmt"
    
    func main() {
        values := []interface{}{
            456, "abc", true, 0.33, int32(789),
            []int{1, 2, 3}, map[int]bool{}, nil,
        }
        for _, x := range values {
            // Here, v is declared once, but it denotes
            // different varialbes in different branches.
            switch v := x.(type) {
            case []int: // type literal
                // The type of v is []int.
                fmt.Println("int slice:", v)
            case string: // one type name
                // The type of v is string.
                fmt.Println("string:", v)
            case int, float64, int32: // multiple type names
                // The type of v is always same as x.
                // In this example, it is interface{}.
                fmt.Println("number:", v)
            case nil:
                // The type of v is always same as x.
                // In this example, it is interface{}.
                fmt.Println(v)
            default:
                // The type of v is always same as x.
                // In this example, it is interface{}.
                fmt.Println("others:", v)
            }
            // Note, each variable denoted by v in the
            // last three branches is a copy of x.
        }
    }
    
    

    在java/php等面向对象语言中,实现接口通常会有关键词implement,而在golang中接口的实现,属于隐式实现,结构体不需要和接口有特殊的关键词关联,只要结构体完全实现接口的所有方法,就实现了接口

测试

  • 单元测试

    在国内的环境,很多开发并不是不关注单元测试,而是公司或环境使得开发不能太关注单元测试。 - 看到有人说国内开发不关注单元测试

    golang测试工具:go test , 可进行单元测试和性能测试

    golang的单元测试,并不需要借助外部工具,也不需要学习新的规则或语法

    go test命令会遍历所有的*_test.go文件中符合上述命名规则的函数,然后生成一个临时的main包用于调用相应的测试函数,然后构建并运行、报告测试结果,最后清理测试中生成的临时文件。

    • 1、文件名必须以xx_test.go命名
    • 2、方法必须是Test[^a-z]开头
    • 3、方法参数必须 t *testing.T
    • 4、使用go test执行单元测试
    func TestSplit(t *testing.T) {
    // 定义一个测试用例类型
        type test struct {
            input string
            sep   string
            want  []string
        }
        // 定义一个存储测试用例的切片
        tests := []test{
            {input: "a:b:c", sep: ":", want: []string{"a", "b", "c"}},
            {input: "a:b:c", sep: ",", want: []string{"a:b:c"}},
            {input: "abcd", sep: "bc", want: []string{"a", "d"}},            
        }
        // 遍历切片,逐一执行测试用例
        for _, tc := range tests {
            got := Split(tc.input, tc.sep)
            if !reflect.DeepEqual(got, tc.want) {
                t.Errorf("excepted:%v, got:%v", tc.want, got)
            }
        }
    }
    
  • 测试覆盖率

    Go提供内置功能来检查你的代码覆盖率。我们可以使用go test -cover来查看测试覆盖率。例如:

    split $ go test -cover
    PASS
    coverage: 100.0% of statements
    ok      jm/test_demo/split       0.005s
    
  • 基准测试/压力测试

    测试CustomFunc函数性能

    go test -bench=CustomFunc 命令执行基准测试

    func BenchmarkCustomFunc(b *testing.B) {
        for i := 0; i < b.N; i++ {
            CustomeFunc("args")
        }
    }
    

    结果:

    split $ go test -bench=CustomFunc
    goos: darwin
    goarch: amd64
    pkg: jm/test_demo/custom
    BenchmarkSplit-8        10000000               167 ns/op
    PASS
    ok      jm/test_demo/split       1.8525s
    

更多关于glang测试

并发编程

并发

进程、线程、协程、并发、并行概念

协程goroutine

goroutine是golang的很好的机制

在Go语言编程中你不需要去自己写进程、线程、协程,需要让某个任务并发执行的时候,只需要把这个任务包装成一个函数,开启一个goroutine去执行这个函数就可以了,在调用函数的时候在前面加上go关键字,就可以为一个函数创建一个goroutine。

注意:在程序启动时,Go程序就会为main()函数创建一个默认的goroutine。当main()函数返回的时候该goroutine就结束了,所有在main()函数中启动的goroutine会一同结束。

func hello() {
    fmt.Println("Hello Goroutine!")
}
func main() {
    hello()
    fmt.Println("main goroutine done!")
}

func main() {
    go hello() // 启动另外一个goroutine去执行hello函数
    fmt.Println("main goroutine done!")
    time.Sleep(time.Second)
}


执行上面的代码你会发现,这一次先打印main goroutine done!,然后紧接着打印Hello Goroutine。

为什么main函数还加了time.Sleep,是因为前面我们说的main函数默认创建一个goroutine,一旦main函数创建的goroutine结束,所有在main函数中启动的goroutine也一同结束,而go hello(),创建协程需要一定时间,在这个时间里main函数就已经结束了,所以go hello()的协程也一并结束了,就不执行了。

Go语言中的操作系统线程和goroutine的关系:

  • 1.一个操作系统线程对应用户态多个goroutine。
  • 2.go程序可以同时使用多个操作系统线程。
  • 3.goroutine和OS线程是多对多的关系,即m:n。

多协程及同步实现sync.WaitGroup

同步实现一直都是通过同步锁实现协同同步,golang中sync包提供了sync.WaitGroup实现goroutine的同步,判断所有协程(异步)任务是否都已经完成(wg.Wait())

sync.WaitGroup有三个方法:

func (wg * WaitGroup) Add(delta int){}
func (wg *WaitGroup) Done(){}
func (wg *WaitGroup) Wait(){}
var wg sync.WaitGroup

func hello(i int) {
    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() // 等待所有登记的goroutine都结束
}

协程并发时,goroutine的调度是随时无序的,所以最后打印时不是按数字顺序的,和多核下线程并发类似

协程串行实现sync.Once

在编程的很多场景下我们需要确保某些操作在高并发的场景下只执行一次,例如只加载一次配置文件、只关闭一次通道等、只处理一次写操作等

sync.Once 有个do方法

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

sync.Once其实内部包含一个互斥锁和一个布尔值,互斥锁保证布尔值和数据的安全,而布尔值用来记录初始化是否完成。这样设计就能保证初始化操作的时候是并发安全的并且初始化操作也不会被执行多次。

var icons map[string]image.Image

var loadIconsOnce sync.Once

func loadIcons() {
    icons = map[string]image.Image{
        "left":  loadIcon("left.png"),
        "up":    loadIcon("up.png"),
        "right": loadIcon("right.png"),
        "down":  loadIcon("down.png"),
    }
}

// Icon 是并发安全的
func Icon(name string) image.Image {
    loadIconsOnce.Do(loadIcons)
    return icons[name]
}

sync.Map

Go语言中内置的map不是并发安全的。

var m = sync.Map{}

func main() {
    wg := sync.WaitGroup{}
    for i := 0; i < 20; i++ {
        wg.Add(1)
        go func(n int) {
            key := strconv.Itoa(n)
            m.Store(key, n)
            value, _ := m.Load(key)
            fmt.Printf("k=:%v,v:=%v\n", key, value)
            wg.Done()
        }(i)
    }
    wg.Wait()
}

runtime包

  • runtime.Gosched()

    协程让出CPU时间片,就绪状态,等待重新安排

  • runtime.Goexit()

    退出当前协程

  • runtime.GOMAXPROCS

    Go运行时的调度器使用GOMAXPROCS参数来确定需要使用多少个OS线程来同时执行Go代码。默认值是机器上的CPU核心数。

channel通道

  • channel创建
# make(chan 元素类型, [缓冲大小])

ch := make(chan int)
  • channel操作

通道有发送(send)、接收(receive)和关闭(close)三种操作。

发送和接收都使用<-符号。

无缓冲通道上的发送操作会阻塞,直到另一个goroutine在该通道上执行接收操作,这时值才能发送成功,两个goroutine将继续执行。相反,如果接收操作先执行,接收方的goroutine将阻塞,直到另一个goroutine在该通道上发送一个值。

无缓冲的通道:

func recv(c chan int) {
    ret := <-c
    fmt.Println("接收成功", ret)
}
func main() {
    ch := make(chan int)
    go recv(ch) // 启用goroutine从通道接收值
    ch <- 10
    fmt.Println("发送成功")
}

有缓冲的通道:

func main() {
    ch := make(chan int, 1) // 创建一个容量为1的有缓冲区通道
    ch <- 10
    fmt.Println("发送成功")
}

可以通过内置的close()函数关闭channel(如果你的管道不往里存值或者取值的时候一定记得关闭管道)

  • channel遍历

我们通常使用的是for range的方式判断通道是否被关闭,并从通道里遍历取值。

// 在主goroutine中从ch2中接收值打印
for i := range ch2 { // 通道关闭后会退出for range循环
    fmt.Println(i)
}
  • channel小结
channel nil 非空 空的 满了 没满
接收 阻塞 接收值 阻塞 接收值 接收值
发送 阻塞 发送值 发送值 阻塞 发送值
关闭 panic 关闭成功,读完数据后返回零值 关闭成功,返回零值 关闭成功,读完数据后返回零值 同上

goroutine池

goroutine池

select

前面流程控制里已经介绍过一次select,select用于同时从多个通道接收数据,select负责监听case里的所有通道,直到其中一个通道处于ready就绪状态

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

import (
   "fmt"
   "time"
)

func test1(ch chan string) {
   time.Sleep(time.Second * 5)
   ch <- "test1"
}
func test2(ch chan string) {
   time.Sleep(time.Second * 2)
   ch <- "test2"
}

func main() {
   // 2个管道
   output1 := make(chan string)
   output2 := make(chan string)
   // 跑2个子协程,写数据
   go test1(output1)
   go test2(output2)
   // 用select监控
   select {
   case s1 := <-output1:
      fmt.Println("s1=", s1)
   case s2 := <-output2:
      fmt.Println("s2=", s2)
   }
}

互斥锁sync.Mutex

在高并发场景,我们经常会遇到资源竞争问题,通常通过加锁及事务来解决并发导致数据读写错乱问题。

比如redis提供锁,sql提供事务

在golang中,多协程更容易出现资源竞争问题,golang在sync包中提供了锁机制

  • 互斥锁

互斥锁是一种常用的控制共享资源访问的方法,它能够保证同时只有一个goroutine可以访问共享资源。

var x int64
var wg sync.WaitGroup
var lock sync.Mutex

func add() {
    for i := 0; i < 5000; i++ {
        lock.Lock() // 加锁
        x = x + 1
        lock.Unlock() // 解锁
    }
    wg.Done()
}
func main() {
    wg.Add(2)
    go add()
    go add()
    wg.Wait()
    fmt.Println(x)
}
  • 读写互斥锁

互斥锁是完全互斥的,但是有很多实际的场景下是读多写少的,当我们并发的去读取一个资源不涉及资源修改的时候是没有必要加锁的,这种场景下使用读写锁是更好的一种选择。读写锁在golang中使用sync包中的 RWMutex 类型。

读写锁分为两种:读锁写锁。当一个goroutine获取读锁之后,其他的goroutine可以随时获取读锁,如果是获取写锁就要等待第一个goroutine的读锁释放;当一个goroutine获取写锁之后,其他的goroutine都要等待,不论是获取读锁还是写锁。

var (
    x      int64
    wg     sync.WaitGroup
    lock   sync.Mutex
    rwlock sync.RWMutex
)

func write() {
    // lock.Lock()   // 加互斥锁
    rwlock.Lock() // 加写锁
    x = x + 1
    time.Sleep(10 * time.Millisecond) // 假设读操作耗时10毫秒
    rwlock.Unlock()                   // 解写锁
    // lock.Unlock()                     // 解互斥锁
    wg.Done()
}

func read() {
    // lock.Lock()                  // 加互斥锁
    rwlock.RLock()               // 加读锁
    time.Sleep(time.Millisecond) // 假设读操作耗时1毫秒
    rwlock.RUnlock()             // 解读锁
    // lock.Unlock()                // 解互斥锁
    wg.Done()
}

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

    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go read()
    }

    wg.Wait()
    end := time.Now()
    fmt.Println(end.Sub(start))
}

原子操作atomic

互斥锁也是通过原子性解决并发安全问题,golang对于基础类型还提供了简单原子操作方式,比起互斥锁,性能更好。

golang原子操作由内置的标准库sync/atomic提供,目前支持更多的是计数级别的原子操作

var x int64
var l sync.Mutex
var wg sync.WaitGroup

// 普通版加函数
func add() {
    // x = x + 1
    x++ // 等价于上面的操作
    wg.Done()
}

// 互斥锁版加函数
func mutexAdd() {
    l.Lock()
    x++
    l.Unlock()
    wg.Done()
}

// 原子操作版加函数
func atomicAdd() {
    atomic.AddInt64(&x, 1)
    wg.Done()
}

func main() {
    start := time.Now()
    for i := 0; i < 10000; i++ {
        wg.Add(1)
        // go add()       // 普通版add函数 不是并发安全的
        // go mutexAdd()  // 加锁版add函数 是并发安全的,但是加锁性能开销大
        go atomicAdd() // 原子操作版add函数 是并发安全,性能优于加锁版
    }
    wg.Wait()
    end := time.Now()
    fmt.Println(x)
    fmt.Println(end.Sub(start))
}

常用标准库

输入与输出-fmt包

输入与输出

  • 常用输出函数

    Print、Printf、Println:直接输出内容

    Sprint、Sprintf、Sprintln:生成内容并返回字符串

    Fprint:将内容输出到一个io.Writer接口类型的变量,经常用于写入文件

    Errorf:根据format参数生成格式化字符串并返回一个包含该字符串的错误

    fmt.Println("打开文件出错,err:", err)//输出带换行
    s2 := fmt.Sprintf("name:%s,age:%d", name, age)//带格式生成并返回
    fmt.Fprintf(fileObj, "往文件中写如信息:%s", name)//带格式写入文件
    err := fmt.Errorf("这是一个错误")
    
  • 常用占位符

    占位符 说明
    %v 值的默认格式表示
    %+v 类似%v,但输出结构体时会添加字段名
    %#v 值的golang语法表示
    %T 打印值的类型
    %% 百分号
    %d 表示10进制数
    %b 表示2进制数
    %f 浮点数,有小数
    %9.2f 宽度9,精度2
    %e 科学计数法
    %s 直接输出字符串或[]byte
    %q 该值对应的双引号括起来的go语法字符串字面值,必要时会采用安全的转义表示
    %p 指针,表示未16进制,并加上前缀0x
  • 常用输入函数

    Scan、Scanf、Scanln:可以在程序运行过程中从标准输入获取用户的输入。

    Scanln比较常用:在终端扫描标准输入,以空格分隔,直到换行结束扫描

    fmt.Scanln(&name, &age, &married)
    fmt.Printf("扫描结果 name:%s age:%d married:%t \n", name, age, married)
    

    bufio.NewReader:获取完整输入内容

    FScan、Fscanf、Fscanln:从文件中获取输入

    Sscan、Sscanf、Sscanln:从字符串获取输入

时间与日期-time包

  • 时间与日期转换

    func timeDemo() {
        now := time.Now() //获取当前时间
        fmt.Printf("current time:%v\n", now)
    
        year := now.Year()     //年
        month := now.Month()   //月
        day := now.Day()       //日
        hour := now.Hour()     //小时
        minute := now.Minute() //分钟
        second := now.Second() //秒
        fmt.Printf("%d-%02d-%02d %02d:%02d:%02d\n", year, month, day, hour, minute, second)
    
        timestamp1 := now.Unix()     //时间戳
        timestamp2 := now.UnixNano() //纳秒时间戳
        fmt.Printf("current timestamp1:%v\n", timestamp1)
        fmt.Printf("current timestamp2:%v\n", timestamp2)   
          
        timeObj := time.Unix(timestamp1, 0)//时间戳转换成时间对象,再通过类似以上当前时间转换成时间格式
    
    }
    
  • 时间间隔(单位)

    time包中时间间隔的定义:

    const (
        Nanosecond  Duration = 1
        Microsecond          = 1000 * Nanosecond
        Millisecond          = 1000 * Microsecond
        Second               = 1000 * Millisecond
        Minute               = 60 * Second
        Hour                 = 60 * Minute
    )
    
    var _time = 10 * time.Second //10秒
    

    例如:time.Duration表示1纳秒,time.Second表示1秒。

  • 时间格式化

    golang的常用时间格式化模板并不是常见的:Y-m-d H:i:s,而是2006-01-02 15:04 ,这是24小时制,2006-01-02 03:04 PM,则是12小时制

    fmt.Println(now.Format("2006-01-02 15:04:05.000 Mon Jan"))
    // 12小时制
    fmt.Println(now.Format("2006-01-02 03:04:05.000 PM Mon Jan"))
    fmt.Println(now.Format("2006/01/02 15:04"))
    fmt.Println(now.Format("15:04 2006/01/02"))
    fmt.Println(now.Format("2006/01/02"))
    

    golang大佬,何必呢,增加跨语言语法记忆难度,虽然只是一点点,但如果其他地方也这样有’意思’,累计下难度就不小了。

  • 时间操作

    golang中的时间,并不是简单的数字加减,time包提供了实践操作的方法:

    • Add:时刻+时间段

      func main() {
          now := time.Now()
          later := now.Add(time.Hour) // 当前时间加1小时后的时间
          beforer := now.Add(-time.Hour) // 当前时间减1小时后的时间
          fmt.Println(later)
      }
      
    • Sub:时刻1 - 时刻2,求两个时间的差值,注意这里并不是(时刻-时间段)的实现,(时刻-时间段)仍然可以用Add(-时间段)来实现

    • Equal:判断时间是否相等,会考虑时区

    • Before:判断是否在某个时刻之前

    • After:判断是否在某个时刻之后

  • 定时器

    定时器,本质上是一个channel,golang中使用time.Ticker(duration)来设置定时器

    • 一次性定时器(延时)

      package main
      
      import (
          "fmt"
          "time"
      )
      
      func main() {
          /*
              用sleep实现定时器
          */
          fmt.Println(time.Now())
          time.Sleep(time.Second)
          fmt.Println(time.Now())
          /*
              用timer实现定时器
          */
          timer := time.NewTimer(time.Second)
          fmt.Println(<-timer.C)
          /*
              用after实现定时器
          */
          fmt.Println(<-time.After(time.Second))
      
      }
      
    • 周期性定时器

      func tickDemo() {
          ticker := time.Tick(time.Second) //定义一个1秒间隔的定时器
          //ticker := time.NewTicker(time.Second)
          for i := range ticker {
              fmt.Println(i)//每秒都会执行的任务
          }
      }
      

命令行参数解析-flag包

flag包是的golang开发命令行工具更为简单。

看一个完整示例,我们就更清楚flag的用途了:

执行命令时要求输入4个参数,并指定了参数的类型与默认值

func main() {
    //定义命令行参数方式1
    var name string
    var age int
    var married bool
    var delay time.Duration
    flag.StringVar(&name, "name", "张三", "姓名")
    flag.IntVar(&age, "age", 18, "年龄")
    flag.BoolVar(&married, "married", false, "婚否")
    flag.DurationVar(&delay, "d", 0, "延迟的时间间隔")

    //解析命令行参数
    flag.Parse()
    fmt.Println(name, age, married, delay)
    //返回命令行参数后的其他参数
    fmt.Println(flag.Args())
    //返回命令行参数后的其他参数个数
    fmt.Println(flag.NArg())
    //返回使用的命令行参数个数
    fmt.Println(flag.NFlag())
}

首先flag提供了命令行help功能,执行命令行会给出相应提示:

    $ ./flag_demo -help
    Usage of ./flag_demo:
      -age int
            年龄 (default 18)
      -d duration
            时间间隔
      -married
            婚否
      -name string
            姓名 (default "张三")

其次flag提供命令行参数parse解析能力:

注意:Args()\NArg()\NFlag()的含义

$ ./flag_demo -name pprof --age 28 -married=false -d=1h30m
pprof 28 false 1h30m0s
[]
0
4

当然如果我们仅仅只是需要简单命令行输入的参数,我们也可以简单的考虑os.Args来获取命令行参数。

os.Args是一个存储命令行参数的字符串切片,它的第一个元素是执行文件的名称,这和大部分语言命令行模式是类似的(python、php等)

日志-log包

官方标准简单log包,功能有限,更多可以实现流水账的日志记录,如果我们需要更多比如不同级别的日志记录,可以选择第三方日志库:logrus、zap等

//使用标准日志log,设置日志输出到xx.log文件,设置flags支持文件名、行号、日志前缀、时间格式
func main() {
    logFile, err := os.OpenFile("./xx.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
    if err != nil {
        fmt.Println("open log file failed, err:", err)
        return
    }
    log.SetOutput(logFile)
    log.SetFlags(log.Llongfile | log.Lmicroseconds | log.Ldate)    
    log.SetPrefix("[JM]")
    log.Println("这里记录一条日志。")
}
[JM]2020/01/14 15:32:45.431506 .../log_demo/main.go:13: 这里记录一条日志。

IO操作-os包

os包提供了Create、NewFile、Open、OpenFile、Remove方法

返回的文件对象,提供了读写方法,比如Write、WriteAt、WriteString、Read、ReadAt方法

  • 文件打开与关闭

    package main
    
    import (
        "fmt"
        "os"
    )
    
    func main() {
        // 只读方式打开当前目录下的main.go文件
        file, err := os.Open("./main.go")
        if err != nil {
            fmt.Println("open file failed!, err:", err)
            return
        }
        // 关闭文件
        file.Close()
    }
    

    golang可以考虑再简化下文件的打开及异常的处理和关闭,python的with open简化了try异常和close的处理,让代码更简洁,编写者不用去关心异常处理和文件关闭处理代码,这本不属于业务逻辑代码,趋势应该是让语言或机器自动实现了

  • 写文件

    file.WriteString("ab\n")
    file.Write([]byte("cd\n"))
    

    我们会更新一篇关于byte与string区别的文章

  • 读文件

    golang文件读取可以用file.Read()和file.ReadAt(),读到文件末尾会返回io.EOF的错误,EOF这是大部分语言读取结尾的标识符了

    golang的os包的读写还需要偏上层封装,不要让开发者去了解这么多读写的原理,比如读取一行、整个文件,这些是开发者更乐意用到的,不用关心读写的原理,至于性能开发者会认为是语言要解决的问题,当然当我们有足够经验以后,就可以使用更底层些的方法来实现,提高性能。如果硬件的发展更快,我们更希望大家都可以不关心底层实现的去使用更上层的方法

    package main
    
    import (
        "fmt"
        "io"
        "os"
    )
    
    func main() {
        // 打开文件
        file, err := os.Open("./xxx.txt")
        if err != nil {
            fmt.Println("open file err :", err)
            return
        }
        defer file.Close()
        // 定义接收文件读取的字节数组
        var buf [128]byte
        var content []byte
        for {
            n, err := file.Read(buf[:])
            if err == io.EOF {
                // 读取结束
                break
            }
            if err != nil {
                fmt.Println("read file err ", err)
                return
            }
            content = append(content, buf[:n]...)
        }
        fmt.Println(string(content))
    }
    

IO操作-bufio包与ioutil包

bufio包实现了带缓冲区的读写,是对文件读写的封装

前面说到开发者更喜欢使用更上层的读写方法,golang的bufio包除了实现带缓冲区的读写提高效率和稳定性外,还提供按行读方法,ioutil包提供了读取整个文件、写文件方法

bufio、ioutil包更多文件、目录读写详见官方标准库

strconv包

  • Atoi

    string类型转换成int类型

  • Itoa

    int转换成string

  • ParaseType系列

    将string转换成指定Type类型

    b, err := strconv.ParseBool("true")
    f, err := strconv.ParseFloat("3.1415", 64)
    i, err := strconv.ParseInt("-2", 10, 64)
    u, err := strconv.ParseUint("2", 10, 64)
    
  • FormatType系列

    将给定类型数据格式化为string类型数据的功能

    s1 := strconv.FormatBool(true)
    s2 := strconv.FormatFloat(3.1415, 'E', -1, 64)
    s3 := strconv.FormatInt(-2, 16)
    s4 := strconv.FormatUint(2, 16)
    

strconv包中还有Append系列、Quote系列等函数。详细见官方标准库

模板-template包

html/template包实现了数据驱动的模板,用于生成可对抗代码注入的安全HTML输出。

  • 模板语法

    {{.}}
    

    模板语法都包含在{{和}}中间,其中{{.}}中的点表示当前对象。

    当我们传入一个结构体对象时,我们可以根据.来访问结构体的对应字段。例如:

    type UserInfo struct {
    Name   string
    Gender string
    Age    int
    }
    
    func sayHello(w http.ResponseWriter, r *http.Request) {
        // 解析指定文件生成模板对象
        tmpl, err := template.ParseFiles("./hello.html")
        if err != nil {
            fmt.Println("create template failed, err:", err)
            return
        }
        user := UserInfo{
            Name:   "枯藤",
            Gender: "男",
            Age:    18,
        }
        // 利用给定数据渲染模板,并将结果写入w
        tmpl.Execute(w, user)
    }
    

    .表示当前对象/结构体user(w只是服务端的一个变量,也保存了user这个结构体而已)

    <body>
    <p>Hello {{.Name}}</p>
    <p>性别:{{.Gender}}</p>
    <p>年龄:{{.Name}}</p>
    </body>
    
  • 模板注释

    {{/* a comment */}}
    注释,执行时会忽略。可以多行。注释不能嵌套,并且必须紧贴分界符始止。
    
  • 管道pipeline

    Go的模板语法中支持使用管道符号|链接多个命令,用法和unix下的管道类似:|前面的命令会将运算结果(或返回值)传递给后一个命令的最后一个位置。

  • Actions

    以下这些动作基本包含golang模板中常用的动作与含义说明

    {{/* a comment */}}
    注释执行时会忽略可以多行注释不能嵌套并且必须紧贴分界符始止就像这里表示的一样
    {{pipeline}}
        pipeline的值的默认文本表示会被拷贝到输出里
    {{if pipeline}} T1 {{end}}
        如果pipeline的值为empty不产生输出否则输出T1执行结果不改变dot的值
        Empty值包括false0任意nil指针或者nil接口任意长度为0的数组切片字典
    {{if pipeline}} T1 {{else}} T0 {{end}}
        如果pipeline的值为empty输出T0执行结果否则输出T1执行结果不改变dot的值
    {{if pipeline}} T1 {{else if pipeline}} T0 {{end}}
        用于简化if-else链条else action可以直接包含另一个if等价于
            {{if pipeline}} T1 {{else}}{{if pipeline}} T0 {{end}}{{end}}
    {{range pipeline}} T1 {{end}}
        pipeline的值必须是数组切片字典或者通道
        如果pipeline的值其长度为0不会有任何输出
        否则dot依次设为数组切片字典或者通道的每一个成员元素并执行T1
        如果pipeline的值为字典且键可排序的基本类型元素也会按键的顺序排序
    {{range pipeline}} T1 {{else}} T0 {{end}}
        pipeline的值必须是数组切片字典或者通道
        如果pipeline的值其长度为0不改变dot的值并执行T0否则会修改dot并执行T1
    {{template "name"}}
        执行名为name的模板提供给模板的参数为nil如模板不存在输出为""
    {{template "name" pipeline}}
        执行名为name的模板提供给模板的参数为pipeline的值
    {{with pipeline}} T1 {{end}}
        如果pipeline为empty不产生输出否则将dot设为pipeline的值并执行T1不修改外面的dot
    {{with pipeline}} T1 {{else}} T0 {{end}}
        如果pipeline为empty不改变dot并执行T0否则dot设为pipeline的值并执行T1
    
  • 比较

    eq      如果arg1 == arg2则返回真
    ne      如果arg1 != arg2则返回真
    lt      如果arg1 < arg2则返回真
    le      如果arg1 <= arg2则返回真
    gt      如果arg1 > arg2则返回真
    ge      如果arg1 >= arg2则返回真
    
     {{eq arg1 arg2 arg3}}
    
  • 嵌套模板

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
        <title>tmpl test</title>
    </head>
    <body>
    
        <h1>测试嵌套template语法</h1>
        <hr>
        {{template "ul.html"}}
        <hr>
        {{template "ol.html"}}
    </body>
    </html>
    
    {{ define "ol.html"}}
    <h1>这是ol.html</h1>
    <ol>
        <li>AA</li>
        <li>BB</li>
        <li>CC</li>
    </ol>
    {{end}}
    

    ul.html模板 不在当前html文档内,需要通过服务端template.ParseFiles指定加载的模板页,才能使用(有点耦合,要是都由前端模板文件去include包含模板可能会更自然些)

    func tmplDemo(w http.ResponseWriter, r *http.Request) {
    tmpl, err := template.ParseFiles("./t.html", "./ul.html")
    if err != nil {
        fmt.Println("create template failed, err:", err)
        return
    }
    user := UserInfo{
        Name:   "枯藤",
        Gender: "男",
        Age:    18,
    }
    tmpl.Execute(w, user)
    

} ```

http包

Go语言内置的net/http包十分的优秀,提供了HTTP客户端和服务端的实现

  • 客户端

    resp, err := http.Get("http://www.baidu.com/")
      
    resp, err := http.Post("http://www.9ong.com/post", "image/jpeg", &buf)
      
    resp, err := http.PostForm("http://www.9ong.com/form",
        url.Values{"key": {"Value"}, "id": {"123"}})
    
    if err != nil {
        // handle error
    }
    //使用完response后必须关闭回复的主体
    defer resp.Body.Close()
    body, err := ioutil.ReadAll(resp.Body)
    

    使用完response后必须关闭回复的主体,总是会有不完美的地方存在哈,还需要自己关闭Body,从编码的角度看。

  • 带参数的请求

    带参数get请求

    apiUrl := "http://127.0.0.1:9090/get"
    // URL param
    data := url.Values{}
    data.Set("name", "枯藤")
    data.Set("age", "18")
    u, err := url.ParseRequestURI(apiUrl)
    if err != nil {
        fmt.Printf("parse url requestUrl failed,err:%v\n", err)
    }
    u.RawQuery = data.Encode() // URL encode
    fmt.Println(u.String())
    resp, err := http.Get(u.String())
    if err != nil {
        fmt.Println("post failed, err:%v\n", err)
        return
    }
    

    带参数post请求

    // 表单数据
    //contentType := "application/x-www-form-urlencoded"
    //data := "name=jm&age=20"
    // json
    contentType := "application/json"
    data := `{"name":"jm","age":20}`
    resp, err := http.Post(url, contentType, strings.NewReader(data))
    

    看完以下的编写方式,我们觉得应该有更舒适第三方http库,只需要:

    http.Get(url,json对象参数|结构体参数)
    http.Post(url,options,data) //options负责设置http头等信息,data是参数json对象或结构体
    
  • 服务端

    // http server
    
    func sayHello(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintln(w, "Hello world")
    }
    
    func main() {
        http.HandleFunc("/", sayHello)
        err := http.ListenAndServe(":9527", nil)
        if err != nil {
            fmt.Printf("http server failed, err:%v\n", err)
            return
        }
    }
    

    自定义server

    s := &http.Server{
        Addr:           ":9527",
        Handler:        myHandler,
        ReadTimeout:    10 * time.Second,
        WriteTimeout:   10 * time.Second,
        MaxHeaderBytes: 1 << 20,
    }
    log.Fatal(s.ListenAndServe())
    

context

在 Go http包的Server中,每一个请求在都有一个对应的 goroutine 去处理。请求处理函数通常会启动额外的 goroutine 用来访问后端服务,比如数据库和RPC服务。用来处理一个请求的 goroutine 通常需要访问一些与请求特定的数据,比如终端用户的身份认证信息、验证相关的token、请求的截止时间。 当一个请求被取消或超时时,所有用来处理该请求的 goroutine 都应该迅速退出,然后系统才能释放这些 goroutine 占用的资源。

介绍goroutine时,我们看到范例并没有在main函数里使用context,goroutine也会自动退出。原因是只有一种情况正在运行的goroutine会因为其他goroutine的结束被终止,就是main函数的退出或程序停止执行.

sync.WaitGroup解决了协程协同同步完成问题,context主要为了解决协程协同取消问题

  • Context接口

    context.Context是一个接口,该接口定义了四个需要实现的方法

    type Context interface {
        Deadline() (deadline time.Time, ok bool) //返回当前Context被取消的时间,也就是完成工作的截止时间
        Done() <-chan struct{} //返回一个Channel,这个Channel会在当前工作完成或者上下文被取消之后关闭,多次调用Done方法会返回同一个Channel
        Err() error //返回当前Context结束的原因
        Value(key interface{}) interface{} //从Context中返回键对应的值,对于同一个上下文来说,多次调用Value 并传入相同的Key会返回相同的结果,该方法仅用于传递跨API和进程间跟请求域的数据
    }
    

注意:以下介绍的都是函数,并不是创建后context上下问对像的方法,而是context包的函数

  • context.Background函数

  • context.TODO函数

    Go内置两个函数:Background()和TODO(),这两个函数分别返回一个实现了Context接口的background和todo。我们代码中最开始都是以这两个内置的上下文对象作为最顶层的partent context,衍生出更多的子上下文对象。

    Background()主要用于main函数、初始化以及测试代码中,作为Context这个树结构的最顶层的Context,也就是根Context。

    TODO(),它目前还不知道具体的使用场景,如果我们不知道该使用什么Context的时候,可以使用这个。

    background和todo本质上都是emptyCtx结构体类型,是一个不可取消,没有设置截止时间,没有携带任何值的Context。

  • context.withCancel函数

    WithCancel返回带有新Done通道的父节点的副本。当调用返回的cancel函数或当关闭父上下文的Done通道时,将关闭返回上下文的Done通道,无论先发生什么情况。

    func gen(ctx context.Context) <-chan int {
        dst := make(chan int)
        n := 1
        go func() {
            for {
                select {
                case <-ctx.Done():
                    return // return结束该goroutine,防止泄露
                case dst <- n:
                    n++
                }
            }
        }()
        return dst
    }
    func main() {
        ctx, cancel := context.WithCancel(context.Background())
        defer cancel() // 当我们取完需要的整数后调用cancel
    
        for n := range gen(ctx) {
            fmt.Println(n)
            if n == 5 {
                break
            }
        }
    }
    
  • context.withDeadline函数

    返回父上下文的副本,并将deadline调整为不迟于d。如果父上下文的deadline已经早于d,则WithDeadline(parent, d)在语义上等同于父上下文。当截止日过期时,当调用返回的cancel函数时,或者当父上下文的Done通道关闭时,返回上下文的Done通道将被关闭,以最先发生的情况为准。

    func main() {
        d := time.Now().Add(50 * time.Millisecond)
        ctx, cancel := context.WithDeadline(context.Background(), d)
    
        // 尽管ctx会过期,但在任何情况下调用它的cancel函数都是很好的实践。
        // 如果不这样做,可能会使上下文及其父类存活的时间超过必要的时间。
        defer cancel()
    
        select {
        case <-time.After(1 * time.Second):
            fmt.Println("overslept")
        case <-ctx.Done():
            fmt.Println(ctx.Err())
        }
    }
    
  • context.WithTimeout函数

    func main() {
        // 设置一个50毫秒的超时
        ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*50)
        wg.Add(1)
        go worker(ctx)
        time.Sleep(time.Second * 5)
        cancel() // 通知子goroutine结束
        wg.Wait()
        fmt.Println("over")
    }
    
  • context.WithValue函数

    WithValue返回父节点的副本,其中与key关联的值为val。

    仅对API和进程间传递请求域的数据使用上下文值,而不是使用它来传递可选参数给函数。

    type TraceCode string
      
    func worker(){
        ...
        key := TraceCode("TRACE_CODE")
        traceCode, ok := ctx.Value(key).(string) // 在子goroutine中获取trace code        
        ...
    }
    func main(){
        ...
        // 在系统的入口中设置trace code传递给后续启动的goroutine实现日志数据聚合
        ctx = context.WithValue(ctx, TraceCode("TRACE_CODE"), "2009")
        wg.Add(1)
        go worker(ctx)
        ...
    }
    
  • 关于cancel()函数

    以上范例中的cancel函数,是通过context.With*系列函数返回得到的第二个值,用于通知同一个context下的所有goroutine结束/取消。

golang的context设计,让我们明白一个道理,能简单处理好一个问题,就是好的解决方案,没有高贵之分。

json/xml

  • json

    json可以和map、struct、interface相互转换

    // 将struct、map转换成json 字符串
    json.Marshal(struct|map)
    
    //将json字符串转换成Person结构体
    type Person struct{
        ...
    }
    jsonStr := []byte(`{"age":"18","name":"5lmh.com","marry":false}`)
    var p Person
    json.Unmarshal(jsonStr,&p)
    

    弱类型的js、php可以随时动态自由的转换json字符串,这个确实舒服太多,怪不得php开发者总说数组强大。

  • xml

    与json包的方法是一样,只是数据源不一样

    xml.Marshal(struct|map)
    xml.Unmarshal(xmlStr,&p)
    
  • msgpack

MSGPack是二进制的json,性能更快,更省空间

需要安装第三方包:go get -u github.com/vmihailenco/msgpack

```go
msgpack.Marshal(struct|map)
msgpack.Unmarshal(msgpackbinary,&p)
```

reflect反射

反射是指在程序运行期对程序本身进行访问和修改的能力

reflect包封装了反射相关的方法:

获取类型信息:reflect.TypeOf,是静态的

获取值信息:reflect.ValueOf,是动态的

反射可以获取interface类型信息、获取值信息、修改值信息

反射可以查看结构体字段、类型、方法,修改结构体的值,调用方法

  • 空接口结合反射

    可以通过 空接口 可以表示任何参数,利用反射判断参数类型

官方标准库

官方标准库

包和工具

包和工具

数据层

  • mysql

建议如果使用web框架,请使用框架的orm,如xrom

  • redis

插件库

插件库


参考: