wangbo123855842 / Learning

15 stars 2 forks source link

Go #34

Open wangbo123855842 opened 3 years ago

wangbo123855842 commented 3 years ago
スクリーンショット 2020-09-07 10 27 09

学习路线

WechatIMG512

前言

Go 是一门编译型,具有静态类型和类 C 语言语法的语言,并且有垃圾回收(GC)机制。 静态类型意味着变量必须是特定的类型。(如:int, string, bool, [] byte 等等),这可以通过在声明变量的时候,指定变量的类型来实现,或者让编译器自行推断变量的类型。

Go 被设计成代码在工作区内运行。 工作区是一个文件夹,这个文件夹由 bin ,pkg,以及 src 子文件夹组成的。

环境变量

运行一个go程序

package main
func main() {
  println("HelloWorld!")
}

执行命令

go run main.go

go run 命令已经包含了编译和运行。 它使用一个临时目录来构建程序,执行完然后清理掉临时目录。 在 go 中程序入口必须是 main 函数,并且在 main 包内

只是编译代码

go build main.go

变量

Go语言的基本类型

bool  // true false
string
int、int8、int16、int32、int64
uint、uint8、uint16、uint32、uint64、uintptr
byte // uint8 的别名
rune // int32 的别名 代表一个 Unicode 码
float32、float64
complex64、complex128

当一个变量被声明之后,系统自动赋予它该类型的零值。所有的内存在 Go 中都是经过初始化的

需要注意的是,简短模式有以下限制

由于使用了:=,而不是赋值的=,因此推导声明写法的左值变量必须是没有定义过的变量。若定义过,将会发生编译错误。

短变量声明的形式在开发中很常用,比如

conn, err := net.Dial("tcp","127.0.0.1:8080")

匿名变量

匿名变量的特点是一个下画线__本身就是一个特殊的标识符,被称为空白标识符。

Go语言变量的作用域

字符串

字符串是一种值类型,且值不可变,即创建某个文本后将无法再次修改这个文本的内容,更深入地讲,字符串是字节的定长数组。

字符串拼接 使用 + 符号。

  1. ASCII 字符串长度使用 len() 函数。
  2. Unicode 字符串长度使用 utf8.RuneCountInString() 函数
fmt.Println(utf8.RuneCountInString("汉字"))
fmt.Println(len("汉字"))
2  # Go 语言的字符串都以 UTF-8 格式保存,每个中文占用 3 个字节,因此使用 len() 获得两个中文文字对应的 6 个字节
6
  1. ASCII 字符串遍历直接使用下标
  2. Unicode 字符串遍历用 for range
msg := "HelloWorld"
for i := 0; i < len(msg) ; i++ {
    fmt.Println(string(msg[i]))
}
msg = "汉字"
for _, s := range msg {
    fmt.Println(string(s));
}

Go 语言的字符串是不可变的。修改字符串时,可以将字符串转换为 []byte 进行修改[]byte 和 string 可以通过强制类型转换互转

Go 语言的字符串无法直接修改每一个字符元素,只能通过重新构造新的字符串并赋值给原来的字符串变量实现。 例如

angel := "Heros never die"
angleBytes := []byte(angel)
for i := 5; i <= 10; i++ {
    angleBytes[i] = ' '
}
fmt.Println(string(angleBytes))

comma := strings.Index(tracer,",") pos := strings.Index(tracer[comma:], "g")

字符串索引比较常用的有如下几种方法 strings.Index:正向搜索子字符串 strings.LastIndex:反向搜索子字符串

Go 语言的标准库自带了 Base64 编码算法 使用 encoding/base64

message := "Away from keyboard. https://golang.org/"
encodedMessage := base64.StdEncoding.EncodeToString([]byte (message))
data, err := base64.StdEncoding.DecodeString(encodedMessage)
if err != nil {
    fmt.Println(err)
} else {
    fmt.Println(string(data))
}

Go语言的字符有以下两种

数据类型转换

类型 B 的值 = 类型 B(类型 A 的值)

指针

创建指针的另一种方法 new() 函数,比如

str := new(string)
*str = "Go语言教程"
fmt.Println(*str)

new() 函数可以创建一个对应类型的指针,创建过程会分配内存,被创建的指针指向默认值

常量和const关键字

const pi = 3.14159

常量的值必须是能够在编译时就能够确定的,可以在其赋值表达式中涉及计算过程,但是所有用于计算的值必须在编译期间就能获得。

正确的做法:const c1 = 2/3
错误的做法:const c2 = getNumber() // 引发构建错误: getNumber() 用做值

iota 常量生成器

在一个 const 声明语句中,在第一个声明的常量所在的行,iota 将会被置为 0,然后在每一个有常量声明的行加1。

const (
    Sunday = iota  //  0
    Monday        //  1
)

类型别名

type TypeAlias = Type

比如

// 将NewInt定义为int类型
type NewInt int
// 将int取一个别名叫IntAlias
type IntAlias = int

将 NewInt 定义为 int 类型,这是常见的定义类型的方法,通过 type 关键字的定义,NewInt 会形成一种新的类型,NewInt 本身依然具备 int 类型的特性。 将 IntAlias 设置为 int 的一个别名,使用 IntAlias 与 int 等效。

控制语句

if condition {
    // do something
}

if 还有一种特殊的写法,可以在 if 表达式之前添加一个执行语句,再根据变量值进行判断

if err := Connect(); err != nil {
    fmt.Println(err)
    return
}

Go语言中的循环语句只支持 for 关键字,而不支持 while 和 do-while 结构 如果要实现 while

sum := 0
for {
    sum++
    if sum > 100 {
        break
    }
}

另外,break,continue 可以结合标签

for j := 0; j < 5; j++ {
    for i := 0; i < 10; i++ {
        if i > 5 {
            break JLoop
        }
        fmt.Println(i)
    }
}
JLoop:

for range 结构是Go语言特有的一种的迭代结构,在许多情况下都非常有用,for range 可以遍历数组、切片、字符串、map 及通道(channel),for range 语法上类似于其它语言中的 foreach 语句 格式

for key, val := range coll {
}

另外,val 是一个值拷贝,修改这个值,不会影响原数组,原切片等。如果需要修改原数组的数据,可以使用 数组变量[key] = xx

Go语言改进了 switch 的语法设计,case 与 case 之间是独立的代码块,不需要通过 break 语句跳出当前 case 代码块以避免执行到下一行

如果想跨越 case 的话,手动使用 fallthrough

var s = "hello"
switch {
case s == "hello":
    fmt.Println("hello")
    fallthrough
case s != "world":
    fmt.Println("world")
}

Go语言中 goto 语句通过标签进行代码间的无条件跳转,同时 goto 语句在快速跳出循环、避免重复退出上也有一定的帮助

数组

数组是一个由固定长度的特定类型元素组成的序列,一个数组可以由零个或多个元素组成。 因为数组的长度是固定的,所以在Go语言中很少直接使用数组。 和数组对应的类型是 Slice(切片),Slice 是可以增长和收缩的动态序列,功能也更灵活。

var a [3]int             // 定义三个整数的数组
fmt.Println(a[0])        // 打印第一个元素
fmt.Println(a[len(a)-1]) // 打印最后一个元素
for i, v := range a {
    fmt.Printf("%d %d\n", i, v)
}

默认情况下,数组的每个元素都会被初始化为元素类型对应的零值

在数组的定义中,如果在数组长度的位置出现 ... 省略号,则表示数组的长度是根据初始化值的个数来计算

q := [...]int{1, 2, 3}

切片

切片 slice 是对数组的一个连续片段的引用,所以切片是一个引用类型。

切片默认指向一段连续内存区域,可以是数组,也可以是切片本身。

a := [3]int{1, 2, 3}
b := a[1:2]
c := a[1:]
d := a[:3]
var strList []string
var numList []int
// 声明一个空切片
var numListEmpty = []int{}

如果需要动态地创建一个切片,可以使用 make() 内建函数

make( []Type, size, cap )

其中 Type 是指切片的元素类型,size 指的是为这个类型初始化多少个元素cap 为预分配的元素数量cap的值设定后不影响 size,只是能提前分配空间,降低多次分配空间造成的性能问题。 比如

b := make([]int, 2, 10)
fmt.Println(b)
fmt.Println(len(b))
fmt.Println(cap(b))
[0 0]
2
10

Go语言的内建函数 append() 可以为切片动态添加元素。

var a []int
a = append(a, 1) // 追加1个元素
a = append(a, 1, 2, 3) // 追加多个元素, 手写解包方式

使用 append() 函数为切片动态添加元素时,如果空间不足以容纳足够多的元素,切片就会进行扩容,此时新切片的cap会发生改变

Go语言的内置函数 copy() 可以将一个数组切片复制到另一个数组切片中。 如果加入的两个数组切片不一样大,就会按照其中较小的那个数组切片的元素个数进行复制。

copy( destSlice, srcSlice) 

比如

slice1 := []int{1, 2, 3, 4, 5}
slice2 := []int{5, 4, 3}
copy(slice2, slice1) // 只会复制slice1的前3个元素到slice2中
copy(slice1, slice2) // 只会复制slice2的3个元素到slice1的前3个位置

Go语言中删除切片元素的本质是,以被删除元素为分界点,将前后两个部分的内存重新连接起来

seq := []string{"a", "b", "c", "d", "e"}
index := 2

// 将删除点前后的元素连接起来
seq = append(seq[:index], seq[index+1:]...)
fmt.Println(seq)

map

映射可以和切片一样,使用 make 方法来创建。

lookup := make(map[string]int)
lookup["goku"] = 9001
power, exists := lookup["vegeta"]
fmt.Println(power, exists)

我们使用 len 方法类获取映射的键的数量

total := len(lookup)

使用 delete 方法来删除一个键对应的值,另外,Go语言中并没有为 map 提供任何清空所有元素的方法

delete(lookup, "goku")

映射是动态变化的。然而我们可以通过传递第二个参数到 make 方法来设置一个初始大小

lookup := make(map[string]int, 100)

Go 还可以这样定义和初始化

lookup := map[string]int{
  "goku": 9001,
  "gohan": 2044,
}

函数 Func

Go语言支持多返回值。 Go语言经常使用多返回值中的最后一个返回参数返回函数执行中可能发生的错误,比如

conn, err := connectToNetwork()

声明

// 无返回值
func log(message string) {
}
// 一个返回值
func add(a b, int) int {
    return 1
}
// 两个返回值
func power(name string) (int, bool) {
    return 1, false
}

我们可以像这样使用最后一个

value, exists := power("goku")

在Go语言中,函数也是一种类型,可以和其他类型一样保存在变量中

func main()  {
    a:= f
    data, code := a(1)
    fmt.Println(data)
    fmt.Println(code)
}
func f(i int) (int,bool) {
    return i, false
}
a := func(i int) (int,bool) {
    return i, false
}
fmt.Println(a(100));
func myfunc(args ...int) {
    for _, arg := range args {
        fmt.Println(arg)
    }
}

...type 格式的类型只能作为函数的参数类型存在,并且必须是最后一个参数 从内部实现机理上来说,类型...type本质上是一个数组切片,也就是[]type

args切片是一个值拷贝,更改args切片不影响原切片,跟直接传递一个切片是不一样的

任意类型的可变参数 如果你希望传任意类型,可以指定类型为 interface{} 用 interface{} 传递任意类型数据是Go语言的惯例用法,使用 interface{} 仍然是类型安全的

func MyPrintf(args ...interface{}) {
    for _, arg := range args {
        switch arg.(type) {
            case int:
                fmt.Println(arg, "is an int value.")
            case string:
                fmt.Println(arg, "is a string value.")
            case int64:
                fmt.Println(arg, "is an int64 value.")
            default:
                fmt.Println(arg, "is an unknown type.")
        }
    }
}

Go语言自带了 testing 测试包,可以进行自动化的单元测试 要开始一个单元测试,需要准备一个 go 源码文件,在命名文件时文件名必须以 _test.go 结尾

demo.go

package demo
func GetArea(weight int, height int) int {
    return weight * height
}

demo_test.go

package demo
import "testing"
func TestGetArea(t *testing.T) {
    area := GetArea(40, 50)
    if area != 2000 {
        t.Error("测试失败")
    }
}

执行测试命令,运行结果

PS \code> go test -v
=== RUN   TestGetArea
--- PASS: TestGetArea (0.00s)
PASS
ok      _/D_/code       0.435s

还可以进行性能测试

package demo
import "testing"
func BenchmarkGetArea(t *testing.B) {
    for i := 0; i < t.N; i++ {
        GetArea(40, 50)
    }
}

运行

PS D:\code> go test -bench="."
goos: windows
goarch: amd64
BenchmarkGetArea-4      2000000000               0.35 ns/op
PASS
ok      _/D_/code       1.166s

覆盖率测试 执行测试命令

PS D:\code> go test -cover
PASS
coverage: 100.0% of statements
ok      _/D_/code       0.437s

另外,GO 语言的函数,不允许方法重载

异常

Go语言是不支持 try…catch…finally 这种异常处理的。 在Go语言中,使用多值返回来返回错误。不要用异常代替错误。 在极个别的情况下,才使用Go中引入的Exception处理:defer, panic, recover。

defer的特性是,在函数返回之前, 调用defer函数的操作, 简化函数的清理工作。 类似于 finally 函数内可以有多个defered函数,但是这些defered函数在函数返回时遵守后进先出的原则

fmt.Println(1111)
defer fmt.Println(2222)
defer fmt.Println(3333)
1111
3333
2222

panic用法挺简单的, 其实就是throw exception。 panic是golang的内建函数,panic会中断函数的正常执行流程, 从函数中跳出来, 跳回到函数的调用者。 对于调用者来说, 这个函数看起来就是一个panic,所以调用者会继续向上跳出, 直到当前goroutine返回defered函数会正常执行

func panicTest() {
    defer fmt.Println("defer panicTest")
    fmt.Println("before")
    panic("exception")
    fmt.Println("after")
}

recover 也是 golang 的一个内建函数, 其实就是try catchrecover如果想起作用的话, 必须在defered函数中使用

如果当前的 goroutine panic 了,那么 recover 将会捕获这个 panic 的值,并且让程序正常执行下去。不会让程序crash

func main()  {
    defer func() {
        // recover处理
        if err := recover(); err != nil {
            fmt.Println(err)
        }
    }()
    fmt.Println("before panicTest")
    panicTest()
    fmt.Println("after panicTest")

}
func panicTest() {
    panic("exception")
}

输出下面内容,goroutine 不会异常终止

before panicTest
defer main
exception

结构体 struct

Go 语言中没有 类 的概念,也不支持 类的继承等面向对象的概念。 Go 语言的结构体与类都是复合结构体。 Go 语言不仅认为结构体能拥有方法,且每种自定义类型也可以拥有自己的方法。

结构体成员也可以称为字段,这些字段有以下特性

  1. 字段拥有自己的类型和值
  2. 字段名必须唯一
  3. 字段的类型也可以是结构体
type Point struct {
    X int
    Y int
}

实例化

结构体本身是一种类型,可以像整型、字符串等类型一样,以 var 的方式声明结构体即可完成实例化

type Point struct {
    X int
    Y int
}
var p Point
p.X = 10
p.Y = 20

Go语言中,还可以使用 new 关键字对类型进行实例化,结构体在实例化后会形成指针类型的结构体

point := new(Point)
point.X = 1
point.Y = 2
point := &Point{}
point.X = 1
point.Y = 2

point是一个指针类型,但是操作结构体的变量,仍然正常使用 . 来操作。

point := Point{
        X: 1,
        Y: 1,
        name: "myName",
    }

或者 使用结构体指针

point := &Point{
        X: 1,
        Y: 1,
        name: "myName",
    }

Go语言的类型或结构体没有构造函数的功能,但是我们可以使用结构体初始化的过程来模拟实现构造函数。

type Cat struct {
    Color string
    Name  string
}
func NewCatByName(name string) *Cat {
    return &Cat{
        Name: name,
    }
}
func NewCatByColor(color string) *Cat {
    return &Cat{
        Color: color,
    }
}

继承

Go语言中的继承是通过内嵌或组合来实现的,所以可以说,在Go语言中,相比较于继承,组合更受青睐。

type A struct {
    ax, ay int
}
type B struct {
    A
    bx, by float32
}
func main()  {
    b := B { A{1,2}, 3.0, 4.0 }
    fmt.Println(b.A.ax)  //  两种方法都可以取到
    fmt.Println(b.ax)
}

方法

Go 方法是作用在接收器(receiver)上的一个函数接收器是某种类型的变量因此方法是一种特殊类型的函数

接收器类型可以几乎是任何类型,不仅仅是结构体类型,任何类型都可以有方法,可以是 int、bool、string 或数组的别名类型,但是接收器不能是一个接口类型。

在面向对象的语言中,类拥有的方法一般被理解为类可以做的事情。在Go语言中方法的概念与其他语言一致,只是Go语言建立的接收器强调方法的作用对象是接收器,也就是类实例,而函数没有作用对象

type Bag struct {
    items []int
}
func (b *Bag) Insert(itemid int) {
    b.items = append(b.items, itemid)
}
func main() {
    b := new(Bag)
    b.Insert(1001)
}

每个方法只能有一个接收器

スクリーンショット 2020-09-14 21 21 24

接收器的格式如下

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

接收器类型可以是 指针类型 或者是 非指针类型

区别在于,当方法作用于非指针接收器时,Go语言会在代码运行时将接收器的值复制一份,在非指针接收器的方法中可以获取接收器的成员值,但修改后无效

指针类型

type Bag struct {
    name string
}
func (b *Bag) display() {
    b.name = "new name"
}
func main() {
    b := Bag {name: "default name"}
    b.display()
    fmt.Println(b.name)
}

打印结果: new name

非指针类型

type Bag struct {
    name string
}
func (b Bag) display() {
    b.name = "new name"
}
func main() {
    b := Bag {name: "default name"}
    b.display()
    fmt.Println(b.name)
}

打印结果: default name

接口

Go语言不是一种传统的面向对象编程语言。它里面没有类和继承的概念。 Go语言中接口类型的独特之处在于它是满足隐式实现的。Go语言中没有类似于 implements 的关键字。也就是说,我们没有必要对于给定的具体类型定义所有满足的接口类型,简单地拥有一些必需的方法就足够了。简单地说,就是实现了接口的方法就可以,不需显式的指定接口

type Shape interface {
     Area() float64
     Perimeter() float64
}

Go语言的每个接口中的方法数量不会很多。Go语言希望通过一个接口精准描述它自己的功能,而通过多个接口的嵌入和组合的方式将简单的接口扩展为复杂的接口

接口与结构体结合,实现多态的效果

import (
    "fmt"
    "math"
)
type Shape interface {
     Area() float64
     Perimeter() float64
}
type Rect struct {
    width float64
    height float64
}
type Circle struct {
    radius float64
}
func (r Rect) Area() float64 {
    return r.width * r.height
}
func (r Rect) Perimeter() float64 {
    return 2 * (r.width + r.height)
}
func (c Circle)  Area() float64 {
    return math.Pi * c.radius * c.radius
}
func (c Circle)  Perimeter() float64 {
    return 2 * math.Pi * c.radius
}
func main() {
    var rshape Shape
    rshape = Rect{10,3}
    var cShape Shape
    cShape = Circle{10}
    fmt.Println(rshape.Area())
    fmt.Println(cShape.Perimeter())
}

当接口没有方法时,它被称为空接口。 这由interface{}表示。 由于空接口没有任何方法,因此所有类型都实现了该接口。 相当于最上级的元素,比如 Java 的 Object。

func explain(o interface{}) {
    fmt.Println("all the data implement interface" , o)
}
func main() {
    rshape := Rect{10,3}
    explain(rshape)
    explain(1111)
}

一个类型可以实现多个接口。

import "fmt"

type Shape interface {
     Area() float64
}
type Volume interface {
    Volume() float64
}
type Rect struct {
    width float64
    height float64
}
func (r Rect) Area() float64 {
    return 1
}
func (r Rect) Volume() float64 {
    return 2
}
func main() {
    var rect Rect
    rect = Rect {10,3}
    var s Shape
    s = rect    // 强制转换成接口Shape类型
    var v Volume
    v = rect    // 强制转换成接口Volumn类型
    fmt.Println(s.Area())
    fmt.Println(v.Volume())
}

使用 o.(type) 来判断接口类型

func explain(o interface{}) {
    switch o.(type) {
    case Volume:
        fmt.Println("this is a Volume")
    case Shape:
        fmt.Println("this is a shape")
    }
}

在go中,接口不能实现其他接口或扩展它们,但我们可以通过合并两个或多个接口来创建新接口

type Shape interface {
     Area() float64
}
type Volume interface {
    Volume() float64
}
type newI interface {
    Shape
    Volume
}

之前,接口的实现方法的接受者是结构体,不是指针类型, 如果换成指针类型,需要这样

func (r *Rect) Area() float64 {
    return r.width
}
func main() {
    rect :=  Rect {10,3}
    var s Shape = &rect // 接受者是指针类型
    fmt.Println(s.Area())
}

Go语言中引入 error 接口类型作为错误处理的标准模式,如果函数要返回错误,则返回值类型列表中肯定包含 error。 一般情况下,如果函数需要返回错误,就将 error 作为多个返回值中的最后一个创建一个 error 最简单的方法就是调用 errors.New 函数,它会根据传入的错误信息返回一个新的 error。

func Sqrt(f float64) (float64, error) {
    if f < 0 {
        return -1, errors.New("math: square root of negative number")
    }
    return math.Sqrt(f), nil
}
func main() {
    result, err := Sqrt(-13)
    if err != nil {
        fmt.Println(err)
    } else {
        fmt.Println(result)
    }
}

Go 语言的源码复用建立在包(package)基础之上。 Go 语言的入口 main() 函数所在的包(package)叫 main,main 包想要引用别的代码,必须同样以包的方式进行引用。

包的习惯用法 包名一般是小写的。 包名一般要和所在的目录同名。 包一般使用域名作为目录名称,这样能保证包名的唯一性。 包名为 main 的包为应用程序的入口包,编译不包含 main 包的源码文件时不会得到可执行文件。 一个文件夹下的所有源码文件只能属于同一个包,同样属于同一个包的源码文件不能放在多个文件夹下。

常用内置包

使用 Go Modules 管理依赖

go module 是Go语言从 1.11 版本之后官方推出的版本管理工具,并且从 Go1.13 版本开始,go module 成为了Go语言默认的依赖管理工具

使用 go module 管理依赖后会在项目根目录下生成两个文件 go.mod 和 go.sum。

go.mod 中会记录当前项目的所依赖

module github.com/gosoon/audit-webhook

go 1.12

require (
    github.com/elastic/go-elasticsearch v0.0.0
    github.com/gorilla/mux v1.7.2
    github.com/gosoon/glog v0.0.0-20180521124921-a5fbfb162a81
)

go.sum记录每个依赖库的版本和哈希值

github.com/elastic/go-elasticsearch v0.0.0 h1:Pd5fqOuBxKxv83b0+xOAJDAkziWYwFinWnBO0y+TZaA=
github.com/gorilla/mux v1.7.2 h1:zoNxOV7WjqXptQOVngLmcSQgXmgk4NMz1HibBchjl/I=

使用 go module 需要设定环境变量

export GO111MODULE=on
export GOPROXY=https://goproxy.io 

然后在项目路径下,执行 go mod init

localhost:GoLearning ouhaku$ go mod init GoLearning
go: creating new go.mod: module GoLearning

之后,在工程中引入第三方的 module

import (
    "github.com/gin-gonic/gin"
)

go run main.go运行代码会发现 go mod 会自动查找依赖自动下载

下载的第三方包,会保存在 GOPATH 的 pkg / mod 下

反射

空接口相当于一个容器,能接受任何东西。如果想获取存储变量的类型信息和值信息就要使用反射机制。 利用 GO语言里面的 Reflect 包来实现反射。

func reflect_typeof(a interface{}) {
    t := reflect.TypeOf(a)
    fmt.Printf("type of a is:%v\n", t)

    k := t.Kind()
    switch k {
    case reflect.Int64:
        fmt.Printf("a is int64\n")
    case reflect.String:
        fmt.Printf("a is string\n")
    }
}
func reflect_value(a interface{}) {
    v := reflect.ValueOf(a)
    k := v.Kind()
    switch k {
    case reflect.Int64:
        fmt.Printf("a is Int64, store value is:%d\n", v.Int())
    case reflect.String:
        fmt.Printf("a is String, store value is:%s\n", v.String())
    }
}
// 创建一个结构体变量
var s Student = Student{
    Name:  "BigOrange",
    Sex:   1,
    Age:   10,
    Score: 80.1,
}

v := reflect.ValueOf(s)
t := v.Type()
kind := t.Kind()

if kind == reflect.Struct {
    for i := 0; i < v.NumField(); i++ {
        field := v.Field(i)
        // 打印字段的名称、类型以及值
        fmt.Printf("name:%s type:%v value:%v\n",
            t.Field(i).Name, field.Type().Kind(), field.Interface())
    }
}

如果要赋值,就需要指针类型

s := Student{
    Name:  "BigOrange",
    Sex:   1,
    Age:   10,
    Score: 80.1,
}

fmt.Printf("Name:%v, Sex:%v,Age:%v,Score:%v \n", s.Name, s.Sex, s.Age, s.Score)
v := reflect.ValueOf(&s)  // 这里传的是地址!!!

v.Elem().Field(0).SetString("ChangeName")
v.Elem().FieldByName("Score").SetFloat(99.9)

fmt.Printf("Name:%v, Sex:%v,Age:%v,Score:%v \n", s.Name, s.Sex, s.Age, s.Score)

并发

Go语言的并发通过goroutine实现。goroutine类似于线程,属于用户态的线程。 goroutine是由Go语言的运行时调度完成,而线程是由操作系统调度完成。Go语言还提供channel在多个goroutine间进行通信。goroutine和channel是 Go 语言秉承的 CSP 并发模式的实现基础。

在java中我们要实现并发编程的时候,我们通常需要自己维护一个线程池,并且需要自己去包装一个又一个的任务和然后自己去调度线程执行任务并维护上下文切换。

goroutine 的概念类似于线程,但 goroutine 由Go 程序运行时的调度和管理Go 程序会智能地将 goroutine 中的任务合理地分配给每个 CPU。 Go语言之所以被称为现代化的编程语言,就是因为它在语言层面已经内置了调度和上下文切换的机制。 在Go语言编程中你不需要去自己写进程、线程、协程,你的技能包里只有一个技能–goroutinue,当你需要让某个任务并发执行的时候,你只需要起一个goroutinue就可以了。

比如,这样使用关键字 go ,就启动了一个goroutine。 使用 time.Sleep(time.Second) 的目的是,在程序启动时,Go程序就会为main()函数创建一个默认的goroutine当main()函数返回的时候该goroutine就结束了,所有在main()函数中启动的goroutine会一同结束

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

在上面代码中的使用time.Sleep肯定是不合适的,Go语言中可以使用sync.WaitGroup来实现并发任务的同步

sync.WaitGroup内部维护着一个计数器,计数器的值可以增加和减少。例如当我们启动了N 个并发任务时,就将计数器值增加N。每个任务完成时通过调用Done()方法将计数器减1。通过调用Wait()来等待并发任务执行完,当计数器值为0时,表示所有并发任务已经完成

var wg sync.WaitGroup
func hello(i int) {
    defer wg.Done()
    fmt.Println("Hello Goroutine!", i)
}
func main() {
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go hello(i)
    }
    wg.Wait()
}

需要注意sync.WaitGroup是一个结构体,传递的时候要传递指针

OS线程(操作系统线程)一般都有固定的栈内存(通常为2MB),一个goroutine的栈在其生命周期开始时只有很小的栈(典型情况下2KB),goroutine的栈不是固定的,他可以按需增大和缩小,goroutine的栈大小限制可以达到1GB,虽然极少会用到这个大。所以在Go语言中一次创建十万左右的goroutine也是可以的。

OS线程是由OS内核来调度的,goroutine则是由Go运行时自己的调度器调度的。 goroutine的调度不需要切换内核语境,所以调用一个goroutine比调度一个线程成本低很多

Go运行时的调度器使用GOMAXPROCS参数来确定需要使用多少个OS线程来同时执行Go代码默认值是机器上的CPU核心数。 例如在一个8核心的机器上,调度器会把Go代码同时调度到8个OS线程上。 Go语言中可以通过runtime.GOMAXPROCS()函数设置当前程序并发时占用的CPU逻辑核心数

func main() {
    runtime.GOMAXPROCS(2)
    go a()
    go b()
    time.Sleep(time.Second)
}

总结一下,Go语言中的操作系统线程和goroutine的关系

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

channel

go语言的并发模型是CSP,提倡通过通信共享内存而不是通过共享内存而实现通信。 如果说goroutine是Go程序并发的执行体,channel就是它们之间的连接。 channel是可以让一个goroutine发送特定值到另一个goroutine的通信机制。 Go 语言中的通道(channel)是一种特殊的类型。通道像一个传送带或者队列,总是遵循先入先出的规则,保证收发数据的顺序。

每一个通道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型

var ch1 chan int   // 声明一个传递整型的通道
var ch2 chan bool  // 声明一个传递布尔型的通道
var ch3 chan []int // 声明一个传递int切片的通道

声明的通道后需要使用make函数初始化之后才能使用。

make(chan 元素类型, [缓冲大小])
ch4 := make(chan int)
ch5 := make(chan int, 1)

channel操作

通道有发送(send)、接收(receive)和关闭(close)三种操作。发送和接收都使用<-符号。

发送

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

接受

x := <- ch // 从ch中接收值并赋值给变量x
<-ch       // 从ch中接收值,忽略结果

关闭

close(ch)

只有在通知接收方goroutine所有的数据都发送完毕的时候才需要关闭通道通道是可以被垃圾回收机制回收的,它和关闭文件是不一样的,在结束操作之后关闭文件是必须要做的,但关闭通道不是必须的。关闭后的通道有以下特点:

  1. 对一个关闭的通道再发送值就会导致panic。
  2. 对一个关闭的通道进行接收会一直获取值直到通道为空。
  3. 对一个关闭的并且没有值的通道执行接收操作会得到对应类型的零值。
  4. 关闭一个已经关闭的通道会导致panic。

通道,又分 无缓冲的通道 和 有缓冲的通道无缓冲的通道只有在有人接收值的时候才能发送值。使用无缓冲通道进行通信将导致发送和接收的goroutine同步化。因此,无缓冲通道也被称为同步通道

只要通道的容量大于零,那么该通道就是有缓冲的通道,通道的容量表示通道中能存放元素的数量

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

我们可以使用内置的len函数获取通道内元素的数量,使用cap函数获取通道的容量。

循环接受通道消息

func main() {
    ch1 := make(chan int)
    ch2 := make(chan int)

    // 开启 goroutine 将 0~100 的数发送到ch1中
    go func() {
        for i := 0; i < 100; i++ {
            ch1 <- i
        }
        close(ch1)
    }()

    // 开启goroutine从ch1中接收值,并将该值的平方发送到ch2中
    go func() {
        for {
            i, ok := <-ch1
            if !ok {
                break
            }
            ch2 <- i * i
        }
        close(ch2)
    }()

    // 在主 goroutine 中从ch2中接收值
    for i := range ch2 {
        fmt.Println(i)
    }
}

我们通常使用的是 for range 的方式

有的时候我们会将通道作为参数在多个任务函数间传递,很多时候我们在不同的任务函数中使用通道都会对其进行限制,比如只能发送或只能接收。Go语言中提供了单向通道来处理这种情况。

func counter(out chan<- int) {
    for i := 0; i < 100; i++ {
        out <- i
    }
    close(out)
}
func squarer(out chan<- int, in <-chan int) {
    for i := range in {
        out <- i * i
    }
    close(out)
}

chan<- int是一个只能发送的通道,可以发送但是不能接收;<-chan int是一个只能接收的通道,可以接收但是不能发送。 在函数传参及任何赋值操作中将双向通道转换为单向通道是可以的,但反过来是不可以的

在某些场景下我们需要同时从多个通道接收数据。通道在接收数据时,如果没有数据可以接收将会发生阻塞。 Go内置了select关键字,可以同时响应多个通道的操作

func main() {
    ch := make(chan int, 1)
    for i := 0; i < 10; i++ {
        select {
        case x := <-ch:
            fmt.Println(x)
        case ch <- i:
        }
    }
}

有时候在Go代码中可能会存在多个goroutine同时操作一个资源,这种情况会发生竞态问题。

互斥锁

互斥锁是一种常用的控制共享资源访问的方法,它能够保证同时只有一个goroutine可以访问共享资源。、 Go语言中使用sync包的Mutex类型来实现互斥锁

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)
}

使用互斥锁能够保证同一时间有且只有一个goroutine进入临界区,其他的goroutine则在等待锁。 当互斥锁释放后,等待的goroutine才可以获取锁进入临界区。

读写互斥锁

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

读写锁在Go语言中使用sync包中的RWMutex类型。读写锁分为两种:读锁和写锁。 当一个goroutine获取读锁之后,其他的goroutine如果是获取读锁会继续获得锁,如果是获取写锁就会等待。 当一个goroutine获取写锁之后,其他的goroutine无论是获取读锁还是写锁都会等待。

rwlock.Lock() // 加写锁
time.Sleep(10 * time.Millisecond) // 假设读操作耗时10毫秒
rwlock.Unlock()                   // 解写锁

rwlock.RLock()               // 加读锁
time.Sleep(time.Millisecond) // 假设读操作耗时1毫秒
rwlock.RUnlock()             // 解读锁
Go语言中内置的map不是并发安全的。在并发的环境下,使用 `sync.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()
}

标准类库

io 基本的 IO 接口

Go 语言中,为了方便开发者使用,将 IO 操作封装在了如下几个包中

  1. io 为 IO 原语(I/O primitives)提供基本的接口
  2. io/ioutil 封装一些实用的 I/O 函数
  3. fmt 实现格式化 I/O,类似 C 语言中的 printf 和 scanf
  4. bufio 实现带缓冲I/O
type Reader interface {
    Read(p []byte) (n int, err error)
}

所有实现了 Read 方法的类型都满足 io.Reader 接口。 也就是说,在所有需要 io.Reader 的地方,可以传递实现了 Read() 方法的类型的实例

比如

func ReadFrom(reader io.Reader, num int) ([]byte, error) {
    p := make([]byte, num)
    n, err := reader.Read(p)
    if n > 0 {
        return p[:n], nil
    }
    return p, err
}

ReadFrom 函数将 io.Reader 作为参数,也就是说,ReadFrom 可以从任意的地方读取数据,只要来源实现了 io.Reader 接口。比如,我们可以从标准输入、文件、字符串等读取数据

// 从标准输入读取
data, err = ReadFrom(os.Stdin, 11)
// 从普通文件读取,其中 file 是 os.File 的实例
data, err = ReadFrom(file, 9)
// 从字符串读取
data, err = ReadFrom(strings.NewReader("from string"), 12)

同样的,所有实现了Write方法的类型都实现了 io.Writer 接口

在fmt标准库中,有一组函数:Fprint/Fprintf/Fprintln,它们接收一个 io.Wrtier 类型参数,也就是说它们将数据格式化输出到 io.Writer 中。

fmt.Fprintln(os.Stdout, "HelloWorld")

有哪些实现了 Read / Writer 接口呢

os.File 同时实现了 io.Reader 和 io.Writer
strings.Reader 实现了 io.Reader
bufio.Reader/Writer 分别实现了 io.Reader 和 io.Writer
bytes.Buffer 同时实现了 io.Reader 和 io.Writer
bytes.Reader 实现了 io.Reader
compress/gzip.Reader/Writer 分别实现了 io.Reader 和 io.Writer
crypto/cipher.StreamReader/StreamWriter 分别实现了 io.Reader 和 io.Writer
crypto/tls.Conn 同时实现了 io.Reader 和 io.Writer
encoding/csv.Reader/Writer 分别实现了 io.Reader 和 io.Writer
mime/multipart.Part 实现了 io.Reader
net/conn 分别实现了 io.Reader 和 io.Writer(Conn接口定义了Read/Write)

常用的类型有,os.File、strings.Reader、bufio.Reader/Writer、bytes.Buffer、bytes.Reader

ReaderAt 接口使得可以从指定偏移量处开始读取数据通过WriterAt 接口将数据写入到数据流的特定偏移量之后

reader := strings.NewReader("HelloWorld");
silce := make([]byte, 2);
n , err := reader.ReadAt(silce, 2)
if(err != nil) {
    panic("reader error")
}
fmt.Printf("%s, %d",silce, n)

一次性从某个地方读或写到某个地方去。可以使用这两个接口

比如,实现将文件中的数据全部读取,显示在标准输出

file, err := os.Open("writeAt.txt")
if err != nil {
    panic(err)
}
defer file.Close()
writer := bufio.NewWriter(os.Stdout)
writer.ReadFrom(file)
writer.Flush()

我们也可以通过 ioutil 包的 ReadFile 函数获取文件全部内容

该接口比较简单,只有一个 Close() 方法,用于关闭数据流。 文件 (os.File)、归档(压缩包)、数据库连接、Socket 等需要手动关闭的资源都实现了 Closer 接口。 实际编程中,经常将 Close 方法的调用放在 defer 语句中

file, err := os.Open("writeAt.txt")
if err != nil {
    panic(err)
}
defer file.Close()

注意,应该将 defer file.Close() 放在错误检查之后

ioutil 方便的IO操作函数集

一次性读取 io.Reader 中的数据

reader := strings.NewReader("HelloWorld")
b,_ := ioutil.ReadAll(reader)
fmt.Println(string(b))

它读取目录并返回排好序的文件和子目录名( []os.FileInfo )

fileInfos, err := ioutil.ReadDir(path)
for _, info := range fileInfos {
    if info.IsDir(){
        fmt.Println(info.Name(),"\\")
    }else{
        fmt.Println(info.Name(),"\\")
    }
}

ReadFile 的实现和ReadAll 类似,不过,ReadFile 会先判断文件的大小,给 bytes.Buffer 一个预定义容量,避免额外分配内存。

通过 TempDir 创建一个临时目录。 TempFile 用于创建临时文件。

b.work, err = ioutil.TempDir("", "go-build")
f1, err := ioutil.TempFile("", "gofmt")

注意:创建者创建的临时文件和临时目录要负责删除这些临时目录和文件。

defer func() {
        f.Close()
        os.Remove(f.Name())
}()

bufio 缓存IO

bufio.Reader 结构包装了一个 io.Reader 对象,提供缓存功能,同时实现了 io.Reader 接口。

read:=bufio.NewReader(file)
for  {
  c, pk, err:=read.ReadLine()
  if err==io.EOF {
     fmt.Printf("到头了\n")
     break
  }
  if err!=nil && err!=io.EOF {
     fmt.Printf("读取错误")
     break
  }
}

对于简单的读取一行,Go1.1增加了一个类型:Scanner。

const input = "This is The Golang Standard Library.\nWelcome you!"
scanner := bufio.NewScanner(strings.NewReader(input))
for scanner.Scan() {
    fmt.Println(scanner.Text()) // Println will add back the final '\n'
}
if err := scanner.Err(); err != nil {
    fmt.Fprintln(os.Stderr, "reading standard input:", err)
}

strings — 字符串操作

常用的操作

a := "hello"
b := "hello world"
fmt.Println(strings.Compare(a, b))

fmt.Println(strings.Contains(b, a))

fmt.Println(strings.ContainsAny(b, a))

fmt.Println(strings.Count(a, "l"))  // 子串出现次数

fmt.Printf("Fields are: %q\n", strings.Fields("  foo bar  baz   "))  // 字符串分割

fmt.Printf("%q\n", strings.Split("foo,bar,baz", ","))

fmt.Println(strings.HasPrefix("Gopher", "Go"))

fmt.Println(strings.Index("Gopher", "p"))

c := []string{"aa","bb", "cc"}
d := strings.Join(c,",")
fmt.Println(d)

bytes byte slice 便利操作

该包定义了一些操作 byte slice 的便利操作。 因为字符串可以表示为 []byte,因此,bytes 包定义的函数、方法等和 strings 包很类似。 比如

a := "Hello"
b := "Hello World"
bytes.ContainsAny([]byte(b), a)

strconv 字符串和基本数据类型之间转换

a := "123"
data , err := strconv.Atoi(a)

Atoi 是 ParseInt 的便捷版,内部通过调用 ParseInt(s, 10, 0) 来实现的。 ParseInt 转为有符号整型。 ParseUint 转为无符号整型。

我们经常会遇到需要将字符串和整型连接起来,在 Java 中,可以通过操作符 "+" 做到。不过,在 Go 语言中,你需要将整型转为字符串类型,然后才能进行连接

a := 123
data := strconv.Itoa(a)

Itoa 内部直接调用 FormatInt(i, 10) 实现的

a := true
str := strconv.FormatBool(a)
bool , err := strconv.ParseBool(str)

unicode

go 对 unicode 的支持包含三个包

  1. unicode
  2. unicode/utf8
  3. unicode/utf16

utf16 包负责 rune 和 uint16 数组之间的转换

unicode 包包含基本的字符判断函数

func IsControl(r rune) bool  // 是否控制字符
func IsDigit(r rune) bool  // 是否阿拉伯数字字符,即 0-9
func IsGraphic(r rune) bool // 是否图形字符
func IsLetter(r rune) bool // 是否字母
func IsLower(r rune) bool // 是否小写字符
func IsMark(r rune) bool // 是否符号字符
func IsNumber(r rune) bool // 是否数字字符,比如罗马数字Ⅷ也是数字字符
func IsOneOf(ranges []*RangeTable, r rune) bool // 是否是 RangeTable 中的一个
func IsPrint(r rune) bool // 是否可打印字符
func IsPunct(r rune) bool // 是否标点符号
func IsSpace(r rune) bool // 是否空格
func IsSymbol(r rune) bool // 是否符号字符
func IsTitle(r rune) bool // 是否 title case
func IsUpper(r rune) bool // 是否大写字符
func Is(rangeTab *RangeTable, r rune) bool // r 是否为 rangeTab 类型的字符
func In(r rune, ranges ...*RangeTable) bool  // r 是否为 ranges 中任意一个类型的字符

utf8 包主要负责 rune 和 byte 之间的转换

判断是否符合 utf8 编码的函数

func Valid(p []byte) bool
func ValidRune(r rune) bool
func ValidString(s string) bool

判断字节串或者字符串的 rune 数

func RuneCount(p []byte) int
func RuneCountInString(s string) (n int)

比如

s := "你好,中国"
fmt.Println(utf8.RuneCountInString(s))
b := []byte(s)
fmt.Println(utf8.RuneCount(b))
5
5

Sort

数据集合,包括自定义数据类型的集合 排序需要实现 sort.Interface 接口的三个方法

type Interface interface {
        // 获取数据集合元素个数
        Len() int
        // 如果 i 索引的数据小于 j 索引的数据,返回 true,且不会调用下面的 Swap(),即数据升序排序。
        Less(i, j int) bool
        // 交换 i 和 j 索引的两个元素的位置
        Swap(i, j int)
}

数据集合实现了这三个方法后,即可调用该包的 Sort() 方法进行排序。

// 学生成绩结构体
type StuScore struct {
    name  string    // 姓名
    score int   // 成绩
}
type StuScores []StuScore
//Len()
func (s StuScores) Len() int {
    return len(s)
}
// Less(): 成绩将有低到高排序
func (s StuScores) Less(i, j int) bool {
    return s[i].score < s[j].score
}
// Swap()
func (s StuScores) Swap(i, j int) {
    s[i], s[j] = s[j], s[i]
}
function main {
    stus := StuScores{
                {"alan", 95},
                {"hikerell", 91},
                }

    // StuScores 已经实现了 sort.Interface 接口 , 所以可以调用 Sort 函数进行排序
    sort.Sort(stus)
}

sort包已经支持的内部数据类型排序

sort包原生支持[]int、[]float64 和[]string 三种内建数据类型切片的排序操作,即不必我们自己实现相关的 Len()、Less() 和 Swap() 方法。

[]int int 切片

s := []int{5, 2, 6, 3, 1, 4} // 未排序的切片数据
sort.Ints(s)
fmt.Println(s)

如果要使用降序排序

s := []int{5, 2, 6, 3, 1, 4} // 未排序的切片数据
sort.Sort(sort.Reverse(sort.IntSlice(s)))
fmt.Println(s) // 将会输出[6 5 4 3 2 1]

container包

该包实现了三个复杂的数据结构:堆,链表,环

常用的list

l := list.New()
// 尾部添加
l.PushBack("canon")
// 头部添加
l.PushFront(67)
// 尾部添加后保存元素句柄
element := l.PushBack("first")
fmt.Println(element.Value)

// 在first之后添加high
l.InsertAfter("high", element)
// 在first之前添加noon
l.InsertBefore("noon", element)
// 使用
l.Remove(element)

// 遍历
for i := l.Front(); i != nil; i = i.Next() {
     fmt.Println(i.Value)
}

日期与时间

Go 语言通过标准库 time 包处理日期和时间相关的问题。

时间可分为时间点与时间段,提供了以下两种基础类型

除此之外 Go 也提供了以下类型,做一些特定的业务

初始化

_time := time.Date(2018, 1, 2, 15, 30, 10, 0, time.Local)
time.Now()

// func Parse(layout, value string) (Time, error)
time.Parse("2006-01-02 15:04:05", "2020-09-20 16:48:23")

time.Parse , 其中layout的时间必须是"2006-01-02 15:04:05"这个时间,不管格式如何,时间点一定得是这个。

格式化

fmt.Println(time.Now().Format("2006-01-02 15:04:05")) // 2018-04-24 10:11:20
fmt.Println(time.Now().Format(time.UnixDate))

时间戳

fmt.Println(time.Now().Unix())

// 获取指定日期的时间戳
dt, _ := time.Parse("2006-01-02 15:04:05", "2020-09-20 16:48:23")
fmt.Println(dt.Unix())

fmt.Println(time.Date(2018, 1,2,15,30,10,0, time.Local).Unix())

其他

now := time.Now()
var month time.Month = now.Month();
fmt.Println(now.Date())
fmt.Println(now.Day())
fmt.Println(now.Year())

fmt.Println(month)  // 输出 September
fmt.Println(int(month))  // 输出 9

Duartion

dt1 := time.Date(2018, 1, 10, 0, 0, 1, 100, time.Local)
dt2 := time.Date(2018, 1, 9, 23, 59, 22, 100, time.Local)

// dt1.Sub(dt2) 返回一个 Duration 类型
du := dt1.Sub(dt2)
fmt.Println(du.Seconds())

比较两个时间点

dt := time.Date(2018, 1, 10, 0, 0, 1, 100, time.Local)
fmt.Println(time.Now().After(dt))   // true
fmt.Println(time.Now().Before(dt))  // false

// 是否相等 判断两个时间点是否相等时推荐使用 Equal 函数
fmt.Println(dt.Equal(time.Now()))

Ticker类型

有时我们会遇到每隔一段时间执行的业务(比如设置心跳时间等),就可以用它来处理,这是一个重复的过程。

tick := time.Tick(1 * time.Second)
// 可通过调用ticker.Stop取消
ticker := time.NewTicker(1 * time.Minute)
for _ = range tick {
   // do something
}

OS 包

os 包规定为所有操作系统实现的接口都是一致的。 有一些某个系统特定的功能,需要使用 syscall 获取。 实际上,os 依赖于 syscall。在实际编程中,我们应该总是优先使用 os 中提供的功能,而不是 syscall

打开一个文件,一般通过 Open 或 Create。Open 是只读。

file, err := os.Open("/tmp/studygolang.txt")
if err != nil {
    // 错误处理,一般会阻止程序往下执行
    return
}
defer file.Close()

flag 包

flag 包实现了命令行参数的解析。

log 包

import (
    "log"
    "os"
)
func main(){
    fileName := "Info_First.log"
    logFile,err  := os.Create(fileName)
    defer logFile.Close()
    if err != nil {
        log.Fatalln("open file error")
    }
    debugLog := log.New(logFile,"[Info]",log.Llongfile)
    debugLog.Println("A Info message here")
    debugLog.SetPrefix("[Debug]")
    debugLog.Println("A Debug Message here ")
}

context 包

控制并发有两种经典的方式,一种是 WaitGroup ,另外一种就是 Context

func main() {
    var wg sync.WaitGroup

    wg.Add(2)
    go func() {
        time.Sleep(2*time.Second)
        fmt.Println("1号完成")
        wg.Done()
    }()
    go func() {
        time.Sleep(2*time.Second)
        fmt.Println("2号完成")
        wg.Done()
    }()
    wg.Wait()
    fmt.Println("好了,大家都干完了,放工")
}

比如一个网络请求Request,每个Request都需要开启一个goroutine做一些事情,这些goroutine又可能会开启其他的goroutine。 所以我们需要一种可以跟踪goroutine的方案,才可以达到控制他们的目的,这就是Go语言为我们提供的Context,称之为上下文非常贴切,它就是goroutine的上下文

过去,我们使用 chan + select 来通知 goroutine 处理结束。比如

func main() {
    stop := make(chan bool)

    go func() {
        for {
            select {
            case <-stop:
                fmt.Println("监控退出,停止了...")
                return
            default:
                fmt.Println("goroutine监控中...")
                time.Sleep(2 * time.Second)
            }
        }
    }()

    time.Sleep(10 * time.Second)
    fmt.Println("可以了,通知监控停止")
    stop<- true
    //为了检测监控过是否停止,如果没有监控输出,就表示停止了
    time.Sleep(5 * time.Second)
}

使用Go Context重写。

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    go func(ctx context.Context) {
        for {
            select {
            case <-ctx.Done():
                fmt.Println("监控退出,停止了...")
                return
            default:
                fmt.Println("goroutine监控中...")
                time.Sleep(2 * time.Second)
            }
        }
    }(ctx)

    time.Sleep(10 * time.Second)
    fmt.Println("可以了,通知监控停止")
    cancel()
    //为了检测监控过是否停止,如果没有监控输出,就表示停止了
    time.Sleep(5 * time.Second)
}

把原来的chan stop 换成Context,使用Context跟踪 goroutine,以便进行控制,比如结束等。

流程解析

context.Background() 返回一个空的Context,这个空的Context一般用于整个Context树的根节点。 然后我们使用 context.WithCancel(parent) 函数,创建一个可取消的子Context,然后当作参数传给goroutine使用,这样就可以使用这个子Context跟踪这个goroutinectx.Done() 判断是否要结束,如果接受到值的话,就可以返回结束goroutine了。 context.WithCancel(parent)函数生成子Context的时候返回的,第二个返回值就是这个取消函数,它是CancelFunc类型的。我们调用它就可以发出取消指令,然后我们的监控goroutine就会收到信号,就会返回结束。

Context控制多个goroutine

控制多个goroutine的例子,其实也比较简单。

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    go watch(ctx,"【监控1】")
    go watch(ctx,"【监控2】")
    go watch(ctx,"【监控3】")

    time.Sleep(10 * time.Second)
    fmt.Println("可以了,通知监控停止")
    cancel()
    //为了检测监控过是否停止,如果没有监控输出,就表示停止了
    time.Sleep(5 * time.Second)
}

func watch(ctx context.Context, name string) {
    for {
        select {
        case <-ctx.Done():
            fmt.Println(name,"监控退出,停止了...")
            return
        default:
            fmt.Println(name,"goroutine监控中...")
            time.Sleep(2 * time.Second)
        }
    }
}

启动了3个监控goroutine进行不断的监控,每一个都使用了Context进行跟踪,当我们使用cancel函数通知取消时,这3个goroutine都会被结束。这就是Context的控制能力,它就像一个控制器一样,按下开关后,所有基于这个Context或者衍生的子Context都会收到通知,这时就可以进行清理操作了,最终释放goroutine,这就优雅的解决了goroutine启动后不可控的问题。

Go内置已经帮我们实现了2个Context接口。

context.Background(),一般作为Context这个树结构的最顶层的Context,也就是根Context。 context.TODO(),它目前还不知道具体的使用场景,如果我们不知道该使用什么Context的时候,可以使用这个。

有了如上的根Context,那么是如何衍生更多的子Context的呢?这就要靠context包为我们提供的With系列的函数了

func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
func WithValue(parent Context, key, val interface{}) Context

通过这些函数,就创建了一颗Context树

  1. WithCancel函数,传递一个父Context作为参数,返回子Context,以及一个取消函数用来取消Context
  2. WithDeadline函数,和WithCancel差不多,它会多传递一个截止时间参数,意味着到了这个时间点,会自动取消Context,当然我们也可以不等到这个时候,可以提前通过取消函数进行取消。
  3. WithTimeout和WithDeadline基本上一样,这个表示是超时自动取消,是多少时间后自动取消Context的意思。
  4. WithValue函数和取消Context无关,它是为了生成一个绑定了一个键值对数据的Context,这个绑定的数据可以通过Context.Value方法访问到

前三个函数都返回一个取消函数CancelFunc,这就是取消函数的类型,该函数可以取消一个Context,以及这个节点Context下所有的所有的Context,不管有多少层级

通过Context我们也可以传递一些必须的元数据,这些数据会附加在Context上以供使用。

var key string="name"

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    //附加值
    valueCtx:=context.WithValue(ctx,key,"【监控1】")
    go watch(valueCtx)
    time.Sleep(10 * time.Second)
    fmt.Println("可以了,通知监控停止")
    cancel()
    //为了检测监控过是否停止,如果没有监控输出,就表示停止了
    time.Sleep(5 * time.Second)
}

func watch(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            //取出值
            fmt.Println(ctx.Value(key),"监控退出,停止了...")
            return
        default:
            //取出值
            fmt.Println(ctx.Value(key),"goroutine监控中...")
            time.Sleep(2 * time.Second)
        }
    }
}

我们可以使用context.WithValue方法附加一对K-V的键值对。 这样我们就生成了一个新的Context,这个新的Context带有这个键值对,在使用的时候,可以通过Value方法读取ctx.Value(key)。 记住,使用WithValue传值,一般是必须的值,不要什么值都传递

Context 使用原则

  1. 不要把Context放在结构体中,要以参数的方式传递
  2. 以Context作为参数的函数方法,应该把Context作为第一个参数,放在第一位。
  3. 给一个函数方法传递Context的时候,不要传递nil,如果不知道传递什么,就使用context.TODO
  4. Context的Value相关方法应该传递必须的数据,不要什么数据都使用这个传递
  5. Context是线程安全的,可以放心的在多个goroutine中传递

Go Web

スクリーンショット 2020-09-16 12 40 17

Go 在编写 web 应用方面非常得力。

net/http

Go语言里面提供了一个完善的 net/http 包,通过http包可以很方便的搭建起来一个可以运行的Web服务。 同时使用这个包能很简单地对Web的路由,静态文件,模版,cookie等数据进行设置和操作。

Go不需要 nginx、apache服务器,因为他直接就监听tcp端口了,做了nginx做的事情。

func sayHelloWorld(reponse http.ResponseWriter, request *http.Request) {
    fmt.Println(request.URL.Path)
    fmt.Println(request.URL.Scheme)
    reponse.Write([]byte("helloworld"))
}

func main() {
    http.HandleFunc("/" , sayHelloWorld)
    err := http.ListenAndServe(":8080", nil)
    if err != nil {
        fmt.Println("Http Server Can't Started")
    }
}

Go 通过简单的几行代码就已经运行起来一个Web服务了,而且这个Web服务内部有支持高并发的特性

Go 实现Web服务的工作模式的流程图

スクリーンショット 2020-09-17 7 45 43

Go 底层其实这样处理的,初始化一个server对象,然后调用了 net.Listen("tcp", addr)也就是底层用TCP协议搭建了一个服务,然后监控我们设置的端口

首先通过Listener接收请求,其次创建一个Conn,最后单独开了一个 goroutine,把这个请求的数据当做参数传递给这个 Conn 去服务,用户的每一次请求都是在一个新的 goroutine 去服务,相互不影响。然后根据路由设置,传递给不同的Handler。

详细的整个流程

スクリーンショット 2020-09-17 8 40 20

Go 的 http 包详解

Go的http有两个核心功能:Conn,ServeMux

与我们一般编写的http服务器不同, Go为了实现高并发和高性能, 使用了goroutines来处理Conn的读写事件, 这样每个请求都能保持独立,相互不会阻塞,可以高效的响应网络事件。这是Go高效的保证

Go在等待客户端请求里面是这样写的

c, err := srv.newConn(rw)
if err != nil {
    continue
}
go c.serve()

这里我们可以看到 客户端的每次请求都会创建一个Conn这个Conn里面保存了该次请求的信息,然后再传递到对应的handler,该handler中便可以读取到相应的header信息,这样保证了每个请求的独立性。

路由器

HttpRouter 路由

Restful 结构

スクリーンショット 2020-09-17 9 26 44

httprouter 是一个高性能、可扩展的HTTP路由。 httprouter.New() 生成了一个 *Router 路由指针,然后使用GET方法注册一个适配/路径的Index函数。 最后*Router作为参数传给ListenAndServe函数启动HTTP服务即可。

使用 HttpRouter 的例子,这里使用了第三方库 github.com/julienschmidt/httprouter

import (
    "fmt"
    "net/http"
    "github.com/julienschmidt/httprouter"
)

func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
    fmt.Fprint(w, "Welcome!\n")
}

func Hello(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
    fmt.Fprintf(w, "hello, %s!\n", ps.ByName("name"))
}

func main() {
    router := httprouter.New()
    router.GET("/", Index)
    router.GET("/hello/:name", Hello)

    http.ListenAndServe(":8080", router)
}

httprouter.Router类型类似于http包中的ServeMux,它实现了http.Handler接口,所以它是一个http.Handler。它可以将请求分配给注册好的handler。 httprouter 中的 Handle 类似于 http.HandlerFunc ,只不过它支持第三个参数 Params,httprouter.Params。 Param类型是 key/value 型的结构,比如

router.GET("/hello/:name", Hello)

这里的:name就是key,当请求的URL路径为/hello/abc,则key对应的value为abc。也就是说保存了一个Param实例

Param{
    Key: "name",
    Value: "abc",
}

Params是Param的slice。也就是说,每个分组捕获到的key/value都存放在这个slice中。 ByName(str)方法可以根据Param的Key检索已经保存在slice中的Param的Value。

由于Params是slice结构,除了ByName()方法可以检索key/value,通过slice的方法也可以直接检索

ps[0].Key
ps[0].Value

CHI 框架

chi 是一个非常轻量级的框架,跟 net/http 兼容。

{
  r := chi.NewRouter()

  // "articles"下的路由
  r.Route("/articles", func(r chi.Router) {

    r.Post("/", createArticle)                                        // POST /articles
    r.Get("/search", searchArticles)                           // GET /articles/search

    // 正则表达式
    r.Get("/{articleSlug:[a-z-]+}", getArticleBySlug)                // GET /articles/home-is-toronto

    // 子路由
    r.Route("/{articleID}", func(r chi.Router) {
      r.Get("/", getArticle)                                          // GET /articles/123
      r.Put("/", updateArticle)                                       // PUT /articles/123
      r.Delete("/", deleteArticle)                                    // DELETE /articles/123
    })
  })

  http.ListenAndServe(":3000", r)
}

chi 的 Handler 跟 net/http 的 Handler 是兼容的。参数也是一样的,http.ResponseWriter*http.Request

func getArticle(w http.ResponseWriter, r *http.Request) {
  articleID := chi.URLParam(r, "articleID")
  // 业务逻辑
  article, err := dbGetArticle(articleID)
  if err != nil {
    http.Error(w, http.StatusText(404), 404)
    return
  }
  w.Write([]byte(fmt.Sprintf("title:%s", article.Title)))
}

使用 mount ,将路由分开

{
  r := chi.NewRouter()
  ...
  // mount 其他的路由器
  r.Mount("/admin", adminRouter())
}

// 独立的路由器
func adminRouter() http.Handler {
  r := chi.NewRouter()
  r.Use(AdminOnly)                // 使用中间件
  r.Get("/", adminIndex)
  r.Get("/accounts", adminListAccounts)
  return r
}

使用 chi 的中间件,chi 的中间件,就是 net/http 的中间件。 接受一个 http.Handler 作为参数,返回一个 http.Handler 。 通过中间件,可以写一些共同的前处理

 r := chi.NewRouter()

  // 官方提供的middleware
  r.Use(middleware.RequestID)
  r.Use(middleware.RealIP)
  r.Use(middleware.Logger)
  r.Use(middleware.Recoverer)

  // 自己开发的middleware
  r.Use(MyMiddleWare)

  r.Route("/admin", func(r chi.Router) {
         // 管理画面认证
   r.Use(AdminOnly)
   r.Get("/", adminIndex)
   r.Get("/accounts", adminListAccounts)
  })
}

// 管理画面认证的中间件
func AdminOnly(next http.Handler) http.Handler {
  return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()
    perm, ok := ctx.Value("acl.permission").(YourPermissionType)
    if !ok || !perm.IsAdmin() {
      http.Error(w, http.StatusText(403), 403)
      return
    }
    next.ServeHTTP(w, r)
  })
}

database/sql

使用数据库时,除了database/sql包本身,还需要引入想使用的特定数据库驱动。

一般使用_别名来匿名导入驱动,驱动的导出名字不会出现在当前作用域中。导入时,驱动的初始化函数会调用sql.Register将自己注册在database/sql包的全局变量sql.drivers中,以便以后通过sql.Open访问。

比如,导入PostgreSQL的驱动

import (
    "database/sql"
    _ "github.com/jackx/pgx/stdlib"
)

加载驱动包后,需要使用sql.Open()来创建sql.DB。

func main() {
    db, err := sql.Open("pgx","postgres://localhost:5432/postgres")
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()
}

执行sql.Open()并未实际建立起到数据库的连接,也不会验证驱动参数。第一个实际的连接会惰性求值,延迟到第一次需要时建立。 sql.DB对象是为了长连接而设计的,不要频繁Open()和Close()数据库。 而应该为每个待访问的数据库创建一个sql.DB实例,并在用完前一直保留它。需要时可将其作为参数传递,或注册为全局对象。

有了sql.DB实例之后就可以开始执行查询语句了。 Go将数据库操作分为两类:Query与Exec。两者的区别在于前者会返回结果,而后者不会。 Query表示查询,它会从数据库获取查询结果(一系列行,可能为空)。 Exec表示执行语句,它不会返回行。

rows, err := db.Query("SELECT generate_series(1,$1)", 10)
if err != nil {
    log.Fatal(err)
}
defer rows.Close()

// 使用rows.Next()作为循环条件,迭代读取结果集
for rows.Next() {
    // 使用rows.Scan从结果集中获取一行结果
    if err = rows.Scan(&n); err != nil {
        log.Fatal(err)
    }
    sum += n
}

// 使用rows.Err()在退出迭代后检查错误
if rows.Err() != nil {
    log.Fatal(err)
}

如果一个查询每次最多返回一行,那么可以用快捷的单行查询来替代冗长的标准查询。

var sum int
err := db.QueryRow("SELECT sum(n) FROM (SELECT generate_series(1,$1) as n) a;", 10).Scan(&sum)
if err != nil {
    fmt.Println(err)
}

需要注意的是,对于单行查询,Go将没有结果的情况视为错误。sql包中定义了一个特殊的错误常量ErrNoRows,当结果为空时,QueryRow().Scan()会返回它。

Query和Exec返回的结果不同,两者的签名分别是

func (s *Stmt) Query(args ...interface{}) (*Rows, error)
func (s *Stmt) Exec(args ...interface{}) (Result, error)

Exec不需要返回数据集,返回的结果是ResultResult接口允许获取执行结果的元数据

type Result interface {
    // 用于返回自增ID,并不是所有的关系型数据库都有这个功能。
    LastInsertId() (int64, error)
    // 返回受影响的行数。
    RowsAffected() (int64, error)
}

比如

stmt, err := db.Prepare(`INSERT INTO test_users(id,name) VALUES ($1,$2) RETURNING id`)
if err != nil {
    fmt.Println(err.Error())
}
res, err := stmt.Exec(1, "Alice")

if err != nil {
    fmt.Println(err)
} else {
    fmt.Println(res.RowsAffected())
    fmt.Println(res.LastInsertId())
}

通过db.Begin()来开启一个事务,Begin方法会返回一个事务对象Tx。 在结果变量Tx上调用Commit()或者Rollback()方法会提交或回滚变更,并关闭事务。

db, _ = sql.Open( "mysql" , "root:@tcp(127.0.0.1:3306)/test?charset=utf8" )
db.SetMaxOpenConns( 2000 )
db.SetMaxIdleConns( 1000 )
db.Ping()

SetMaxOpenConns用于设置最大打开的连接数,默认值为0表示不限制。 SetMaxIdleConns用于设置闲置的连接数。设置最大的连接数,可以避免并发太高导致连接mysql出现too many connections的错误。设置闲置的连接数则当开启的一个连接使用完成后可以放在池里等候下一次使用。

日志框架 Zap

github.com/uber-go/zap 是非常快的、结构化的,分日志级别的Go日志库。 将 debug 级别的log写入。

import (
    "github.com/natefinch/lumberjack"
    "go.uber.org/zap"
    "go.uber.org/zap/zapcore"
)
var sugarLogger *zap.SugaredLogger

func InitLogger() {
    writeSyncer := getLogWriter()
    encoder := getEncoder()
    core := zapcore.NewCore(encoder, writeSyncer, zapcore.DebugLevel)

    logger := zap.New(core)
    sugarLogger = logger.Sugar()
}

func getEncoder() zapcore.Encoder {
    return zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig())
}

func getLogWriter() zapcore.WriteSyncer {
    file, _ := os.Create("./test.log")
    return zapcore.AddSync(file)
}

使用Lumberjack进行日志切割归档

func getLogWriter() zapcore.WriteSyncer {
    lumberJackLogger := &lumberjack.Logger{
        Filename:   "./test.log",
        MaxSize:    10,
        MaxBackups: 5,
        MaxAge:     30,
        Compress:   false,
    }
    return zapcore.AddSync(lumberJackLogger)
}

Lumberjack Logger采用以下属性作为输入

Filename: 日志文件的位置
MaxSize:在进行切割之前,日志文件的最大大小(以MB为单位)
MaxBackups:保留旧文件的最大个数
MaxAges:保留旧文件的最大天数
Compress:是否压缩/归档旧文件

encoding/json

将一个对象编码成JSON数据,接受一个interface{}对象,返回[]byte和error。 Marshal函数将会递归遍历整个对象,依次按成员类型对这个对象进行编码。

类型转换规则如下

bool类型 转换为JSON的Boolean
整数,浮点数等数值类型 转换为JSON的Number
string 转换为JSON的字符串(带""引号)
struct 转换为JSON的Object,再根据各个成员的类型递归打包
数组或切片 转换为JSON的Array
[]byte 会先进行base64编码然后转换为JSON字符串
map 转换为JSON的Object,key必须是string
interface{} 按照内部的实际类型进行转换
nil 转为JSON的null
channel,func等类型 会返回UnsupportedTypeError

比如

type ColorGroup struct {
    ID     int
    Name   string
    Colors []string
}

func main() {
    group := ColorGroup{
        ID:     1,
        Name:   "Reds",
        Colors: []string{"Crimson", "Red", "Ruby", "Maroon"},
    }
    b, err := json.Marshal(group)
    if err != nil {
        fmt.Println("error:", err)
    }
    os.Stdout.Write(b)
}

将JSON数据解码,接受一个byte切片

func Unmarshal(data []byte, v interface{}) error

比如

var jsonBlob = []byte(`[
    {"Name": "Platypus", "Order": "Monotremata"},
    {"Name": "Quoll",    "Order": "Dasyuromorphia"}
]`)
type Animal struct {
    Name  string
    Order string
}
var animals []Animal
err2 := json.Unmarshal(jsonBlob, &animals)
if err2 != nil {
    fmt.Println("error:", err2)
}
fmt.Printf("%+v", animals)

结构体必须是大写字母开头的成员才会被JSON处理到,小写字母开头的成员不会有影响。

Mashal时,结构体的成员变量名将会直接作为JSON Object的key打包成JSON Unmashal时,会自动匹配对应的变量名进行赋值,大小写不敏感。 Unmarshal时,如果JSON中有多余的字段,会被直接抛弃掉;如果JSON缺少某个字段,则直接忽略不对结构体中变量赋值,不会报错。

如果希望手动配置结构体的成员和JSON字段的对应关系,可以在定义结构体的时候给成员打标签

type Message struct {
    Name string `json:"msg_name"`       // 对应JSON的msg_name
    Body string `json:"body,omitempty"` // 如果为空置则忽略字段
    Time int64  `json:"-"`              // 直接忽略字段
}
var m = Message{
    Name: "Alice",
    Body: "",
    Time: 1294706395881547000,
}
data, err := json.Marshal(m)
if err != nil {
    fmt.Printf(err.Error())
    return
}
fmt.Println(string(data))

Output:
{"msg_name":"Alice"}

现在有这么一种场景,结构体中的其中一个字段的格式是未知的:

type Command struct {
    ID   int
    Cmd  string
    Args *json.RawMessage
}

使用json.RawMessage的话,Args字段在Unmarshal时不会被解析,直接将字节数据赋值给Args。我们可以能先解包第一层的JSON数据,然后根据Cmd的值,再确定Args的具体类型进行第二次Unmarshal。

这里要注意的是,一定要使用指针类型*json.RawMessage,否则在Args会被认为是[]byte类型,在打包时会被打包成base64编码的字符串。

HttpClient

可以使用 net/http 包中的 client,发送 http 请求。

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

func main() {
    client := &http.Client{}
    request, _ := http.NewRequest("GET", "http://192.168.4.104:8800", nil)
    request.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
    request.Header.Set("Accept-Charset", "GBK,utf-8;q=0.7,*;q=0.3")
    request.Header.Set("Accept-Encoding", "gzip,deflate,sdch")
    request.Header.Set("Accept-Language", "zh-CN,zh;q=0.8")
    request.Header.Set("Cache-Control", "max-age=0")
    request.Header.Set("Connection", "keep-alive")

    response, _ := client.Do(request)
    if response.StatusCode == 200 {
        body, _ := ioutil.ReadAll(response.Body)
        bodystr := string(body)
        fmt.Println(bodystr)
    }
}

go-redis

package redisdb

import (
    "fmt"
    "github.com/go-redis/redis" // 实现了redis连接池
    "tbkt/config"
    "tbkt/logger"
    "time"
)

// 定义redis链接池
var client *redis.Client

// 初始化redis链接池
func init(){
    client = redis.NewClient(&redis.Options{
        Addr:     config.RedisAddr, // Redis地址
        Password: config.RedisPwd,  // Redis账号
        DB:       config.RedisDB,   // Redis库
        PoolSize: config.PoolSize,  // Redis连接池大小
        MaxRetries: 3,              // 最大重试次数
        IdleTimeout: 10*time.Second,            // 空闲链接超时时间
    })
    pong, err := client.Ping().Result()
    if err == redis.Nil {
        logger.Info("Redis异常")
    } else if err != nil {
        logger.Info("失败:", err)
    } else {
        logger.Info(pong)
    }
}

// 向key的hash中添加元素field的值
func HashSet(key, field string, data interface{}) {
    err := client.HSet(key, field, data)
    if err != nil {
        logger.Error("Redis HSet Error:", err)
    }
}

// 批量向key的hash添加对应元素field的值
func BatchHashSet(key string, fields map[string]interface{}) string {
    val, err := client.HMSet(key, fields).Result()
    if err != nil {
        logger.Error("Redis HMSet Error:", err)
    }
    return val
}

// 通过key获取hash的元素值
func HashGet(key, field string) string {
    result := ""
    val, err := client.HGet(key, field).Result()
    if err == redis.Nil {
        logger.Info("Key Doesn't Exists:", field)
        return result
    }else if err != nil {
        logger.Info("Redis HGet Error:", err)
        return result
    }
    return val
}

// 批量获取key的hash中对应多元素值
func BatchHashGet(key string, fields ...string) map[string]interface{} {
    resMap := make(map[string]interface{})
    for _, field := range fields {
        var result interface{}
        val, err := client.HGet(key, fmt.Sprintf("%s", field)).Result()
        if err == redis.Nil {
            logger.Info("Key Doesn't Exists:", field)
            resMap[field] = result
        }else if err != nil {
            logger.Info("Redis HMGet Error:", err)
            resMap[field] = result
        }
        if val != "" {
            resMap[field] = val
        }else {
            resMap[field] = result
        }
    }
    return resMap
}

// 获取自增唯一ID
func Incr(key string) int {
    val, err := client.Incr(key).Result()
    if err != nil {
        logger.Error("Redis Incr Error:", err)
    }
    return int(val)
}

// 添加集合数据
func SetAdd(key, val string){
    client.SAdd(key, val)
}

// 从集合中获取数据
func SetGet(key string)[]string{
    val, err := client.SMembers(key).Result()
    if err != nil{
        logger.Error("Redis SMembers Error:", err)
    }
    return val
}

gofmt

gofmt 可以将 go 的源代码格式化成符合官方统一标准的风格,属于语法风格层面上的小型重构。

gofmt  -w  hello.go  

会格式化该源文件的代码然后将格式化后的代码覆盖原始内容

gofmt  -w  *.go

格式化并重写所有 Go 源文件

gofmt   project  

格式化并重写project目录下所有 Go 源文件

go vet

vet 是一个优雅的工具,每个 Go 开发者都要知道并会使用它。 它可以在编译阶段和运行阶段发现bug。 vet 是 Go tool 套件的一部分,它和 go 编译器一起发布,这意味着它不需要额外的依赖,可以很方便地通过以下的命令调用。

比如下面的程序,在Go里面编译器没有任何输出。

package mainimport "fmt"

func main() {
    str := "hello world!"
    fmt.Printf("%d\n", str)
}

这是vet发挥作用的时候了。

$ go tool vet ex1.go
ex1.go:7: arg str for printf verb %d of wrong type: string

effective_go

effective_go

中文版

wangbo123855842 commented 3 years ago

Wire

Wire 是一个轻巧的 Golang 依赖注入工具。它由 Go Cloud 团队开发,通过自动生成代码的方式在编译期完成依赖注入

作为一个代码生成工具, Wire 可以生成 Go 源码并在编译期完成依赖注入。Wire 生成的代码与手写无异。

安装

go get github.com/google/wire/cmd/wire

使用

wire.go 中,调用各个结构体(对象)的初始化方法,比如

wire.Build(
    controller.NewTenantController,
    service.NewTenantService,
    usecase.NewTenantUseCase,
)

Controller 的初始化方法 NewTenantController 里面,需要两个成员变量 TenantService 和 TenantUseCase

type tenantController struct {
    service.TenantService
    usecase.TenantUseCase
}

func NewTenantController(ts service.TenantService, tu usecase.TenantUseCase) TenantController {
    return &tenantController{TenantService: ts, TenantUseCase: tu}
}

有了这些代码以后,运行 wire 命令将生成 wire_gen.go 文件,其中保存了 injector 函数的真正实现。

tenantService := service.NewTenantService(xxx)
tenantUseCase := usecase.NewTenantUseCase(xxx)
tenantController := controller.NewTenantController(tenantService, tenantUseCase)

要触发“生成”动作有两种方式:go generatewire前者仅在 wire_gen.go 已存在的情况下有效,而后者在任何时候都有可以调用。

wangbo123855842 commented 3 years ago

Go Session

在 Go 的标准库中并没有提供对 Sessoin 的实现。我们需要自己来实现 Session。

实现 Session 主要需要考虑以下几点

实现的过程,参考 Go 语言中使用 Session

wangbo123855842 commented 3 years ago

sqlx

官方文档

在项目中我们通常可能会使用 database/sql 连接MySQL数据库。 sqlx 可以认为是 Go 语言内置 database/sql 的超集,它在优秀的内置 database/sql 基础上提供了一组扩展。

Handle Types

sqlx 设计和 database/sql 使用方法是一样的。包含有4中主要的 handle types

所有的 handler types 都提供了对 database/sql 的兼容,意味着当你调用 sqlx.DB.Query 时,可以直接替换为 sql.DB.Query。 这就使得 sqlx 可以很容易的加入到已有的数据库项目中。

此外,sqlx 还有两个 cursor 类型

连级到数据库

你可以通过 Open 创建一个 sqlx.DB 或 **通过 NewDb 从已存在的 sql.DB 中创建一个新的 sqlx.DB。

var db *sqlx.DB

// exactly the same as the built-in
db = sqlx.Open("sqlite3", ":memory:")

// from a pre-existing sql.DB; note the required driverName
db = sqlx.NewDb(sql.Open("sqlite3", ":memory:"), "sqlite3")

// 创建一个连接,测试是否通过
err = db.Ping()

调用 connect,打开一个 DB 的同时连接 DB。这个函数打开一个新的 DB 并尝试 Ping

db, err := sqlx.Connect("mysql", "xxx")
if err != nil {
    log.Fatalln(err)
}
db.SetMaxIdleConns(0) // 设置理想最大连接数

查询

取得结果的时候,使用 rows.StructScan

var survey model.Survey
const q = `SELECT tenant_id, survey_id, status, created_at, created_by, updated_at, updated_by FROM survey WHERE survey_id = ? AND tenant_id = ?`

if err := r.db.QueryRowxContext(ctx, q, surveyID, tenantID).StructScan(&survey); err != nil {
    if errors.Is(err, sql.ErrNoRows) {
        return nil, isxerr.New(err, isxerr.ResourceNotFound, "survey not found")
    }
    return nil, err
}
return &survey, nil

查询多条

var questions []*model.SurveyQuestion
const q = `SELECT tenant_id, survey_id, question_id, description, question_type, answer_required, question_order FROM survey_question WHERE survey_id = ? AND tenant_id = ? ORDER BY question_order`

if err := r.db.SelectContext(ctx, &questions, q, surveyID, tenantID); err != nil {
    return nil, err
}
return questions, nil

sql.Tx 的 ExecContext方法来执行 insert,update,delete

var tx *sql.Tx
const q = `INSERT INTO survey (tenant_id, survey_id, status, created_by, updated_by) VALUES (?,?,?,?,?) ON DUPLICATE KEY UPDATE status = ?, updated_by = ?`
if _, err := tx.ExecContext(ctx, q, tenantID, survey.SurveyID, survey.Status, userID, userID,
    survey.Status, userID,
); err != nil {
    return err
}
return nil
wangbo123855842 commented 3 years ago

testing 包

golang 标准库 testing 包为 Go 代码支持了自动化测试。使用 go test 命令来执行

函数测试定义

func TestXxx(*testing.T)

go testing 包的介绍和使用 Go Test 总结

stretchr/testify

golang的测试框架stretchr/testify

wangbo123855842 commented 3 years ago

gomock

スクリーンショット 2020-10-19 12 25 22