Golang

一、Go语言基础

1. Golang为何高效

  • Go语言拥有与Java一样的GC机制,且有c++的指针,丰富的标准库,Go目前已经内置了大量的库
  • 协程(goroutine)和信道(channel):值得称赞的是,Go 为生成协程和使用信道提供了轻量级的语法。
  • 语言层面支持并发,这个就是Go最大的特色,天生的支持并发。

不像以前我们要用Thread库 来新建线程,还要用线程安全的队列库来共享数据。

for i := 0; i < 10; i++ {
//前面加上go就可以直接调用协程运行
go func(i int) {
//time.Sleep(time.Second * 1)
fmt.Println(i)
}(i)
}
  • 部署简单,可直接编译成机器码,不依赖其他库

采用知乎上一位与底层硬件打交道的开发人员的话:


225 人赞同了该回答
好多答主都是做WEB这块的,可能会忽略一些特殊领域,比如我们这些做智能终端设备相关项目的。现在很多智能终端系统,例如我们常用的快递柜、取票机等。基本上操作系统都采用了Android或者Linux系统,硬件采用ARM主板和一堆的控制板。基本上甲方都需要加一些系统定制的内容,比如状态栏隐藏、外设驱动适配等,这些都是系统级,采用C/C++/Java完成内核和系统的定制,这个没什么说的。重点是对这一堆控制板的底层接口、逻辑控制和上层接口的实现。以前我们都是采用APP里的NDK来实现,大量代码耗费在Java和C++的相互调用上。后来直接采用C/C++直接编译为可执行文件通过远程调用提供给上层,开发效率有所提升。然而,甲方需求中很多新出现的内容开始让我们应接不暇。例如:采用JSON格式通讯,采用浏览器作为客户端调用底层,多个控制板需要并发同时控制。这些东西有的对于Java来说好弄,有的对于C++来说好弄,但是没有一个能同时方便的实现上述功能的,直到发现了Go。考虑到行业人员因素,像我们这种做平台和控制的软件工程师,大多对C和C++较为熟悉,转去学习Go做这种必要的后台程序较为方便。而且硬件平台又特殊在不能方便的部署各类服务器,用Java较为不便,用C/C++也不方便。即使采用Java和C++,又会存在交叉编译,以及一大堆依赖库的坑。而Go的标准库都基本具备了这些功能,同时又能合理的利用现有的C/C++代码。比较忙,写的比较粗略,大致意思就是这样。

发布于 2017-09-18


2. 一个基本的go程序

主函数入口要求:

  • 必须是main包,package main
  • 必须是main方法,func main()
  • 文件名不一定是main.go,但一般习惯与main.go
package main // 包,表明代码所在的模块

import "fmt"// 引入代码依赖
//功能实现
func main() {
//不需要返回值,可以通过os.Exit返回状态
//os.Args来获取命令行参数
fmt.Println("Hello World")
}
  • golang支持自动类型推断的赋值,但不支持隐式的类型转换,别名和原有类型也不能隐式转换
//1.golang支持自动类型推断的赋值
//2.golang不支持隐式的类型转换,别名和原有类型也不能隐式转换
func TestFibList(t *testing.T) {
// var a int = 1
// var b int = 1
// var (
// a int = 1
// b = 1
// )
a := 1
// a := 1
b := 1
t.Log(a)
for i := 0; i < 5; i++ {
t.Log(" ", b)
tmp := a
a = b
b = tmp + a
}
}
  • 不支持指针运算
  • 字符串是值类型,默认初始化是空字符串,而不是nil
//1.不支持指针运算
a := 1
aPtr := &a
//aPtr = aPtr + 1

//2.字符串是值类型,默认初始化是空字符串,而不是nil
var s string
t.Log("*" + s + "*") //初始化零值是“”

3. 数组与切片

  • 数组是固定大小的,而切片是可以伸缩的数组。声明只是少写一个容量大小。
  • 数组必须是相同维数且相同个数才能比较,每个元素相同才相等;而切片只能和nil比较
//1.数组比较
a := [...]int{1, 2, 3, 4}
b := [...]int{1, 3, 2, 4}
// c := [...]int{1, 2, 3, 4, 5}
d := [...]int{1, 2, 3, 4}
t.Log(a == b)
//t.Log(a == c)
t.Log(a == d)
//2.切片只能与nil进行比较
a := []int{1, 2, 3, 4}
b := []int{1, 2, 3, 4}
// if a == b { //切片只能和nil比较
// t.Log("equal")
// }
t.Log(a, b)
  • 数组的遍历与截取,与python类似
func TestArrayTravel(t *testing.T) {
for i := 0; i < len(arr); i++ {
t.Log(arr[i])
}
//for idx, e := range arr3 {
// t.Log(idx, e)
//}
for _, e := range arr {
t.Log(e)
}
}

func TestArraySection(t *testing.T) {
arr := [...]int{1, 2, 3, 4, 5}
//a[开始索引,结束索引),左闭右开
arr_sec := arr[3:]
t.Log(arr_sec)
}
  • 切片的内部结构

无非就是维护一个数组,扩容时重新分配然后挪过去,维护一个大小和容量。

//切片的一般声明格式,3是大小,5是容量
s2 := make([]int, 3, 5)
t.Log(len(s2), cap(s2))

//切片的添加值,类似于StringBuilder这些可变的字符序列
s := []int{}
for i := 0; i < 10; i++ {
s = append(s, i)
t.Log(len(s), cap(s))
}

切片内部结构

  • 切片的共享存储空间

func TestSliceShareMemory(t *testing.T) {
year := []string{"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep",
"Oct", "Nov", "Dec"}
//两个切片有交集,共享区域
Q2 := year[3:6]
summer := year[5:8]
//修改共享区域,两者都修改
summer[0] = "Unknow"
t.Log(Q2)
t.Log(year)
}

4. map与set实现

//map初始化,10位容量,长度len为0
m3 := make(map[int]int, 10)
t.Logf("len m3=%d", len(m3))//len m3=0

//map遍历
func TestTravelMap(t *testing.T) {
m1 := map[int]int{1: 1, 2: 4, 3: 9}
for k, v := range m1 {
//k - v键值对
t.Log(k, v)
}
}

// 通过ok返回true或者false来判断是否存在,而不是nil
if v, ok := m1[3]; ok {
t.Log(ok)
t.Logf("Key 3's value is %d", v)
} else {
t.Log(ok)
t.Log("key 3 is not existing.")
}
  • map的value可以是个方法func(op type),这样就可以实现多态。

    格式为map[type]type,第一个type是key的类型,第二个是value的类型

func TestMapWithFunValue(t *testing.T) {
//Map实现多态和工厂模式
m := map[int]func(op int) int{}
m[1] = func(op int) int { return op }
m[2] = func(op int) int { return op * op }
m[3] = func(op int) int { return op * op * op }
t.Log(m[1](2), m[2](2), m[3](2))
}
  • map来实现set

golang内置集合中没有set的实现,但可以用map[type]bool来实现元素的唯一性

func TestMapForSet(t *testing.T) {
//Map实现Set
mySet := map[int]bool{}
mySet[1] = true
n := 3
if mySet[n] {
t.Logf("%d is existing", n)
} else {
t.Logf("%d is not existing", n)
}
mySet[3] = true
t.Log(len(mySet))
delete(mySet, 1)
if mySet[n] {
t.Logf("%d is existing", n)
} else {
t.Logf("%d is not existing", n)
}
}

5. 字符串

开始我们提到了string是个值类型,这点区别于其他语言,它不是引用或者是指针类型

string是只读的byte slice,len函数时返回它的字节数

  • 常见的字符串函数
//切割字符串
func TestStringFn(t *testing.T) {
s := "A,B,C"
parts := strings.Split(s, ",")
for _, part := range parts {
t.Log(part)
}
t.Log(strings.Join(parts, "-"))
}
//字符串与整型的相互转换
func TestConv(t *testing.T) {
s := strconv.Itoa(10)
t.Log("str" + s)
if i, err := strconv.Atoi("10"); err == nil {
t.Log(10 + i)
}
}

6. 函数是一等公民

  1. 可以有多个返回值
  2. 所以参数都是值传递,slice、map、channel会有引用传递的错觉
  3. 函数可以作为变量值
  4. 可以作为参数和返回值
//函数的结构
func (指针类型/结构类型,代表某接口/结构的方法) fun_name(ops ...int) return_type {
}
  1. 匿名函数

    定义一个匿名函数。

func() {

}

​ 定义一个匿名函数并调用执行

func() {

}()
package main

import "fmt"

func main() {

//匿名函数 1
//f1 为函数地址
f1 := func(x, y int) (z int) {
z = x + y
return
}
fmt.Println(f1)
fmt.Println(f1(5, 6))

//匿名函数 2
//直接创建匿名函数并运行
f2 := func(x, y int) (z int) {
z = x + y
return
}(7, 8)
fmt.Println(f2)

//匿名函数 2 (无参数的形式)
func() {
fmt.Println(9 + 10)
}() //刚开始学的时候一直不明白为什么后面要加个括号

}

7. 面向对象OOP

  • 封装对象
type Employee struct {
Id string
Name string
Age int
}
  • 方法
func (e *Employee) String() string {
//一般采用指针调用,不会出现内存拷贝的情况,是使用同一块内存空间
fmt.Printf("Address is %x", unsafe.Pointer(&e.Name))
return fmt.Sprintf("ID:%s/Name:%s/Age:%d", e.Id, e.Name, e.Age)
}

func TestStructOperations(t *testing.T) {
e := Employee{"0", "Bob", 20}
e2 := new(Employee) //返回指针,等价于 e2 := &Employee{"0", "Bob", 20}
fmt.Printf("e is %T\n", e)
fmt.Printf("Address is %x\n", unsafe.Pointer(&e.Name))
fmt.Println("调用string函数")
fmt.Printf(e.String())
}
//打印结果:虽然是结构调用string,但实际上是指针调用
//不会存在内存拷贝的情况,使用同一块内存空间
e is encap.Employee
Address is c042068880
调用string函数
Address is c042068880
ID:0/Name:Bob/Age:20
  • 接口:采用Duck Type式接口实现

接口的最佳使用实践:

  1. 倾向于使用小的接口定义,很多接口只包含一个方法
  2. 如果需要多个功能最好编写多个接口
  3. 只依赖于必要功能的最小接口
type Programmer interface {
WriteHelloWorld(name string) string
}

type GoProgrammer struct {
name string
}

//优点:可以先写方法,后面如果有重复的,再加接口(无需implement)
func (g *GoProgrammer) WriteHelloWorld(name string) string{
g.name = name
return g.name
}

func TestClient(t *testing.T) {
var p Programmer = new(GoProgrammer)
t.Log(p.WriteHelloWorld("yyf"))
}
  • Go不支持继承,但可以复合来复用

匿名类型嵌入,不是继承,因为如果内部是父类,外部是子类的话,就会产生问题。

父类定义的方法无法访问子类的数据方法

  1. 子类并没有继承父类的方法
  2. 不支持子类的替换
//假装是父类
type Pet struct {
}

func (p *Pet) Speak() {
fmt.Print("...")
}

func (p *Pet) SpeakTo(host string) {
p.Speak()
fmt.Println(" ", host)
}
//假装是子类
type Dog struct {
Pet
}

func (d *Dog) Speak() {
fmt.Print("Wang!")
}

//内嵌的类型 不能当成继承使用
//不支持访问子类的数据和方法重载
func TestDog(t *testing.T) {
dog := new(Dog)
dog.SpeakTo("Chao")
}

打印结果:dog调用自己里面pet的speakto方法,然后调用的是pet的speak方法,所以不是继承关系。

… Chao

  • 多态与空接口
  1. Golang实现多态,传入接口,则作为参数时必须是个指向子类多态的指针

例如:goProg := &GoProgrammer{},goProg是GoProgrammer的指针类型。

type Code string
type Programmer interface {
WriteHelloWorld() Code
}

//Go
type GoProgrammer struct {
}

func (p *GoProgrammer) WriteHelloWorld() Code {
return "fmt.Println(\"Hello World!\")"
}
//Java
type JavaProgrammer struct {
}

func (p *JavaProgrammer) WriteHelloWorld() Code {
return "System.out.Println(\"Hello World!\")"
}

//一个方法,传入programmer指针
func writeFirstProgram(p Programmer) {
fmt.Printf("%T %v\n", p, p.WriteHelloWorld())
}

func TestPolymorphism(t *testing.T) {
//Programmer是一个接口,则作为参数时,必须是个指向子类多态的指针
goProg := &GoProgrammer{}
javaProg := new(JavaProgrammer)
writeFirstProgram(goProg)
writeFirstProgram(javaProg)
}
  1. 空接口可以表示任何类型,像java中的Object类,但是要通过断言来判断类型。
//传入空接口(可以看做是指针),实现多态
func DoSomething(p interface{}) {
if i, ok := p.(int); ok {
fmt.Println("Integer", i)
return
}
if s, ok := p.(string); ok {
fmt.Println("string", s)
return
}
fmt.Println("Unknow Type")
//也可以用switch来实现
//switch v := p.(type) {
//case int:
// fmt.Println("Integer", v)
//case string:
// fmt.Println("String", v)
//default:
// fmt.Println("Unknow Type")
//}
}

func TestEmptyInterfaceAssertion(t *testing.T) {
DoSomething(10)
DoSomething("10")
}

8. 错误处理

Go与其他语言不同,没有异常机制,但是用errors来new一个错误实例

例如:errors.New("n should be not less than 2")

var LessThanTwoError = errors.New("n should be not less than 2")
var LargerThenHundredError = errors.New("n should be not larger than 100")

func GetFibonacci(n int) ([]int, error) {
if n < 2 {
return nil, LessThanTwoError
}
if n > 100 {
return nil, LargerThenHundredError
}
fibList := []int{1, 1}

for i := 2; i < n; i++ {
fibList = append(fibList, fibList[i-2]+fibList[i-1])
}
return fibList, nil
}

func TestGetFibonacci(t *testing.T) {
if v, err := GetFibonacci(1); err != nil {
t.Error(err)
} else {
t.Log(v)
}
}

异常机制一般及早返回,避免代码嵌套

// 一般及早返回,避免代码嵌套
func GetFibonacci2(str string) {
var (
i int
err error
list []int
)
if i, err = strconv.Atoi(str); err != nil {
fmt.Println("Error", err)
return
}
if list, err = GetFibonacci(i); err != nil {

fmt.Println("Error", err)
return
}
fmt.Println(list)

}

9. panic与recover

  • panic用于不可恢复的错误退出前执行defer指定方法中的内容,类似于析构函数。OS.exit不会执行defer

  • recover用于恢复错误,就像try-catch,捕捉panic里面的错误,执行打印异常等等操作,不必返回异常。

//panic
func TestPanicVxExit(t *testing.T) {
//panic会调用defer函数然后退出(类似于析构函数)
defer func() {
fmt.Println("执行了defer方法")
}()
fmt.Println("Start")
panic(errors.New("Something wrong!"))
//os.Exit(-1)
}
执行结果:抛出异常
//recover
func TestRecover(t *testing.T) {
//recover就像try-catch,捕捉panic里面的错误
defer func() {
if err := recover(); err != nil {
fmt.Println("recovered from ", err)
}
}()
fmt.Println("Start")
panic(errors.New("Something wrong!"))
}
打印:
Start
recovered from Something wrong!

10. package引用

  • GOPATH的设置

传统的方式是放到src目录下。举个栗子,你的GOPATH是D:\workspace\www\src\go_work,那么你的项目应该新建目录D:\workspace\www\src\go_work\project_01,D:\workspace\www\src\go_work\project_02。假设project_01内的结构是

main.go

file1

file1.go(packagefile1)

假设project_02内的结构是

main.go

file2

file1.go(packagefile2)

那么你在project_01内要想引用project_02的packagefile2,应该写importproject_02/file2


  • 大写开头才能在包外访问,小写则不能
package series
import "fmt"
//小写的方法是不能在包外访问的
func square(n int) int {
return n * n
}

func GetFibonacciSeries(n int) []int {
ret := []int{1, 1}
for i := 2; i < n; i++ {
ret = append(ret, ret[i-2]+ret[i-1])
}
return ret
}
//包外引用series中的方法
package client
import (
"go_learning/ch15/series"
"testing"
)
func TestPackage(t *testing.T) {
t.Log(series.GetFibonacciSeries(5))
//t.Log(series.square(5)) 小写的不能访问
}
  • 获取远程的依赖

go get -u + github路径,例如go get -u github.com/easierway/concurrent_map

  • package的init方法

顾名思义在main函数执行时,会先将所有依赖的init方法执行一遍

//1.外部引用
package series
import "fmt"

func init() {
fmt.Println("init1")
}

func init() {
fmt.Println("init2")
}

//小写的方法是不能在包外访问的
func square(n int) int {
return n * n
}

func GetFibonacciSeries(n int) []int {
ret := []int{1, 1}
for i := 2; i < n; i++ {
ret = append(ret, ret[i-2]+ret[i-1])
}
return ret
}
//2.调用方法
package client
import (
"fmt"
"go_learning/ch15/series"
"testing"
)

func TestPackage(t *testing.T) {
fmt.Println(series.GetFibonacciSeries(5))
//fmt.Println(series.square(5)) 小写的不能访问
}
//打印
init1
init2
[1 1 2 3 5]
  • 下划线_的作用

有些时候我们并不需要把整个包都导入进来,仅仅是是希望它执行init()函数而已。这个时候就可以使用 import _ 引用该包。即使用【import _ 包路径】只是引用该包,仅仅是为了调用init()函数,所以无法通过包名来调用包中的其他函数。

在框架中使用格为重要,一些依赖的init方法执行。例如beego框架中的run方法

package main

import (
//引入routers包时,会执行其init方法
_ "BeegoDemo1/routers"
"github.com/astaxie/beego"
)

func main() {
beego.Run()
}

11. 文件操作

11.1 读操作

  • 带缓冲区,读取一个换行结束,适合大文件
file , err := os.Open("d:/test.txt")
if err != nil {
fmt.Println("open file err=", err)
}

//当函数退出时,要及时的关闭file
defer file.Close() //要及时关闭file句柄,否则会有内存泄漏.

// 创建一个 *Reader ,是带缓冲的,默认大小4096
reader := bufio.NewReader(file)
//循环的读取文件的内容
for {
str, err := reader.ReadString('\n') // 读到一个换行就结束
if err == io.EOF { // io.EOF表示文件的末尾
break
}
//输出内容
fmt.Printf(str)
}

fmt.Println("文件读取结束...")
  • ioutil一次性读取全部到内存,适合小文件
//使用ioutil.ReadFile一次性将文件读取到位
file := "d:/test.txt"
//文件的Open和Close被封装到 ReadFile 函数内部
content, err := ioutil.ReadFile(file)
if err != nil {
fmt.Printf("read file err=%v", err)
}

//fmt.Printf("%v", content) // []byte
fmt.Printf("%v", string(content)) // string内容

11.2 写操作

与读大同小异了,先写入缓冲区,再用writer来写入writer.Flush()文件。

创建输出流时,有几个文件打开模式

const (
// Exactly one of O_RDONLY, O_WRONLY, or O_RDWR must be specified.
O_RDONLY int = syscall.O_RDONLY // open the file read-only.
O_WRONLY int = syscall.O_WRONLY // open the file write-only.
O_RDWR int = syscall.O_RDWR // open the file read-write.
// The remaining values may be or'ed in to control behavior.
O_APPEND int = syscall.O_APPEND // append data to the file when writing.
O_CREATE int = syscall.O_CREAT // create a new file if none exists.
O_EXCL int = syscall.O_EXCL // used with O_CREATE, file must not exist.
O_SYNC int = syscall.O_SYNC // open for synchronous I/O.
O_TRUNC int = syscall.O_TRUNC // truncate regular writable file when opened.
)
//打开一个存在的文件中,将原来的内容覆盖
os.OpenFile(filePath, os.O_WRONLY | os.O_TRUNC, 0666)
//打开一个存在的文件,在原来的内容追加
file, err := os.OpenFile(filePath, os.O_WRONLY | os.O_APPEND, 0666)
//打开一个存在的文件,将原来的内容读出显示在终端,并且追加
//这种就是又要读,又要写
file, err := os.OpenFile(filePath, os.O_RDWR | os.O_APPEND, 0666)
//写入测试
filePath := "d:/abc.txt"
file, err := os.OpenFile(filePath, os.O_WRONLY | os.O_CREATE, 0666)
if err != nil {
fmt.Printf("open file err=%v\n", err)
return
}
//及时关闭file句柄
defer file.Close()

//准备写入5句 "hello, Gardon"
str := "hello,Gardon\r\n" // \r\n 表示换行
//写入时,使用带缓存的 *Writer
writer := bufio.NewWriter(file)
for i := 0; i < 5; i++ {
writer.WriteString(str)
}
//写入文件
writer.Flush()

二、并发编程

1. 协程机制

进程、线程(内核级线程)、协程(用户级线程)

  • goroutine 和协程

     本质上,goroutine 就是协程。 不同的是,Golang 在 runtime、系统调用等多方面对 goroutine 调度进行了封装和处理,当遇到长时间执行或者进行系统调用时,会主动把当前 goroutine 的CPU (P) 转让出去,让其他 goroutine 能被调度并执行,也就是 Golang 从语言层面支持了协程。

    ​ Golang 的一大特色就是从语言层面原生支持协程,在函数或者方法前面加 go关键字就可创建一个协程。

  • 进程、线程、协程区别

    对于 进程、线程,都是有内核进行调度,有 CPU 时间片的概念,进行 抢占式调度(有多种调度算法)

    对于 协程(用户级线程),这是对内核透明的,比线程更加轻量级的存在重点:是完全由用户程序进行调度控制的。因为是由用户程序自己控制,那么就很难像抢占式调度那样做到强制的 CPU 控制权切换到其他进程/线程,通常只能进行 协作式调度,需要协程自己主动把控制权转让出去之后,其他协程才能被执行到。协程不是进程也不是线程,是一个特殊的函数,这个函数可以在某个地方挂起,并且可以重新在挂起处外继续运行。

  • Thread与Groutine 与内核线程的对应关系

    Java Thread 是 1 : 1

    Groutine 是 M : N 的关系(多对多)

好处:如果一比一的话,发生切换就是直接进行内核线程的切换,消耗比较大

在这里插入图片描述

  • 协程发生上下文切换条件:
  1. 系统调用;
  2. 读写channel;
  3. gosched主动放弃,会将G扔进全局队列;
  • 协程阻塞

一个G如果发生I/O事件、阻塞等事件时:

(1)协程运行时会有一个守护进程,它会去进行计数,记录每个协程处理器完成的协程数目,当一段时间发现Processor完成的数量没有变化时,会往协程的任务栈里面插入一个特殊的标记,当协程运行到非内联函数时就会读到这个标记,会把自己中断下来插到协程等待队列的队尾,然后切换成下一个的协程运行。

(2)当某个协程被系统中断了,例如I/O需要等待的时候,为了提高整体的并发度,Processor会移动到另一个可使用的系统线程上去(例如从M0移到M1)。继续执行它所挂队列中的其他的协程,当被中断的协程被唤醒后,会把自己加到某个Processor的等待队列里或者全局等待队列中。中断和切换时,会保存上下文信息,例如寄存器和堆栈的信息。

在这里插入图片描述

2. 访问共享内存,并发操作

  • 锁机制 sync.Mutex,与其他语言的锁机制是一样的。

当我们访问共享变量时,加锁机制,控制并发访问

func TestCounterThreadSafe(t *testing.T) {
var mut sync.Mutex
counter := 0
for i := 0; i < 5000; i++ {
go func() {
defer func() {
mut.Unlock()
}()
mut.Lock()
counter++
}()
}
time.Sleep(1 * time.Second)
t.Logf("counter = %d", counter)
}
  • 使用sync.WaitGroup计数器可以执行完后进行通知,而不用强制等待固定时间
//等待一个或者多个协程处理完,再执行,类似于Java的countDownLatch计数器
//优点:不需要强制等待一定的时间,而第二种是直接通知
func TestCounterWaitGroup(t *testing.T) {
var mut sync.Mutex
var wg sync.WaitGroup
counter := 0
for i := 0; i < 5000; i++ {
//增加一个需要等待的
wg.Add(1)
go func() {
defer func() {
//一个协程执行完释放一个通知
wg.Done()
mut.Unlock()
}()
mut.Lock()
counter++
}()
}
//等待所有协程释放资源,然后通知你执行完
wg.Wait()
t.Logf("counter = %d", counter)
}

3. CSP并发机制

​ 描述了两个独立的并发实体通过共享的通讯 channel(管道)进行通信的并发模型。

  1. golang中的channel独立于两个实体对象,两个实体之间是匿名的,这个就实现实体中间的解耦。
  2. channel是有容量限制的,并且是异步的模型
  3. channel内部有锁对象来维护内部的同步机制。
  4. 同时channel也分阻塞非阻塞两种,只是声明有细微差别,加上容量大小罢了。
//(1)创建普通的channel,只要channel中的消息没被获取,就会一直阻塞等待在那里
//(2)创建BufferedChannel,非阻塞异步的channel
//retCh := make(chan string)
retCh := make(chan string, 1)
  • channel的
  • 阻塞和非阻塞测试
//挂起50微秒,返回个字符串
func service() string {
time.Sleep(time.Millisecond * 50)
return "Done"
}
//打印1,挂起100微妙,打印3
func otherTask() {
fmt.Println("1.working on something else.")
time.Sleep(time.Millisecond * 100)
fmt.Println("3.Task is done.")
}
//异步创建个channel
//(1)调用一个协程去执行打印2,放入channel,然后阻塞等待获取外部主函数中获取channel后再打印5
//(2)调用一个协程去执行打印2,放入channel,然后非阻塞的返回,打印5,
func AsyncService() chan string {
(1)retCh := make(chan string)
(2)retCh := make(chan string, 1)
go func() {
ret := service()
fmt.Println("2.returned result.")
retCh <- ret
fmt.Println("5.service exited.")
}()
return retCh
}

func TestAsynService(t *testing.T) {
//AsyncService只异步返回一个channel,先执行otherTask
retCh := AsyncService()
otherTask()
fmt.Println("4.In the channel is", <-retCh)//done
time.Sleep(time.Second * 1)
}
打印结果:
1)阻塞 (2)非阻塞
1.working on something else. 1.working on something else.
2.returned result. 2.returned result.
3.Task is done. 5.service exited.
4.In the channel is Done 3.Task is done.
5.service exited. 4.In the channel is Done

4. 多路选择和超时控制

  • golang提供select进行多种情况的选择,如果满足其中一个就执行,并且有默认的(都不满足)。
  • golang中多个并发程序的数据同步是采用通道来传输,超时设计是通过使用select和case语句,类似于switch和case,在每一个case里进行一个io操作,比如读或者写,在最后一个case里调用time包里的After方法,可以达到超时检测效果。
func service() string {
//通过睡眠时间来判断是否超时
(1)不超时time.Sleep(time.Millisecond * 50)
(2)超时time.Sleep(time.Millisecond * 100)
return "Done"
}
//AsyncService内部调用service
//避免超时,保证高可用行性
func TestSelect(t *testing.T) {
select {
//及时的返回,就打印通道中的值
case ret := <-AsyncService():
t.Log(ret)
//超100ms,则就打印超时
case <-time.After(time.Millisecond * 100):
t.Error("time out")
}
}

5. channel的关闭和广播机制

close(ch)通过close函数,对该channel进行关闭。

  • 所有的 channel 接收者都会在 channel 关闭时,立刻从阻塞等待中返回且上述 ok 值为 false。这个广播机制常被利用,进⾏向多个订阅者同时发送信号。如:退出信号。
  • v, ok <-ch; ok为 bool 值,true 表示正常接受,false 表示通道关闭
  • 向关闭的 channel 发送数据,会导致 panic

例如:下面实现当数据传送完毕时,通过close(ch)来关闭通道,通知其它从channel中获取数据的实体,当获取<-ch完毕后,返回false时,自动结束。

func dataProducer(ch chan int, wg *sync.WaitGroup) {
go func() {
for i := 0; i < 10; i++ {
ch <- i
}
//数据发送完毕,关闭channel
close(ch)
//如果通道关闭,仍然往通道里面写,会抛出panic
//ch <- 1 panic: send on closed channel
wg.Done()
}()
}

func dataReceiver(ch chan int, wg *sync.WaitGroup) {
go func() {
for {
//从channel中获取数据,true为正常接受,false为channel关闭
//广播机制,同时向多个协程发送channel关闭
if data, ok := <-ch; ok {
fmt.Println(data)
} else {
break
}
}
wg.Done()
}()
}

func TestCloseChannel(t *testing.T) {
var wg sync.WaitGroup
ch := make(chan int)
wg.Add(1)
dataProducer(ch, &wg)
//开两个接收者协程来从channel中获取数据
wg.Add(1)
dataReceiver(ch, &wg)
wg.Add(1)
dataReceiver(ch, &wg)
//都wg.Done()结束后
wg.Wait()
}

6. 任务的取消

  • 通过关闭channel,close(cancelChan)来取消任务

通过close函数的广播机制,a := <-cancelChan每个协程都接收到零值,从而知道channel关闭,退出。

func isCancelled(cancelChan chan struct{}) bool {
select {
case a := <-cancelChan:
fmt.Println("close函数,向channel发送了零值:", a)
return true
default:
return false
}
}

func TestCancel(t *testing.T) {
cancelChan := make(chan struct{}, 0)
//开启五个协程来进行执行channel任务
for i := 0; i < 5; i++ {
go func(i int, cancelCh chan struct{}) {
for {
if isCancelled(cancelCh) {
break
}
time.Sleep(time.Millisecond * 5)
}
//如果返回true,即给初始大小0的channel发送了close消息
//通过close广播机制,每个协程都接收到零值
fmt.Printf("协程%d Cancelled\n", i)
}(i, cancelChan)
}
//发生close命令
cancel_2(cancelChan)
time.Sleep(time.Second * 1)
}
  • 通过关闭context来取消关联任务

在这里插入图片描述

• 根 Context:通过 context.Background () 创建
• 子 Context:context.WithCancel(parentContext) 创建
ctx, cancel := context.WithCancel(context.Background())
当前 Context 被取消时,基于他的子 context 都会被取消
接收取消通知 <-ctx.Done()

func isCancelled(ctx context.Context) bool {
select {
//接受取消通知
case <-ctx.Done():
return true
default:
return false
}
}

//通过context来取消任务
func TestCancel(t *testing.T) {
//ctx为主任务,cancel为关闭函数
ctx, cancel := context.WithCancel(context.Background())
//开启五个协程来进行任务的关闭
for i := 0; i < 5; i++ {
go func(i int, ctx context.Context) {
for {
if isCancelled(ctx) {
break
}
time.Sleep(time.Millisecond * 5)
}
fmt.Println(i, "Cancelled")
}(i, ctx)
}
//取消context,接收到取消的通知,它的子context也会取消
cancel()
time.Sleep(time.Second * 1)
}

7. 常见的并发任务

7.1 只执行一次(单例模式)

golang在sync提供sync.Once这个对象来实现只执行一次的单例模式场景。

type Singleton struct {
data string
}
var singleInstance *Singleton
var once sync.Once

func GetSingletonObj() *Singleton {
once.Do(func() {
fmt.Println("Create Obj")
singleInstance = new(Singleton)
})
return singleInstance
}

func TestGetSingletonObj(t *testing.T) {
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
obj := GetSingletonObj()
//检验对象地址,是否为同一对象
fmt.Printf("%X\n", unsafe.Pointer(obj))
wg.Done()
}()
}
wg.Wait()
}

7.2 仅需任意任务完成

采用golang的channel的特点。当channel放入,就直接返回第一个放入的。

使用BuffedChannel来存数据,避免其它的协程阻塞,造成协程一直阻塞,内存泄漏。

func TestFirstResponse(t *testing.T) {
t.Log("Before:", runtime.NumGoroutine())
t.Log(FirstResponse(t))
time.Sleep(time.Second * 1)
t.Log("After:", runtime.NumGoroutine())
}

func FirstResponse(t *testing.T) string {
//开10个协程来往channel里面放数据,channel有值就返回
numOfRunner := 10
//采用BuffedChannel来防止内存泄露,执行完后协程仍然阻塞
ch := make(chan string, numOfRunner)
for i := 0; i < numOfRunner; i++ {
go func(i int) {
ret := runTask(i)
ch <- ret
}(i)
}
//当channel放入,就直接返回第一个放入的
return <-ch
}

func runTask(id int) string {
time.Sleep(10 * time.Millisecond)
return fmt.Sprintf("The result is from %d", id)
}

7.3 必须所有任务完成

在sync包下,有sync.WaitGroup来实现全部执行完返回,但我们采用csp机制下的channel来实现

在上文中,我们直接修改返回条件,将channel中所以都获取出来再返回。

for i := 0; i < numOfRunner; i++ {
go func(i int) {
ret := runTask(i)
ch <- ret
}(i)
}
finalRet := ""
for j := 0; j < numOfRunner; j++ {
finalRet += <-ch + "\n"
}
return finalRet

8. 对象池

还是基于golang中的channel来实现对象池技术,对buffedchannel进行存取操作,从而实现池化技术

//对象池
type ObjPool struct {
bufChan chan *ReusableObj
}
//池中对象
type ReusableObj struct {
poolName string
}
//对象池的操作方法
func NewObjPool(numOfObj int) *ObjPool {
objPool := ObjPool{}
//只能有一个channel对象,指针类型
objPool.bufChan = make(chan *ReusableObj, numOfObj)
//往channel里面注入池化的对象
for i := 0; i < numOfObj; i++ {
objPool.bufChan <- &ReusableObj{}
}
return &objPool
}

func (p *ObjPool) GetObj(timeout time.Duration) (*ReusableObj, error) {
select {
case ret := <-p.bufChan:
return ret, nil
case <-time.After(timeout): //超时控制
return nil, errors.New("time out")
}

}

func (p *ObjPool) ReleaseObj(obj *ReusableObj) error {
select {
case p.bufChan <- obj:
return nil
default:
//超出池的容量大小
return errors.New("overflow")
}
}

//测试方法
func TestObjPool(t *testing.T) {
pool := NewObjPool(10)
// if err := pool.ReleaseObj(&ReusableObj{}); err != nil { //尝试放置超出池大小的对象
// t.Error(err)
// }
for i := 0; i < 11; i++ {
//获取到池对象,如果里面没有获取超时,则抛出异常
if v, err := pool.GetObj(time.Second * 1); err != nil {
t.Error(err)
} else {
fmt.Printf("%T\n", v)
//放回到池中
if err := pool.ReleaseObj(v); err != nil {
t.Error(err)
}
}
}
fmt.Println("Done")
}

9. sync.Pool 对象缓存

名字虽然是pool,但更像个对象的缓存catch,避免重复创建提高开销,用以缓存对象。

  • sync.Pool对象的生命周期

每一次GC会清除掉sync.pool缓存的对象,所以对象的缓存有效期为下一次GC 之前。

  • sync.Pool对象的特点

• 适合于通过复用降低复杂对象的创建和 GC 代价
• 会对共享变量进行获取,所以有锁的开销
• 生命周期受 GC 影响,不适合于做连接池等,需自己管理生命周期的资源的池化

总结:是否使用sync.Pool取决于锁的对象创建开销大,还是sync.Pool里面对象的开销大

  • 对象的获取

在这里插入图片描述

  • 对象的放入

在这里插入图片描述

func TestSyncPool(t *testing.T) {
pool := &sync.Pool{
New: func() interface{} {
fmt.Println("Create a new object.")
return 100
},
}

v := pool.Get().(int)
fmt.Println(v)
pool.Put(3)
runtime.GC() //GC 会清除sync.pool中缓存的对象
v1, _ := pool.Get().(int)
fmt.Println(v1)
}

func TestSyncPoolInMultiGroutine(t *testing.T) {
pool := &sync.Pool{
New: func() interface{} {
fmt.Println("Create a new object.")
return 10
},
}
//只放三个对象
pool.Put(100)
pool.Put(100)
pool.Put(100)
//多协程情况下,sync.Pool的对象情况
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func(id int) {
fmt.Println(pool.Get())
wg.Done()
}(i)
}
wg.Wait()
}

三、进阶与实战

1. 测试

1.1 单元测试

我们一开始就在使用单元测试,类似与Java中的junit。

格式是:

  1. 源码⽂文件以 _test 结尾:xxx_test.go
  2. 测试⽅方法名以 Test 开头:func TestXXX(t *testing.T) {…}

在这里插入图片描述

//必须以Test开头
func TestSquare(t *testing.T) {
inputs := [...]int{1, 2, 3}
expected := [...]int{1, 4, 9}
for i := 0; i < len(inputs); i++ {
ret := square(inputs[i])
if ret != expected[i] {
t.Errorf("input is %d, the expected is %d, the actual %d",
inputs[i], expected[i], ret)
}
}
}
  • 内置单元的测试框架

• Fail, Error: 该测试失败,该测试继续,其他测试继续执行
• FailNow, Fatal: 该测试失败,该测试中止,其他测试继续执行

//t.Error是错误后继续执行
func TestErrorInCode(t *testing.T) {
fmt.Println("Start")
t.Error("Error")
fmt.Println("End")
}

//t.Fail是失败,不能继续执行
func TestFailInCode(t *testing.T) {
fmt.Println("Start")
t.Fatal("Error")
fmt.Println("End")
}
  • 使用断言来进行
func TestSquareWithAssert(t *testing.T) {
inputs := [...]int{1, 2, 3}
expected := [...]int{1, 4, 9}
for i := 0; i < len(inputs); i++ {
ret := square(inputs[i])
assert.Equal(t, expected[i], ret)
}
}

1.2. Benchmark性能测试

Benchmark是一种性能测试方法,格式是前后添加b.ResetTimer() 和 b.StopTimer(),即可测试中间的代码性能

func BenchmarkConcatStringByBytesBuffer(b *testing.B) {
elems := []string{"1", "2", "3", "4", "5"}
b.ResetTimer()
for i := 0; i < b.N; i++ {
var buf bytes.Buffer

for _, elem := range elems {
buf.WriteString(elem)

}
}
b.StopTimer()
}
go test -bench=. -benchmem //或者是benchmark方法名称
//加-benchmem 看内存分配大小等等

2. 反射编程

2.1 常见api

reflect.TypeOf 返回为类型 (reflect.Type),再用kind获取类型
• 通过 kind 的来判断类型
reflect.ValueOf 返回为值 (reflect.Value)
• 可以从 reflect.Value 获得类型 reflect.ValueOf(f).Type()

func CheckType(v interface{}) {
t := reflect.TypeOf(v)
switch t.Kind() {
case reflect.Float32, reflect.Float64:
fmt.Println("Float")
case reflect.Int, reflect.Int32, reflect.Int64:
fmt.Println("Integer")
default:
fmt.Println("Unknown", t)
}
}
func reflectTest01(b interface{}) {
//通过反射获取的传入的变量的 type , kind, 值
//1. 先获取到 reflect.Type
rTyp := reflect.TypeOf(b)
fmt.Println("rType=", rTyp)

//2. 获取到 reflect.Value
rVal := reflect.ValueOf(b)

n2 := 2 + rVal.Int()
fmt.Println("n2=", n2)

fmt.Printf("rVal=%v rVal type=%T\n", rVal, rVal)
//输出 rVal=100 rVal type=reflect.Value

//下面我们将 rVal 转成 interface{}
iV := rVal.Interface()
//将 interface{} 通过断言转成需要的类型
num2 := iV.(int)
fmt.Println("num2=", num2)
//演示反射[对结构体的反射]
//下面我们将 rVal 转成 interface{}
iV := rVal.Interface()
fmt.Printf("iv=%v iv type=%T \n", iV, iV)
//将 interface{} 通过断言转成需要的类型
//这里,我们就简单使用了一带检测的类型断言.
//同学们可以使用 swtich 的断言形式来做的更加的灵活
stu, ok := iV.(Student)
if ok {
fmt.Printf("stu.Name=%v\n", stu.Name)
}
}

2.2 通过反射调用方法

按名字访问结构的成员
reflect.ValueOf(*e).FieldByName("Name")
按名字访问结构的⽅方法
reflect.ValueOf(e).MethodByName("UpdateAge").Call([]reflect.Value{reflect.ValueOf(1)})

func TestInvokeByName(t *testing.T) {
e := &Employee{"1", "Mike", 30}

//反射调用方法
reflect.ValueOf(e).MethodByName("UpdateAge").
Call([]reflect.Value{reflect.ValueOf(1)})
t.Log("Updated Age:", e)
}

注意:这二者的不同,前者是返回两个参数一个数据,一个bool值,后者是返回name属性的值。

reflect.TypeOf(*e).FieldByName("Name")
reflect.ValueOf(*e).FieldByName("Name")
//reflect_test.go:48: {Name string firstname:"杨" 16 [1] false} true
//reflect_test.go:49: Mike

2.3 反射解析Struct Tag

type Employee struct {
EmployeeID string
Name string `firstname:"杨"`
Age int
}
if nameField, ok := reflect.TypeOf(*e).FieldByName("Name"); !ok {
t.Error("Failed to get 'Name' field.")
} else {
//get到某一字段的属性名-值,获取到值
t.Log(nameField.Tag.Get("firstname"))
}

3. “不安全”编程Unsafe

Unsafe类很少用,顾名思义不安全的类,一般用于外部c程序交互使用

注意:go里面原子类型操作必须使用Unsafe类。

atomic.StorePointer(&shareBufPtr, unsafe.Pointer(&data))

atomic.LoadPointer(&shareBufPtr)

//合理的类型转换,别名的转换
func TestConvert(t *testing.T) {
a := []int{1, 2, 3, 4}
b := *(*[]MyInt)(unsafe.Pointer(&a))
t.Log(b)
}

//原子类型操作
func TestAtomic(t *testing.T) {
var shareBufPtr unsafe.Pointer
//原子读写操作的定义,写每次都开辟一块内存
writeDataFn := func() {
data := []int{}
for i := 0; i < 100; i++ {
data = append(data, i)
}
atomic.StorePointer(&shareBufPtr, unsafe.Pointer(&data))
}
readDataFn := func() {
data := atomic.LoadPointer(&shareBufPtr)
//打印首地址和数据
fmt.Println(data, *(*[]int)(data))
}

//进行10次多线程的读写操作
var wg sync.WaitGroup
writeDataFn()
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
for i := 0; i < 10; i++ {
writeDataFn()
time.Sleep(time.Microsecond * 100)
}
wg.Done()
}()
wg.Add(1)
go func() {
for i := 0; i < 10; i++ {
readDataFn()
time.Sleep(time.Microsecond * 100)
}
wg.Done()
}()
}
wg.Wait()
}

4. 常见任务

4.1 JSON解析

  • 内置的json解析

主要原理是通过反射基于struct的Tag来识别json,所以效率低下

var jsonStr = `{
"basic_info":{
"name":"Mike",
"age":30
},
"job_info":{
"skills":["Java","Go","C"]
}
} `

func TestEmbeddedJson(t *testing.T) {
e := new(Employee)
//内置的json解析工具,将json串映射到一个对象(结构)中
//内部采用反射机制,效率低下
err := json.Unmarshal([]byte(jsonStr), e)
if err != nil {
t.Error(err)
}
fmt.Println(*e)

//把对象反解析为json数据
if v, err := json.Marshal(e); err == nil {
fmt.Println(string(v))
} else {
t.Error(err)
}
}
  • 使用easyjson进行json解析
import "encoding/json"
func TestEasyJson(t *testing.T) {
//EasyJsonc采用代码生成而非是反射
e := Employee{}
e.UnmarshalJSON([]byte(jsonStr))
fmt.Println(e)
if v, err := e.MarshalJSON(); err != nil {
t.Error(err)
} else {
fmt.Println(string(v))
}
}

使用benchmark进行性能比较:easyjson明显效率更高

BenchmarkEmbeddedJson-8           286228              3732 ns/op
BenchmarkEasyJson-8 1000000 1029 ns/op

4.2 Http服务和Restful服务

  • net包下原生的http
func main() {
//设置url对于的响应返回handler
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello World!")
})
http.HandleFunc("/time/", func(w http.ResponseWriter, r *http.Request) {
t := time.Now()
timeStr := fmt.Sprintf("{\"time\": \"%s\"}", t)
w.Write([]byte(timeStr))
})
//监听端口
http.ListenAndServe(":8080", nil)
}
  • 使用 github.com/julienschmidt/httprouter

特点可以使用REST风格的API,使用get、post、delete方法

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)
//带参数的get请求
router.GET("/hello/:name", Hello)
router.GET("/hello/:name", Hello)
//Post请求router.POST()
log.Fatal(http.ListenAndServe(":8080", router))
}
  • 获取到服务端的json数据
//实体类
type Employee struct {
ID string `json:"id"`
Name string `json:"name"`
Age int `json:"age"`
}
//http的处理器,通过名字获取到
//ps httprouter.Params 获取Get的参数
func GetEmployeeByName(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
qName := ps.ByName("name")
var (
ok bool
info *Employee
infoJson []byte
err error
)
if info, ok = employeeDB[qName]; !ok {
w.Write([]byte("{\"error\":\"Not Found\"}"))
return
}
//转成对象
if infoJson, err = json.Marshal(info); err != nil {
w.Write([]byte(fmt.Sprintf("{\"error\":,\"%s\"}", err)))
return
}
w.Write(infoJson)
}

在这里插入图片描述

5. 高效的性能代码

5.1 锁对性能的影响

总结:

• 减少锁的影响范围
• 减少发⽣生锁冲突的概率
• 读多写少情况下sync.Map,甚至比ConcurrentMap性能好
• 读写频繁ConcurrentMap
• 避免锁的使⽤用

  • 测试只读状况下,“裸奔”和读写锁性能分析
//“裸奔”
func lockFreeAccess() {
var wg sync.WaitGroup
wg.Add(NUM_OF_READER)
for i := 0; i < NUM_OF_READER; i++ {
go func() {
for j := 0; j < READ_TIMES; j++ {
_, err := cache["a"]
if !err {
fmt.Println("Nothing")
}
}
wg.Done()
}()
}
wg.Wait()
}
//读时加读锁
func lockAccess() {
var wg sync.WaitGroup
wg.Add(NUM_OF_READER)
m := new(sync.RWMutex)
for i := 0; i < NUM_OF_READER; i++ {
go func() {
for j := 0; j < READ_TIMES; j++ {

m.RLock()
_, err := cache["a"]
if !err {
fmt.Println("Nothing")
}
m.RUnlock()
}
wg.Done()
}()
}
wg.Wait()
}

benchmark测试结果:加锁比不加锁多两个数量级

pkg: go1/go_learning/ch48/lock
BenchmarkLockFree-8 206 6134092 ns/op
BenchmarkLock-8 8 149600300 ns/op

5.2 加读/写锁、sync.Map、ConcurrentMap的性能比较

(1)读写情况下增加 读/写锁

type RWLockMap struct {
m map[interface{}]interface{}
lock sync.RWMutex
}

func (m *RWLockMap) Get(key interface{}) (interface{}, bool) {
m.lock.RLock()
v, ok := m.m[key]
m.lock.RUnlock()
return v, ok
}

func (m *RWLockMap) Set(key interface{}, value interface{}) {
m.lock.Lock()
m.m[key] = value
m.lock.Unlock()
}

func (m *RWLockMap) Del(key interface{}) {
m.lock.Lock()
delete(m.m, key)
m.lock.Unlock()
}

(2)读写情况下使用sync.Map

  • 重点:适合读多写少,且 Key 相对稳定的环境,不经常修改
  • 它原理是用空间换时间来减少锁冲突,采用指针的方式间接实现值的映射,所以存储空间会较 built-in map ⼤

底层维护一个只读的空间,同样大小的写空间用读写锁

当读miss时会去dirty中找但是多读了一次还加了读写锁,所以性能不好,当miss的次数到一定量时,dirty区域就会更新到Read区域,底层是原子的CAS操作。

在这里插入图片描述

type SyncMapBenchmarkAdapter struct {
m sync.Map
}

func (m *SyncMapBenchmarkAdapter) Set(key interface{}, val interface{}) {
m.m.Store(key, val)
}

func (m *SyncMapBenchmarkAdapter) Get(key interface{}) (interface{}, bool) {
return m.m.Load(key)
}

func (m *SyncMapBenchmarkAdapter) Del(key interface{}) {
m.m.Delete(key)
}

(3)读写频繁建议使用ConcurrentMap

ConcurrentMap与Java一样,使用分段锁,比起使用读写锁锁住整个Map,它减少了锁的粒度

在这里插入图片描述

type ConcurrentMapBenchmarkAdapter struct {
cm *concurrent_map.ConcurrentMap
}

func (m *ConcurrentMapBenchmarkAdapter) Set(key interface{}, value interface{}) {
m.cm.Set(concurrent_map.StrKey(key.(string)), value)
}

func (m *ConcurrentMapBenchmarkAdapter) Get(key interface{}) (interface{}, bool) {
return m.cm.Get(concurrent_map.StrKey(key.(string)))
}

func (m *ConcurrentMapBenchmarkAdapter) Del(key interface{}) {
m.cm.Del(concurrent_map.StrKey(key.(string)))
}
  • benchmark测试结果
BenchmarkSyncmap/map_with_RWLock-8         	     578	   1952523 ns/op
BenchmarkSyncmap/sync.map-8 686 1468380 ns/op
BenchmarkSyncmap/concurrent_map-8 1377 885345 ns/op

6. GC友好的代码

6.1 复杂对象传引用

go语言类似与c语言时值传递,每次传递都会拷贝对象,所以对于一些复杂对象(数组、结构体)我们一般采用传引用的方式

func withReference(arr *[NumOfElems]Content) int

6.2 避免内存分配和复制

对于切片,我们有时不指定大小,就会频繁的分配内存和复制,导致GC

• 初始化至合适的大小
自动扩容是有代价的(切片)
• 复用内存

//自增长
func TestAutoGrow(t *testing.T) {
for i := 0; i < times; i++ {
s := []int{}
for j := 0; j < numOfElems; j++ {
s = append(s, j)
}
}
}
//初始化大小
func TestProperInit(t *testing.T) {
for i := 0; i < times; i++ {
s := make([]int, 0, 100000)
for j := 0; j < numOfElems; j++ {
s = append(s, j)
}
}
}

func TestOverSizeInit(t *testing.T) {
for i := 0; i < times; i++ {
s := make([]int, 0, 800000)
for j := 0; j < numOfElems; j++ {
s = append(s, j)
}
}
}

benchmark测试性能结果:很明显初始化容量大小后,复用内存更多,避免了扩容分配内存,执行次数更少

BenchmarkAutoGrow-8       	    2064	    623091 ns/op
BenchmarkProperInit-8 7076 178709 ns/op
BenchmarkOverSizeInit-8 1737 651929 ns/op

6.3 高效的字符串拼接

与Java一样,go里面string也是不可变类型,所以+循环拼接效率很低,建议采用strings.Builder

String底层还是byte数组,所以可以使用bytes.Buffer一段连续的存储空间,可以往里面写,最后转成string类型。

func BenchmarkStringAdd(b *testing.B) {
b.ResetTimer()
for idx := 0; idx < b.N; idx++ {
var s string
for i := 0; i < numbers; i++ {
s += strconv.Itoa(i)
}

}
b.StopTimer()
}
func BenchmarkBytesBuf(b *testing.B) {
b.ResetTimer()
for idx := 0; idx < b.N; idx++ {
var buf bytes.Buffer
for i := 0; i < numbers; i++ {
buf.WriteString(strconv.Itoa(i))
}
_ = buf.String()
}
b.StopTimer()
}
func BenchmarkStringBuilder(b *testing.B) {
b.ResetTimer()
for idx := 0; idx < b.N; idx++ {
var builder strings.Builder
for i := 0; i < numbers; i++ {
builder.WriteString(strconv.Itoa(i))

}
_ = builder.String()
}
b.StopTimer()
}

benchmark性能测试结果:

BenchmarkStringAdd-8       	  150411	      7418 ns/op
BenchmarkBytesBuf-8 750408 1648 ns/op
BenchmarkStringBuilder-8 1085298 1189 ns/op

7. 网络Socket编程

golang里面的TCP编程还是类似于Java,使用Socket编程

  • 客户端client
func main() {
conn, err := net.Dial("tcp", "192.168.20.253:8888")
if err != nil {
fmt.Println("client dial err=", err)
return
}
//功能一:客户端可以发送单行数据,然后就退出
reader := bufio.NewReader(os.Stdin) //os.Stdin 代表标准输入[终端]

for {

//从终端读取一行用户输入,并准备发送给服务器
line, err := reader.ReadString('\n')
if err != nil {
fmt.Println("readString err=", err)
}
//如果用户输入的是 exit就退出
line = strings.Trim(line, " \r\n")
if line == "exit" {
fmt.Println("客户端退出..")
break
}

//再将line 发送给 服务器
_, err = conn.Write([]byte(line + "\n"))
if err != nil {
fmt.Println("conn.Write err=", err)
}
}


}
  • 服务端server
//监听到连接后,执行的事件
func process(conn net.Conn) {
//这里我们循环的接收客户端发送的数据
defer conn.Close() //关闭conn

for {
//创建一个新的切片
buf := make([]byte, 1024)
//conn.Read(buf)
//1. 等待客户端通过conn发送信息
//2. 如果客户端没有wrtie[发送],那么协程就阻塞在这里
//fmt.Printf("服务器在等待客户端%s 发送信息\n", conn.RemoteAddr().String())
n , err := conn.Read(buf) //从conn读取
if err != nil {

fmt.Printf("客户端退出 err=%v", err)
return //!!!
}
//3. 显示客户端发送的内容到服务器的终端
fmt.Print(string(buf[:n]))
}

}

func main() {

fmt.Println("服务器开始监听....")
//net.Listen("tcp", "0.0.0.0:8888")
//1. tcp 表示使用网络协议是tcp
//2. 0.0.0.0:8888 表示在本地监听 8888端口
listen, err := net.Listen("tcp", "0.0.0.0:8888")
if err != nil {
fmt.Println("listen err=", err)
return
}
defer listen.Close() //延时关闭listen

//循环等待客户端来链接我
for {
//等待客户端链接
fmt.Println("等待客户端来链接....")
conn, err := listen.Accept()
if err != nil {
fmt.Println("Accept() err=", err)

} else {
fmt.Printf("Accept() suc con=%v 客户端ip=%v\n", conn, conn.RemoteAddr().String())
}
//这里准备其一个协程,为客户端服务
go process(conn)
}

//fmt.Printf("listen suc=%v\n", listen)
}

8. Go操作Mysql数据库

Go 语言中的database/sql包定义了对数据库的一系列操作。database/sql/driver包定义了应被数据库驱动实现的接口,这些接口会被sql包使用。

但是 Go 语言没有提供任何官方的数据库驱动,所以我们需要导入第三方的数据库驱动。不过我们连接数据库之后对数据库操作的大部分代码都使用 sql 包。

8.1 获取数据库连接

1) 创建一个 db.go 文件,导入 database/sql 包以及第三方驱动包

package models

import (
"database/sql"
_ "github.com/go-sql-driver/mysql"
)

2) 定义两个全局变量,sql.DB后续增删改查中通用一个数据库操作句柄

var(
Db *sql.DB
err error
)

3) 创建 init 函数,在函数体中调用 sql 包的 Open 函数获取连接

func init() {
//Open函数打开一个指定的数据库,第二个是数据源
//参数 dataSourceName 的格式:
//数据库用户名:数据库密码@[tcp(localhost:3306)]/数据库名
Db, err = sql.Open("mysql", "root:root@tcp(localhost:3306)/test")
if err != nil {
panic(err.Error())
}
}

返回的 DB 可以安全的被多个 go 程同时使用,并会维护自身的闲置连接池。这样一来,Open 函数只需调用一次。很少需要关闭 DB。

8.2 增删改查操作

  • 向某个表中插入数据
//User 结构体
type User struct {
ID int
Username string
Password string
Email string
}

//与jdbc操作一致
func (user *User) AddUser() error {
//1.写sql语句
sqlStr := "insert into users(username,password,email) values(?,?,?)"
//2.预编译
inStmt, err := utils.Db.Prepare(sqlStr)
if err != nil {
fmt.Println("预编译出现异常:", err)
return err
}
//3.执行,写入到数据库中
_, err2 := inStmt.Exec("xxx", "123456", "xxx@qq.com")
if err2 != nil {
fmt.Println("执行出现异常:", err2)
return err
}
return nil
}
//也可以调用 DB 的 Exec 方法添加用户
func (user *User) AddUser2() error {
//1.写sql语句
sqlStr := "insert into users(username,password,email) values(?,?,?)"
//2.执行
_, err := utils.Db.Exec(sqlStr, "admin2", "666666", "xxx@qq.com")
if err != nil {
fmt.Println("执行出现异常:", err)
return err
}
return nil
}

9. Go操作Redis

9.1 基本命令使用

需要导入第三方包"github.com/garyburd/redigo/redis",就可以实现Redis的基本命令

  1. 先打开redis服务端

在这里插入图片描述

  1. 再启动main程序
func main() {
//通过go 向redis 写入数据和读取数据
//1. 链接到redis
conn, err := redis.Dial("tcp", "127.0.0.1:6379")
if err != nil {
fmt.Println("redis.Dial err=", err)
return
}
defer conn.Close() //关闭..

//2. 通过go 向redis写入数据 Hash [key-val]
_, err = conn.Do("HSet", "user01", "name", "john")
if err != nil {
fmt.Println("hset err=", err)
return
}

_, err = conn.Do("HSet", "user01", "age", 18)
if err != nil {
fmt.Println("hset err=", err)
return
}

//3. 通过go 向redis读取数据

r1, err := redis.String(conn.Do("HGet","user01", "name"))
if err != nil {
fmt.Println("hget err=", err)
return
}

r2, err := redis.Int(conn.Do("HGet","user01", "age"))
if err != nil {
fmt.Println("hget err=", err)
return
}

//因为返回 r是 interface{}
//因为 name 对应的值是string ,因此我们需要转换
//nameString := r.(string)

fmt.Printf("操作ok r1=%v r2=%v \n", r1, r2)
}

9.2 使用Redis连接池

还是使用那个第三方包,内部有redis连接池实现。

//定义一个全局的pool
var pool *redis.Pool

//当启动程序时,就初始化连接池
func init() {

pool = &redis.Pool{
MaxIdle: 8, //最大空闲链接数
MaxActive: 0, // 表示和数据库的最大链接数, 0 表示没有限制
IdleTimeout: 100, // 最大空闲时间
Dial: func() (redis.Conn, error) { // 初始化链接的代码, 链接哪个ip的redis
return redis.Dial("tcp", "localhost:6379")
},
}

}

func main() {
//先从pool 取出一个链接
conn := pool.Get()
defer conn.Close()

_, err := conn.Do("Set", "name", "汤姆猫~~")
if err != nil {
fmt.Println("conn.Do err=", err)
return
}

//取出
r, err := redis.String(conn.Do("Get", "name"))
if err != nil {
fmt.Println("conn.Do err=", err)
return
}

fmt.Println("r=", r)

//如果我们要从pool 取出链接,一定保证链接池是没有关闭
//pool.Close()
conn2 := pool.Get()

_, err = conn2.Do("Set", "name2", "汤姆猫~~2")
if err != nil {
fmt.Println("conn.Do err~~~~=", err)
return
}

//取出
r2, err := redis.String(conn2.Do("Get", "name2"))
if err != nil {
fmt.Println("conn.Do err=", err)
return
}

fmt.Println("r=", r2)

//fmt.Println("conn2=", conn2)
}