go实战总结

426 阅读14分钟

每天go一点点,只记录学习中, 自己用到和迷惑的东西, 记录精华部分, 不求大而全.

忙成狗, 把最近经常用的更新一波.

Hello world程序

image-20200601073912088
package main // 入口必须是package main

import "fmt"

func main() {  // 入口必须是main方法
	fmt.Println("hello world")
}

运行go程序

在终端输入命令:

go run helloworld.go //直接运行

go build helloworld.go  // 进行编译,得到二进制文件,很好的便携性
./helloworld

应用程序入口

  1. 必须是main包: package main
  2. 必须是main函数:func main()
  3. 文件名不一定是main.go
  4. 目录名不一定是main(无需和package保持一致)

退出返回值

Go的main函数不支持任何返回值

通过os.Exit来返回状态

func main() {  
	fmt.Println("hello world")
  os.Exit(0) // 返回状态
}

获取命令行参数

Go的main不支持传入参数

通过os.Args获取命令行参数

测试程序

文件需要以_test.go结尾

测试函数要以Test开头

go数据类型

基本数据类型

image-20200603105312599

不允许类型转化

image-20200603105448898
package typetest

import "testing"

type myInt int64 // 定义int64的别名

func TestImplicit(t *testing.T) {
	var a int = 1
	var b int64
	// b = a //  cannot use a (type int) as type int64 in assignment
	b = int64(a) // 支持显示类型转换
	t.Log(b)

	var c myInt
	// c = b // cannot use b (type int64) as type myInt in assignment
	c = myInt(b) // 支持显示类型转换
	t.Log(c)
}

指针类型/string类型

image-20200603110634147
func TestPoint(t *testing.T) {
	a := 1
	aPtr := &a
	// aPtr = aPtr + 1           // 不支持指针运算 mismatched types *int and int
	t.Log(a, aPtr)            // 1 0xc0000a60d8
	t.Logf("%T, %T", a, aPtr) // int, *int
}
func TestString(t *testing.T) {
	var s string         // string位数值类型,不是引用类型,所以不能用nil去判断
	t.Log("*" + s + "*") // **
	t.Log(len(s))        // 0
	/*  判断字符串为空
	if s=="" {
	  ....
	}
	*/
}

运算符

Go语言没有++a --a

== 比较数组

image-20200603111834116

==其他语言==中,数组名往往是引用类型,我们比较数据名的时候,往往比较的是引用是否相同,而不是值是否相同。

==go==中,如果两数组满足一定条件,可以进行数组值的比较

&^ 按位置零

右边为1: 左边bit位置零

右边为0: 左边bit位不变

1 &^ 1 == 0
0 &^ 1 == 0
1 &^ 0 == 1
0 &^ 0 == 0

条件与循环

循环:for

image-20200603133102023 image-20200603133143454

条件 if

image-20200603133352499

if的条件可以分为两部分: 变量定义、条件(应用于函数多返回值)

image-20200603133602462

条件 switch

image-20200603133808681 image-20200603133842650
func TestCondition(t *testing.T) {
	n := 0
	for n < 5 {   /* while */
		t.Log(n)
		n++
	}
}

func TestCondition2(t *testing.T) {
	for i := 0; i < 5; i++ {
		switch i {
		case 1, 3:
			t.Log("Odd")
		case 2, 4:
			t.Log("Even")
		default:
			t.Log("Other")
		}
	}
}

数组和切片

数组的声明

image-20200603142006413
func TestArrayInit(t *testing.T) {
  var a [3]int // 初始化为0
  
  b := [...]int{1,2,3} // 自动判断元素个数
}

func TestArrayTravs(t *testing.T) {
  /* 传统方法 */
  for i:=0;i<len(a);i++ {
    t.Log(a[i])
  }
  
  /* go range方式 */
  for idx, e range a { //获取下标idx和值e
    t.Log(idx, e)
  }
  
  /* go range方式 */
  for _, e range a { // 不获取下标, 用 _ 占位
    t.Log(e)
  }
}

数组切片

image-20200603143004959

切片

声明方式:

image-20200603144612338
func TestSlict(t *testing.T) {
	a := []int{1, 2, 3, 4} // 声明并初始化
	t.Log(len(a), cap(a))  // 打印切片的元素个数, 容量大小

	b := make([]int, 3, 5)
	t.Log(len(b), cap(b)) // 3 5
	b = append(b, 1)      // 向切片b添加一个元素1
	t.Log(b[0])
}

切片的共享存储结构

image-20200603145952879
months := []string{"", "January", ..., "December"}  //  months是切片,含有13个元素, len==13, capacity==13

Q2 := months[4:7]  // len==3, capacity==9 (从下标4开始,一直到切片months的最后,下标为12)
summer := mouths[6:9] // len==3, capacity==7 

// Q2和summer共享了内存 months[6] ,图中绿色部分
summer[0] = "Unknow" // 绿色部分改为了Unknow
t.Log(Q2) // Q2共享绿色部分,也改变了

数组 vs 切片

数组长度固定

数组可比较

Map

map声明

image-20200603151510972
  1. 最常用的是前两种声明方法。
  2. 使用make是为了指定capacity,避免map动态的增长,提升性能。
    1. make只指定capacity,但不初始化len(因为指定len的部分,会初始化为0,但是map是不能初始化为0的)
  3. 可以访问map的len, len(m), 但是==不能访问==map的capacity, cap(m)

map的元素访问

访问map不存在的key,如果value类型是int,则默认是0. 不是nil, ==不能通过nil来判断key是否存在==

为了区分key是否存在,利用go的==多返回值== v, ok := m[key]

func TestMapKV(t *testing.T) {
	m := map[int]int{}     // 声明一个空的map
	if v, ok := m[1]; ok { // 访问map一个元素的时候, 会返回两个值, v: value,  ok: bool 是否存在
		t.Logf("key 1's value is :%d", v)
	} else {
		t.Log("key 1 not exists!")
	}
}

map的遍历

image-20200603153221664

map与工厂模式

image-20200603153442540 image-20200603154311187

实现set

image-20200603154545487
func TestSet(t *testing.T) {
	set1 := map[int]bool{} // 声明一个map,key是int,value是bool
	elem := 1
	if exists := checkexist(set1, elem); !exists { // 判断key是否存在
		set1[elem] = true
	} else {
		t.Logf("key %d already exists!", elem)
	}

	delete(set1, 1) // 删除key值
}

// 判断key是否存在
func checkexist(set map[int]bool, elem int) bool {
	if set[elem] == true {
		return true
	} else {
		return false
	}
}

// ----------------------------------------------------------------

func TestSetNew(t *testing.T) {
	set := map[int]bool{}
	// set[1] = true
	if _, ok := set[1]; ok {
		fmt.Println("key exists!")
	} else {
		fmt.Println("key not exists!")
	}
}

string

与其他语言的差异

image-20200603155924040
  1. 不能通过nil判断, 要通过 s == ""判断
  2. string==只可读==,不能赋值s[0]='c' // 出错 ; 返回的字节数,不是字符数
  3. 可以存放任意数据,不一定是可见字符
image-20200603160128084

unicode vs utf8

image-20200603160516372

Unicode是字符集,有哪些字符

utf8是unicode在内存中怎么存的(16进制)

image-20200603161037461

rune 能够获取字符串的unicode编码

image-20200603161235537

字符串遍历

image-20200603161459128

当遍历字符串的时候,==得到的value是rune(unicode编码)==,不是utf8

t.Logf("%[1]c, %[1]d", c) : [1]表示都用第一个参数c

字符串常用函数

  1. 字符串分割与连接

    image-20200603162059262
  2. 字符串和其他类型的转换

    image-20200603162330998
    • 整型转字符串,可以直接转
    • 字符串转整型,有两个返回值,所以要用if判断一下

函数

与其他语言的区别

image-20200603162624792

函数装饰器(函数式编程)

// 可以将timeSpend理解为函数装饰器
func timeSpend(innerFun func(op int) int) func(op int) int {
	// 入参是要传入给innerFun的参数
	return func(n int) int {
		startTime := time.Now()
		fmt.Println("startTime is : ", startTime)

		// 执行传入的程序
		ret := innerFun(n)

		// 打印耗时
		fmt.Println("the funciton cost :", time.Since(startTime).Seconds())

		// 返回innerFun的返回值
		return ret
	}
}

func testCostFun(n int) int {
	time.Sleep(time.Second * 2)
	return n
}

func TestTimeSpendFunc(t *testing.T) {
	// 类似装饰器,添加点功能,然后把新的函数返回(一般会保证入参和返回参数和被装饰的函数一致)
	retFunc := timeSpend(testCostFun)
	retFunc(10)
}
image-20200603171630627

可变参数

image-20200603172131104

可变长参数会被==转化成数组==,我们可以对其进行遍历

延迟执行defer

image-20200603172503088

defer的函数,会在函数的返回值返回之前执行。

和C++的finally类似

用于==释放资源、锁==等。

go的面向对象编程

go不支持继承。

接口采用ducktype的方式

封装(数据、行为)

数据的封装:结构体

image-20200603173047790

结构体初始化

image-20200603173142108

行为的封装

行为的定义

image-20200603173526709
// 定义一个myStruct的String方法, 入参是(), 返回值是string的方法
func (e myStruct) String() string {
	return fmt.Sprintf("name: %s, age is %d", e.name, e.age)
}

/* 这两个方法定义一个就好
func (e *myStruct) String() string {
	return fmt.Sprintf("name: %s, age is %d", e.name, e.age)
}
*/

func TestStruct(t *testing.T) {
	e := myStruct{18, "panghu"} // e为myStruct对象
  e1 := &myStruct(18, "panghu2") // ePtr 为myStruct对象的指针
  
	// 无论是对象,还是对象指针,调用的方式都是 '.' 成员运算符
  t.Log(e.String()) 
  t.Log(e1.String()) 
}

go的接口

利用ducktype实现接口

image-20200603191857595

image-20200603193312572

go接口与其他接口的区别

image-20200603193438739
  1. 非入侵性:接口的实现不依赖于接口(不需要显示的用implement)
  2. client对接口有依赖,所以可以将接口和client打包一个package,接口的实现可以单独打包

接口变量

image-20200603194233329

type自定义类型

image-20200603194315566
package type_test

import (
	"fmt"
	"testing"
)

type innerFunc func() string // 自定义函数类型 innerFunc 表示函数类型: 入参为空,返回值是string
// 这样就可以用innerFunc 来代替 func() string函数类型了

// 入参:inner是func() string类型, 返回值是 func() string类型
func sayHelloFirst(inner innerFunc) innerFunc {
	return func() string {
		fmt.Println("hello !")
		ret := inner()
		return ret
	}
}

func WriteGOGOGO() string {
	return "gogogo"
}

func TestType(t *testing.T) {
	ret := sayHelloFirst(WriteGOGOGO)
  t.Log(ret()) // 注意ret是一个函数,调用要加ret()
}

扩展(组合)与复用(继承)

go不支持继承, 只能使用组合。

通过==匿名嵌套==,可以使用父类的方法。

  • 但==实际上还是组合==,不是继承,因为无法实现多态(不支持父类引用指向子类对象)。

  • go不支持LSP原则(父类能用的地方,子类都能用),不支持父类引用指向子类对象(==不支持隐式类型转换==)

image-20200603233454021

接口实现多态

接口变量来接收实现对象的指针

image-20200603234007424

需要注意一点,==接口变量只能接收实现类对象的指针==,不能接受实现类的对象

image-20200603235807946

空接口与断言

image-20200604061658566

空接口类似C++里面的 void*, 可以==接收任何类型==(注意不是指针)

但是==使用空接口的时候,必须将其转换为某种类型==

  • go里面不是使用强制类型转换,而是使用断言 p.(sometype)
  • 断言返回两个值 v, ok,只有第二个值是true,才可以使用第一个转换后的值

接口的最佳实践

image-20200604064020333

错误处理

go的错误机制与其他语言的区别

image-20200604072855816 image-20200604082624331 image-20200612152801578 image-20200612152910647

panic 和 recover

panic and os.Exit

image-20200604101511338

func panic(v interface{})

  • panic的形参是一个空接口,可以接收任何类型,我们通常传入一个error类型

panic() vs os.Exit()

image-20200604101554914

func Exit(code int)

func TestPanicRecoverOsExit(t *testing.T) {
	
	// 比较panic和Os.exit

  // fmt.Println("call panic:")
	// ERROR1 := errors.New("this is panic")
	// panic(ERROR1)  // 打印堆栈信息

  fmt.Println("call os.Exit():")
	os.Exit(-1) // exit status 255
}

recover: 捕获panic

image-20200604104747828 对比其他语言的异常捕获 image-20200604104837020 recover panic捕获
  • defer中调用
    • recover需要在defer中进行调用,如果不在defer中调用,则不会阻止panicking sequence。
    • recover会返回传给panic的 err value
  • 不在defer中调用recover
    • recover不会阻止panickiing sequence
    • recover() 返回 nil
// 在defer中调用recover

func TestRecoverAndPanic(t *testing.T) {
  // defer捕获panic错误, 因为在defer中,所以会阻止panicking sequence
	defer func() {
		if err := recover(); err != nil {
			fmt.Printf("(err value is : %s(%T)", err, err) // this is err from paninc(*errors.errorString)
		}
	}()

  // panic
	ERROR1 := errors.New("this is err from paninc")
	panic(ERROR1)
}
// 在defer外部调用recover

func TestRecoverAndPanic(t *testing.T) {
	fmt.Println("here========")
  // 如果recover不在defer中调用, 不会阻止panicking sequence
  // recover()返回nil,不会返回err信息
	if err := recover(); err != nil {
		fmt.Printf("(err value is : %s(%T)", err, err) 
	}
	fmt.Println("here============")

	ERROR1 := errors.New("this is err from paninc")
	panic(ERROR1)
}

/* 输出 */

here========
here============
--- FAIL: TestRecoverAndPanic (0.00s)
panic: this is err from paninc [recovered]
	panic: this is err from paninc

goroutine 6 [running]:
testing.tRunner.func1.1(0x1119280, 0xc000048480)
	/usr/local/go/src/testing/testing.go:940 +0x2f5
testing.tRunner.func1(0xc000108120)
	/usr/local/go/src/testing/testing.go:943 +0x3f9
panic(0x1119280, 0xc000048480)

构建可复用模块:package

package的约定

image-20200604123156703
  1. 一个目录下的所有文件,package名字必须一致。

设置GOPATH

vi ~/.bash_profile

# 添加自己的项目路径
export GOPATH="/Users/panghu/go:/Users/panghu/Documents/go_project"  # 会自动添加src目录,所以package只需要从src下的目录开始写就ok

source ~/.zshrc

重启Atom,查看GOPATH路径:

image-20200604132941976

构建一个package

image-20200604133333981

init方法

image-20200605083707330
package test_init

// main被执行前,这两个init方法都会被执行
func init() {
    fmt.Println("init1")
}

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

使用远程的packaage

image-20200605084552730
go get -u https://github.com/easierway/concurrent_map.git
import cm "github.com/easierway/concurrent_map"  //起一个别名

依赖管理vendor

image-20200605085138387

这样就可以将当前project/package所依赖的特殊版本的package放到vendor目录下

常用的依赖管理工具

这些工具都利用了vendor路径

image-20200605090110997
brew install glide

# 进入项目
glide init

# 安装
glid install

协程机制

线程threadvs协程groutine

image-20200605090314889

java线程和KSE(系统线程/内核对象)对应关系是1:1

协程和KSE对应关系是多对多

package groutine_test

import (
	"fmt"
	"testing"
)

// 利用协程的方式,执行函数 func(i int) {fmt.Println(i)}
func TestGroutine(t *testing.T) {
	for i := 1; i < 10; i++ { // 这里启动了9个协程
		go func(i int) {   // func前加go
			fmt.Println(i)
    }(i) // 方法体后加()  因为go都是值传递,所以传入的参数会被各个协程func复制,不存在竞争关系。
	}
}

共享内存并发机制(锁、等待组)

sync.Mutex

sync.WaitGroup

image-20200612164241440
package sharemem_test

import (
	"fmt"
	"sync"
	"testing"
	"time"
)

// 线程不安全的协程
func TestCount(t *testing.T) {
	count := 0
	for i := 0; i < 5000; i++ {
		go func(i int) {
			count++
		}(i)
	}

	// 防止TestCount所在的协程执行的比我们自己用go启动的协程快,
	//所以让TestCount等待1秒,让我们自己启动的协程都执行完毕
	time.Sleep(1 * time.Second)
	fmt.Printf("count is : %d\n", count) // 4476, 因为没有加锁,所以不是我们的预期值5000
}

// 线程安全的协程
func TestCountThreadSafe(t *testing.T) {
	var mut sync.Mutex
	count := 0
	for i := 0; i < 5000; i++ {
		go func() {
			defer func() {
				mut.Unlock()
			}() // 在defer里面释放锁,防止忘记了

			mut.Lock()
			count++
		}()
	}

	// 防止TestCountThreadSafe所在的协程执行的比我们自己用go启动的协程快,
	//所以让TestCountThreadSafe等待1秒,让我们自己启动的协程都执行完毕
	time.Sleep(1 * time.Second)
	fmt.Printf("count is : %d\n", count) // 4476, 因为没有加锁,所以不是我们的预期值5000
}

// 线程安全,并且等待自己起的线程都执行完再结束的函数
func TestCountThreadSafeWaitGroup(t *testing.T) {
	var mut sync.Mutex
	var wg sync.WaitGroup

	count := 0
	// 启5000个协程
	for i := 0; i < 5000; i++ {
		// 将协程加入到waitgroup中
		wg.Add(1)
		go func() { // 启动一个协程
			defer func() {
				mut.Unlock()
				wg.Done() // 协程执行完,把waitgroup数量-1
			}()
			mut.Lock()
			count++
		}()
	}

	wg.Wait() // 等待waitgroup里面的所有协程结束
	fmt.Printf("count is %d\n", count)
}

CSP并发机制

CSP vs Actor

image-20200612170849019

除此之外,go会主动的去处理channel传来的消息,和Actor的被动处理不同。

go channel的两种类型

image-20200612171349036

代码实战

image-20200612191619784
package async_test

import (
	"fmt"
	"testing"
	"time"
)

// 异步服务
// (目标:调用这个异步服务RemoteService后,无需阻塞去等待它的返回值,而是在需要的时候再去获取 ==》 利用channel实现)
// 将RemoteServcie的返回值,放到channel中,等待接收方去主动获取
func RemoteService() string {

	fmt.Println("remote service started!")
	time.Sleep(time.Millisecond * 100)
	fmt.Println("remote service done")

	return "Result of remote servcie"
}

// InvokeAsyncService的工作有两个
// 1. 构造一个channel(连接生产者和消费者)
//      1.1 channel的生产者:协程(协程去调用RemoteService,并把返回的结果写channel)
//      1.2 channel的消费者:调用方TestAsyncService (InvokeAsyncService把channel返回给调用方)
// 2. 启了一个协程,该协程调用RemoteService,并将RemoteService的结果写入到channel中
func InvokeAsyncService() chan string {
	fmt.Println("InvokeAsyncService start...")
	// 构造一个阻塞型的channel(只能传递string类型的消息)
	// 这个channel用于RemoteServcie往里面写数据
    // 阻塞型channel
	retChn := make(chan string)
	// 非阻塞型channel
	// retChn := make(chan string, 2) // 可以放两个string类型消息

	// 启一个协程,调用RemoteService,并将RemoteService的结果,写入上面创建的chennel中
	go func() {
		fmt.Println("call RemoteServcie...")
		// 获取RemoteService的结果
		ret := RemoteService()
		// 将结果放到 channel中,如果接收方不在,会被阻塞
		fmt.Println("send ret data to channel....")
		retChn <- ret
	}()

	fmt.Println("InvokeAsyncService Ended!")
	return retChn
}

func DoSomethingElse() {
	fmt.Println("do something else...")
	time.Sleep(time.Millisecond * 1000)
	fmt.Println("do somthing else DONE")
}

func TestAsyncService(t *testing.T) {
	// 获取channel,channel的写入是异步服务的结果(由InvokeAsyncService启的协程向channel中写入)
	chRet := InvokeAsyncService()

	// 调完异步服务后,做自己的事情,不用等待异步服务结束
	DoSomethingElse()

	// 自己的事情做完了,去channel中获取异步服务RemoteService的结果数据
	// 如果channel中没有数据:阻塞状态,等待数据到来
	fmt.Printf("channel message received: %s \n", <-chRet)
}

多路选择和超时机制

image-20200612193622998

func RemoteService() string {

	time.Sleep(time.Millisecond * 100)

	return "Result of remote servcie"
}

func InvokeAsyncService() chan string {
	retChn := make(chan string)

	go func() {
		ret := RemoteService()
		retChn <- ret
	}()

	return retChn
}

func TestAsyncService(t *testing.T) {
	chRet := InvokeAsyncService()

	select {
	case ret := <-chRet:
		fmt.Printf("async service's result is: %s\n", ret)
	case <-time.After(time.Millisecond * 200):  // 超过200毫秒,还没有结果,就超时
		t.Error("time out!")
	}
}

addressable

go addressable 详解

有几个点需要解释下:

  • 常数为什么不可以寻址?: 如果可以寻址的话,我们可以通过指针修改常数的值,破坏了常数的定义。
  • map的元素为什么不可以寻址?:两个原因,如果对象不存在,则返回零值,零值是不可变对象,所以不能寻址,如果对象存在,因为Go中map实现中元素的地址是变化的,这意味着寻址的结果是无意义的。
  • 为什么slice不管是否可寻址,它的元素读是可以寻址的?:因为slice底层实现了一个数组,它是可以寻址的。
  • 为什么字符串中的字符/字节又不能寻址呢:因为字符串是不可变的。

空接口切片

原文: InterfaceSlice

我们都知道, interface{}可以接收任意类型, 所以经常会有人写出下面的代码

var dataSlice []int = foo()
var interfaceSlice []interface{} = dataSlice

然后会报错:

cannot use dataSlice (type []int) as type []interface { } in assignment

你会有这样的疑问: 为什么我可以给interface{}赋任何值, 却不能给[]interface{}赋任何切片呢?

为什么会报错?

为什么这个式子不能成立[]interface{} := []myType?

主要有两个原因:

[]interface{} 并不是一个空接口interface{}类型, 而是一个切片slice类型! 只不过这个切片的元素都是空接口interface{}而已.

这么说或许还有点模糊, 我们来针对每个interface{}来看一下, 其实, 每个interface{}在接收一个变量后, 会有两部分, 一部分是该变量的类型, 一部分是该变量的值或者指针.

所以对于一个长度为N的[]interface{}来说, 它的数据长度是N*2. 而[]myType虽然长度也是N, 但是它的每个元素都是myType, 不需要再记录类型, 所以[]myType的数据部分的长度为N.

所以, 由于[]interface{}[]myType的数据类型都是不同的, 自然也就无法快速赋值.

该怎么做呢?

make实例化一个长度为len([]myType)的空接口切片.

然后对每个元素进行赋值.

var dataSlice []int = foo()
var interfaceSlice []interface{} = make([]interface{}, len(dataSlice))
for i, d := range dataSlice {
	interfaceSlice[i] = d
}

反射的应用

今天在工作中, 遇到别人开发一个接口, 返回一个**interface{}, 我只知道返回值是一个结构体struct**.

现在我要从这个interface{}中获取我所需要的字段值. 应该怎么做呢?

或许你会说, 直接**类型选择**不就ok了吗? 类似下面这样

retValue := functionOfThrid()
switch t := retValue.(type) {
	case structType1:
    	...
	case structType2:
    	...
    default:
    	...
}

但是如果你无法枚举所有的struct类型呢?

此时我们就要用到反射了.

整体的思路是:

  1. 获取retValue的type, 判断它的Kind(结构体 or 结构体指针)
  2. 获取retValue的value, 并通过它去获取相应字段的值.
retValue := functionOfThird()
t := reflect.TypeOf(retValue)
v := reflect.ValueOf(retValue)
switch t.Kind() {
    case reflect.Ptr: // 结构体指针类型(已知肯定是结构体)
        fieldValue 		= v.Elem().FieldByName("fieldName").Int() // 获取整型字段1
        fieldValue2 	= v.Elem().FieldByName("fieldName2").String() // 获取字符串字段2
    case reflect.Struct: // 结构体类型 
        fieldValue 		= v.FieldByName("fieldName").Int()
        fieldValue2    	= v.FieldByName("fieldName2").String()
    default:
    	//err处理
}

这里没有涉及反射的基础知识. 有兴趣可以查看下文, 但是不推荐使用反射!!

Go语言基础--反射

Json解析

go的json解析相对于动态语言还是比较麻烦的, 其中最重要的还是要知道如何根据json写出对应的正确的struct

没啥好总结的, 直接贴原文 细说Golang的JSON解析