基础语法

  • 同一文件夹下的文件,包名得一致。

  • := 表示声明+赋值,只能用于局部变量,不能用于全局变量。

  • 局部变量声明不使用会报错,全局变量声明后可以不使用。

  • 指针和引用例子

    1
    2
    3
    4
    5
    6
    7
    8
    var a int = 4
    var ptr *int

    ptr = &a //指针指向a的实际值的地址

    fmt.Printf("a 的值为 %d\n", a);
    fmt.Printf("*ptr 为 %d\n", *ptr);
    //*ptr和a此时等价了

iota

const 关键字出现时将被重置为 0(const 内部的第一行之前)

const 中每新增一行常量声明将使 iota 计数一次(iota 可理解为 const 语句块中的行索引)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func main() {
const (
a = iota //0
b //1
c //2
d = "ha" //独立值,iota += 1
e //"ha" iota += 1
f = 100 //iota +=1
g //100 iota +=1
h = iota //7,恢复计数
i //8
)
fmt.Println(a,b,c,d,e,f,g,h,i)
}

…操作符

  • 第一个用法主要是用于函数有多个不定参数的情况,可以接受多个不确定数量的参数。
  • 第二个用法是slice可以被打散进行传递。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func test1(args ...string) { //可以接受任意个string参数
for _, v:= range args{
fmt.Println(v)
}
}

func main(){
var strss= []string{
"qwr",
"234",
"yui",
"cvbc",
}
test1(strss...) //切片被打散传入
}
//output:
//qwr
//234
//yui
//cvbc

switch

switch 默认只执行一次,成功就跳出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var grade string = "B"
var marks int = 90

//switch接变量
switch marks {
case 90: grade = "A"
case 80: grade = "B"
case 50,60,70 : grade = "C"
default: grade = "D"
}
//switch不接,case接判断表达式
switch {
case grade == "A" :
fmt.Printf("优秀!\n" )
case grade == "B", grade == "C" :
fmt.Printf("良好\n" )
case grade == "D" :
fmt.Printf("及格\n" )
case grade == "F":
fmt.Printf("不及格\n" )
default:
fmt.Printf("差\n" );
}

要继续执行下一个case用 fallthrough ,要跳出用 break

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
switch {
case false:
fmt.Println("1、case 条件语句为 false")
fallthrough
case true:
fmt.Println("2、case 条件语句为 true")
fallthrough
case false:
fmt.Println("3、case 条件语句为 false")
fallthrough
case true:
fmt.Println("4、case 条件语句为 true")
case false:
fmt.Println("5、case 条件语句为 false")
fallthrough
default:
fmt.Println("6、默认 case")
}

//输出结果如下
2case 条件语句为 true
3case 条件语句为 false
4case 条件语句为 true

switch 从第一个判断表达式为 true 的 case 开始执行,如果 case 带有 fallthrough,程序会继续执行下一条 case,且它不会去判断下一个 case 的表达式是否为 true。

for

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
for i := 0; i <= 10; i++ {
sum += i
}

//类似while
for sum <= 10{
sum += sum
}

//无限循环
for {
sum++ // 无限循环下去
}

//For-each range 循环
numbers := [6]int{1, 2, 3, 5}
for i,x:= range numbers {
fmt.Printf("第 %d 位 x 的值 = %d\n", i,x)
}

slice

切片是对数组的抽象。

len() 函数查看当前长度, cap() 函数查看容量。

数组是确定长度的, array := [2]int{1,2} 或者 array := [...]int{1,2} ,

切片可以不确定长度, slice:= []int{1,2}

切片初始化

1
2
3
4
s :=[] int {1,2,3 }
s := arr[:] //用数组初始化切片
s1 := s[startIndex:endIndex] //用切片初始化切片
s :=make([]int,len,cap) //make函数初始化

追加元素

numbers = append(numbers, 0)

切片扩容

1
2
numbers1 := make([]int, len(numbers), (cap(numbers))*2)
copy(numbers1,numbers)

range

关键字

数组和切片中它返回元素的索引和索引对应的值,在集合中返回 key-value 对。

Map

1
2
3
4
5
6
7
8
9
10
//创建集合
//没有make的话,默认是nil,不能插入键值对,即
//var countryCapitalMap map[string]string
countryCapitalMap := make(map[string]string)
//插入键值对
countryCapitalMap [ "France" ] = "巴黎"
//查看元素在集合中是否存在,第二个返回值是布尔类型
capital, ok := countryCapitalMap [ "American" ]
//删除元素
delete(countryCapitalMap, "France")

string长度

string中有中文不能用 len() 函数来获取长度,因为golang中string底层是通过byte数组实现的。中文字符在utf-8编码下占3个字节。

如果有中文,需要用

1
2
b := "hello 你好"
fmt.Println(strings.Count(b, "") - 1)

strings.Count(a,b) 函数记录字符串a中出现了多少次字符串b,""则代表字符串长度

结构体struct

结构体指针参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type Books struct {
title string
}
func main(){
var book1 Books
book1.title = "book1name"
//book1 := Books{title : "book1name",}

print(&book1)
}

func print(book *Books){
fmt.Println(book.title)
}

函数func

模板

1
2
3
func function_name( [parameter list] ) [return_types] {
函数体
}

例子

1
2
3
4
5
6
7
8
func swap(x, y string) (string, string) {
return y, x
}

func main() {
a, b := swap("Google", "Runoob")
fmt.Println(a, b)
}

接口interface

接口也是 Go 语言中的一种类型,它能够出现在变量的定义、函数的入参和返回值中并对它们进行约束

有两种interface,一种是带有一组方法的接口,另一种是不带任何方法的 interface{},Go 语言使用 runtime.iface 表示第一种接口,使用 runtime.eface 表示第二种不包含任何方法的接口 interface{}

interface{} 类型不是任意类型,Go 语言的任意类型都可以转换成 interface{}

  • 接口是方法的集合,方法作用于变量。
  • 一个 struct 实现了某个接口里的所有方法,就叫做这个 struct 实现了该接口。
  • 主意区分方法与函数,方法用 . 调用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* 定义接口 */
type interface_name interface {
method_name1 [return_type]
method_name2 [return_type]
}

/* 定义结构体 */
type struct_name struct {
/* variables */
}

/* 实现接口方法 */
func (struct_name_variable struct_name) method_name1() [return_type] {
/* 方法实现 */
}

一个例子:
项目结构:

1
2
3
4
test
├──study
| └──study.go
└──main.go
  • 结构体的方法实现了接口里的方法,即结构体实现了接口

  • New() 函数的返回参数是接口

  • var _ Study = (*study)(nil) 要求 *study 去实现 Study,若 Study 接口被更改或未全部实现时,在编译时就会报错

  • type study struct 结构体私有,接口公有

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    // study.go
    package study

    import "github.com/pkg/errors"

    var _ Study = (*study)(nil)

    type Study interface {
    Read(msg string) string
    Write(msg string) string
    }

    type study struct {
    Name string
    }

    func (s *study) Read(msg string) string {
    return s.Name + " 读 " + msg
    }

    func (s *study) Write(msg string) string {
    return s.Name + " 写 " + msg
    }

    func New(name string) (Study, error) {
    if name == "" {
    return nil, errors.New("name required")
    }
    return &study{
    Name: name,
    }, nil
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    // main.go

    package main

    import (
    "test/study"
    "fmt"
    )

    func main() {
    name := "Tom"
    s, err := study.New(name) //这里的study是包名,跨包调用函数
    if err != nil {
    fmt.Println(err)
    }

    fmt.Println(s.Read("english"))
    fmt.Println(s.Write("english"))
    }

    // 输出
    Tom 读 english
    Tom 写 english

defer

声明一个延迟函数,该函数会放在一个列表中,在defer语句的外层函数返回之前系统会执行该延迟函数。

defer特点有:

  • 函数返回之前执行
  • 可以放在函数中任意位置
  • 可以同时设置多个defer函数,多个defer函数执行遵循FILO顺序
  • defer函数的传入参数在定义时就已经明确
  • 可以修改函数中的命名返回值
  • 用于文件资源,锁资源、数据库连接等释放和关闭
  • 和recover一起处理panic

详解

其他技巧

1
2
3
4
   a := 123
b := "abc"
c := 1.23
return fmt.Sprintf("rsesult int: %d, string %s, float %.2f", a, b, c)

并发

go关键字

使用 go 关键字,开启轻量级线程goroutine

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import (
"fmt"
"time"
)

func say(s string) {
for i := 0; i < 5; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Println(s)
}
}

func main() {
//两个线程将会启动,输出的顺序是不定的
go say("world")
say("hello")
}

chan关键字

通道可用于两个 goroutine 之间通过传递一个指定类型的值来同步运行和通讯。

1
2
3
4
5
6
7
8
9
10
11
12
13
//创建通道
ch := make(chan int)
//创建带缓冲区的通道,缓冲区未满,不会阻塞发送
ch := make(chan int, 100)

ch<-1

//关闭通道
close(ch)
//遍历通道
for i := range ch {
fmt.Println(i)
}
操作 nil channel closed channel channel
close panic panic 成功
C<- 阻塞 panic 阻塞或成功发送
<-C 阻塞 永远不阻塞(读完数据读0值) 阻塞或成功接收
  • close()函数表示chan不再能有写入,只能读取数据,所有数据都读完后,再读取不会阻塞,会读零值。

  • for ... range 用在读取chan时,必须先确保chan已经close了,否则会因为阻塞而导致死锁,即不断等待新的chan写入而死锁。或者可以改为 for i := 0; i <= 10; i++这种循环 。

  • 只读chan和只写chan

    1
    2
    3
    ch := make(chan int,10)
    var readch <-chan int = ch
    var writech chan<- int = ch

使用技巧

  • 检查chan是否关闭
1
v, ok := <-a  // 检查是否成功关闭(ok = false:已关闭)
  • 等待一个事件,通过 close 一个 channel。
1
2
3
4
5
6
c := make(chan bool)
go func() {
// close 的 channel 会读到一个零值
close(c)
}()
<-c
  • 搭配select做超时控制
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func main() {
finishReq(5 * time.Second)
}

func finishReq(timeout time.Duration) int {
ch := make(chan int, 1)
go func() {
result := fn()
ch <- result // block
}()
select {
case result := <-ch:
fmt.Printf("result is %v", result)
return result
case <-time.After(timeout):
fmt.Println("time out")
return 0
}
}
func fn() int {
time.Sleep(7 * time.Second)
fmt.Println("do sth")
return 1
}
  • 心跳
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func main() {
done := make(chan bool)

defer func() {
close(done)
}()

ticker := time.NewTicker(1 * time.Second)
go func() {
for {
select {
case <-done:
ticker.Stop()
fmt.Println("done")
return
case <-ticker.C:
fmt.Println("tick")
}
}
}()
time.Sleep(5 * time.Second)
}
  • 多个goroutine同步响应
1
2
3
4
5
6
7
8
9
10
11
12
13
func main() {
c := make(chan struct{})
for i := 0; i < 5; i++ {
go do(c)
}
close(c)
time.Sleep(1000 * time.Second)
}
func do(c <-chan struct{}) {
// 会阻塞直到收到 close
<-c
fmt.Println("hello")
}

context包

教程

使用 context可以更好的做并发控制,能更好的管理 goroutine滥用。

创建context

context包主要提供了两种方式创建context:

  • context.Backgroud()
  • context.TODO()

这两个函数其实只是互为别名,没有差别,官方给的定义是:

context.Background 是上下文的默认值,所有其他的上下文都应该从它衍生(Derived)出来。
context.TODO 应该只在不确定应该使用哪种上下文时使用;
所以在大多数情况下,我们都使用 context.Background 作为起始的上下文向下传递。

上面的两种方式是创建根context,不具备任何功能,具体实践还是要依靠context包提供的四个With系列函数来进行派生

WithValue携带数据

一层一层地传递键值对数据。

1
2
3
func WithValue(parent Context, key, val interface{}) Context
//value函数获取key对应的值
func (c *valueCtx) Value(key interface{}) interface{}
  • 不建议使用 context值传递关键参数,关键参数应该显示的声明出来,不应该隐式处理,context中最好是携带签名、trace_id这类值。
  • 在获取键值对时,先从当前 context中查找,没有找到会在从父 context中查找该键对应的值直到在某个父 context中返回 nil 或者查找到对应的值。
  • 因为携带 value也是 keyvalue的形式,为了避免 context因多个包同时使用 context而带来冲突,key建议采用内置类型。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
//例子

const (
KEY = "trace_id"
)

func NewRequestID() string {
return strings.Replace(uuid.New().String(), "-", "", -1)
}

func NewContextWithTraceID() context.Context {
ctx := context.WithValue(context.Background(), KEY,NewRequestID())
return ctx
}

func PrintLog(ctx context.Context, message string) {
fmt.Printf("%s|info|trace_id=%s|%s",time.Now().Format("2006-01-02 15:04:05") , GetContextValue(ctx, KEY), message)
}

func GetContextValue(ctx context.Context,k string) string{
v, ok := ctx.Value(k).(string)
if !ok{
return ""
}
return v
}

func ProcessEnter(ctx context.Context) {
PrintLog(ctx, "Golang梦工厂")
}


func main() {
ProcessEnter(NewContextWithTraceID())
}

//输出结果
//2021-10-31 15:13:25|info|trace_id=7572e295351e478e91b1ba0fc37886c0|Golang梦工厂

超时控制

当一次请求到达我们设置的超时时间,就会及时取消,不在往下执行。

使用函数 withTimeoutwithDeadline . 本质 withTimout内部也是调用的 WithDeadline

会返回一个 cancelFunc方法,通过调用这个方法可以达到提前进行取消,不过在使用的过程还是建议在自动取消后也调用 cancelFunc去停止定时减少不必要的资源浪费。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
//例子:达到超时时间终止接下来的执行
func main() {
HttpHandler()
}

func NewContextWithTimeout() (context.Context,context.CancelFunc) {
return context.WithTimeout(context.Background(), 3 * time.Second)
}

func HttpHandler() {
ctx, cancel := NewContextWithTimeout()
defer cancel()
deal(ctx)
}

func deal(ctx context.Context) {
for i:=0; i< 10; i++ {
time.Sleep(1*time.Second)
select {
case <- ctx.Done():
fmt.Println(ctx.Err())
return
default:
fmt.Printf("deal time is %d\n", i)
}
}
}

//输出结果
/*
deal time is 0
deal time is 1
context deadline exceeded
*/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//例子:没有达到超时时间,手动终止接下来的执行
func main() {
HttpHandler1()
}

func NewContextWithTimeout1() (context.Context,context.CancelFunc) {
return context.WithTimeout(context.Background(), 3 * time.Second)
}

func HttpHandler1() {
ctx, cancel := NewContextWithTimeout1()
defer cancel()
deal1(ctx, cancel)
}

func deal1(ctx context.Context, cancel context.CancelFunc) {
for i:=0; i< 10; i++ {
time.Sleep(1*time.Second)
select {
case <- ctx.Done():
fmt.Println(ctx.Err())
return
default:
fmt.Printf("deal time is %d\n", i)
cancel()
}
}
}

/* 输出结果
deal time is 0
context canceled
*/

WithCancel取消控制

日常业务开发中我们往往为了完成一个复杂的需求会开多个 gouroutine去做一些事情,这就导致我们会在一次请求中开了多个 goroutine却无法控制他们,这时我们就可以使用 withCancel来衍生一个 context传递到不同的 goroutine中,当我想让这些 goroutine停止运行,就可以调用 cancel来进行取消。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//例子
func main() {
ctx,cancel := context.WithCancel(context.Background())
go Speak(ctx)
time.Sleep(10*time.Second)
cancel()
time.Sleep(1*time.Second)
}

func Speak(ctx context.Context) {
for range time.Tick(time.Second){
select {
case <- ctx.Done():
fmt.Println("我要闭嘴了")
return
default:
fmt.Println("balabalabalabala")
}
}
}

/* 输出
balabalabalabala
....省略
balabalabalabala
我要闭嘴了
*/

我们使用 withCancel创建一个基于 Background的ctx,然后启动一个讲话程序,每隔1s说一话,main函数在10s后执行 cancel,那么 speak检测到取消信号就会退出。

四大方法

  • Deadlne方法:当 Context自动取消或者到了取消时间被取消后返回
  • Done方法:当 Context被取消或者到了 deadline返回一个被关闭的 channel
  • Err方法:当 Context被取消或者关闭后,返回 context取消的原因
  • Value方法:获取设置的 key对应的值

sync包

Mutex

1
2
3
4
5
mutex := &sync.Mutex{}

mutex.Lock()
// Update共享变量 (比如切片,结构体指针等)
mutex.Unlock()

RWMutex

1
2
3
4
5
6
7
8
9
mutex := &sync.RWMutex{}

mutex.Lock()
// Update 共享变量
mutex.Unlock()

mutex.RLock()
// Read 共享变量
mutex.RUnlock()

加了读锁(RLock)不会阻塞读,但是会阻塞写。

加了写锁(Lock)会阻塞读写。

锁定/解锁速度:RLock(RWMutex) > Lock(Mutex) > Lock(RWMutex)。因此,只有在频繁读取和不频繁写入的场景里,才应该使用 sync.RWMutex

WaitGroup

使用场景:一个goroutine等待一组goroutine执行完成。

原理:sync.WaitGroup拥有一个内部计数器。当计数器等于0时,则 Wait()方法会立即返回。否则它将阻塞执行 Wait()方法的goroutine,直到计数器等于0时为止。

增加计数器 Add(int) ,计数器减少1 Done()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//一个例子
import "fmt"
import "sync"

func main () {
wg := &sync.WaitGroup{}
wg.Add(8)
for i := 0; i < 8; i++ {

go func() {
// Do something
wg.Done()
}()
}

wg.Wait()
// 继续往下执行...
}

//main goroutine会在八个goroutine都执行wg.Done()将计数器变为0后才能继续执行。

断言与反射

reflect包相关

gRPC

RPC远程过程调用,可以远程调用服务端实现的方法,如同调用本地方法一样,只需传入输入参数和返回参数即可。

QuickStart

一个简单的gRPC客户端和服务端的实现例子

Protocol Buffers

官方教程
中文教程
一种数据序列化的格式协议,类似于json。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
syntax = "proto3";

message OtherMessage {
string oth = 1;
}
message SearchRequest {
//保留字段7,8,9,11
reserved 11, 7 to 9;

string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;

//编译成切片
repeated string results = 4;

//最多允许这一组中的一个字段出现
oneof test_oneof {
string name = 5;
int64 value = 6;
}

//key只能为int或string,且map不能被repeated修饰
map<int64,string> values = 10;

//枚举类型
enum EnumTest{
Monday = 0;
Tuesday = 1;
}
EnumTest enumTest =12;

//message嵌套,如果外部不需要使用OtherMessage,可以像枚举那样在当前message内部定义
OtherMessage otherMessage = 13;
}
  • 建议使用 reserved关键字来废弃不用字段,以免未来要重新启用

server传输模式

四种模式脑图

1
2
3
4
5
6
7
8
9
10
11
//发请求,回响应
rpc GetFeature(Point) returns (Feature) {}

//客户端发请求,服务端返回流式信息
rpc ListFeatures(Rectangle) returns (stream Feature) {}

//客户端发流式信息,服务端接受并返回响应
rpc RecordRoute(stream Point) returns (RouteSummary) {}

//双向发送接受流式信息
rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}

详细参考教程

gRPC GateWay

使得gRPC服务在外部可用(通过http请求)

参考教程

等待补充知识点

  • 结构体里放接口