基础概念

变量类型 #

声明 #

var a int
a := 0

// 可以两个同时赋值
a, b := 0, 0

进制 #

十六进制 0x 开头为十六进制

八进制 0 开头为八进制

数据结构 #

channel #

slice #

map #

struct #

for 循环 #

for range #

for string #

go 中对字符串 for 循环得到的是索引和 int32 类型的数字

for map #

Map 是无序的,也就是说每次遍历 map 的顺序是不一样的。

for k, v := range m {
    fmt.Println(k, v)
}

for channel #

func RunCronTask() {
	ticker := time.NewTicker(time.Minute * 5)
    for range ticker.C {
        fmt.Println("开始定时任务")
    }
}

channel #

带缓冲区和不带缓冲区的区 #

c1 := make(chan int)  // 不带缓冲区
c2 := make(chan int, 100)  // 带缓冲区
  • 非缓冲 channelchannel 发送和接收动作是同时发生的
    • 发送阻塞直到数据被接收,接收阻塞直到读到数据。
    • 例如 ch := make(chan int) ,如果没 goroutine 读取接收者<-ch ,那么发送者ch<- 就会一直阻塞
  • 缓冲 channel 类似一个队列。
    • 当缓冲满时发送者阻塞,当缓冲空时接收者阻塞。

使用 for range #

Go提供了range关键字,将其使用在 channel 上时,会自动等待 channel 的动作一直到 channel 被关闭,如下:

ticker := time.NewTicker(time.Minute * 5)
for range ticker.C {
	doSomeThing()
}

如下例子,ch 初始化以后,取出的值默认是 false。

ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
ch := make(chan bool)
go func() {
  defer close(ch)
  //具体的任务,这里模拟做的任务需要1秒完成
  time.Sleep(time.Second * 1)
}()
select {
  case <- ch:
  fmt.Println("ch bool", <- ch)
  case <- ctx.Done():
  fmt.Println("ctx done")
}
// output:
// ch bool false

channel 带有互斥锁,一个管道同时仅允许被一个协程读写。

switch/case/default #

fallthrough

select/case/default

“面向对象”

进阶语法

goto #

单引号 #

单引号在 Golang 表示一个字符,使用一个特殊类型 rune 表示字符型。rune 为 int32 的别名,它完全等价于 int32,习惯上用它来区别字符值和整数值。rune 表示字符的 Unicode 码值。

传值还是传引用 #

引用类型: 引用类型变量的值为一串地址,变量存储在栈中,变量的数据存储在地址所指向的堆空间中。slice、map、channel、interface、指针 等是引用类型。

值类型: 值类型变量和变量的数据都是存储在栈中,int、float、bool、array、sturct、string等都是值类型。

结论:Go 语言的函数传参都是值传递。

传参给函数时,外部的值类型无法被修改,引用类型(可能)会被修改,需要看是否是 update 还是 append。

func main() {
	m := make(map[string]int)
	modifyMap(m)
	fmt.Println(m)

	s := make([]string, 1)
	modifySlice(s)
	fmt.Println(s)

	appendSlice(s)
	fmt.Println(s)
}

func modifyMap(m map[string]int) {
	m["hello"] = 1
}

func modifySlice(s []string) {
	s[0] = "hello"
}

func appendSlice(s []string) {
	s = append(s, "new")
}

// output:
map[hello:1]
[hello]
[hello]

接口 #

实现接口的类型和初始化返回的类型两个维度共组成了四种情况,然而这四种情况不是都能通过编译器的检查:

结构体实现接口 结构体指针实现接口
结构体初始化变量 通过 不通过
结构体指针初始化变量 通过 通过

四种中只有使用指针实现接口,使用结构体初始化变量无法通过编译,因为方法的参数是 *Cat,编译器不会无中生有创建一个新的指针;即使编译器可以创建新指针,这个指针指向的也不是最初调用该方法的结构体。

select #

go 中的 select 可以让 goroutine 同时等待多个 channel 可读或可写。

空的 select 语句会直接阻塞当前 Goroutine,导致 Goroutine 进入无法被唤醒的永久休眠状态。

  1. 除 default 外,如果只有一个 case 语句评估通过,那么就执行这个case里的语句;
  2. 除 default 外,如果有多个 case 语句评估通过,那么通过伪随机的方式随机选一个;
  3. 如果 default 外的 case 语句都没有通过评估,那么执行 default 里的语句;
  4. 如果没有 default,那么代码块会被阻塞,直到有一个 case 通过评估;否则一直阻塞

itoa #

参考 https://studygolang.com/articles/22498

slice 切片和数组 array #

// 创建数组
var arr := []int{1,2,3}

// 动态数组创建,类似创建数组,但是没有指定固定长度
var al []int     //创建slice
sl := make([]int,10)  //创建有10个元素的slice
sl:=[]int{1,2,3} //创建有初始化元素的slice

区别:

  • 声明数组时,方括号内写明了数组的长度或者直接定义元素,声明slice时候,方括号内为空。
  • 作为函数参数时,数组传递的是数组的副本,而slice传递的是指针。
  • Array 长度不可变,不可扩容,Slice 可以

make 和 new #

make 可以初始化 Slice、Map、Channel,返回的是实例的引用。

new 只分配内存,可以分配任意类型的数据,返回的是指针。

defer #

defer 的语句在 return 之后执行

  1. 多个 defer 的执行顺序为“后进先出”;
  2. 所有函数在执行 RET 返回指令之前,都会先检查是否存在 defer 语句,若存在则先逆序调用 defer 语句进行收尾工作再退出返回;
  3. 匿名返回值是在 return 执行时被声明,有名返回值则是在函数声明的同时被声明,因此在 defer 语句中只能访问有名返回值,而不能直接访问匿名返回值;
  4. return 其实应该包含前后两个步骤:第一步是给返回值赋值(若为有名返回值则直接赋值,若为匿名返回值则先声明再赋值);第二步是调用RET返回指令并传入返回值,而 RET 则会检查 defer 是否存在,若存在就先逆序插播 defer 语句,最后 RET 携带返回值退出函数;

‍‍因此 defer、return、返回值三者的执行顺序应该是:return 最先给返回值赋值;接着 defer 开始执行一些收尾工作;最后 RET 指令携带返回值退出函数。

panic #

panic 能够改变程序的控制流,调用 panic 后会立刻停止执行当前函数的剩余代码,并在当前 Goroutine 中递归执行调用方的 defer

func test() {
	defer fmt.Println("in main")
	defer func() {
		defer fmt.Println("in defer func")
		defer func() {
			panic("panic again and again")
		}()
		panic("panic again")
	}()

	panic("panic once")
}

➜ go run main.go test

in defer func
in main
panic: panic once
	panic: panic again
	panic: panic again and again

defer 是后进先出。panic 需要等 defer 结束后才会向上传递。出现 panic 时候,会先按照 defer 的后入先出的顺序执行,最后才会执行 panic。

recover #

recover 只能在 defer 里会生效

  • panic 只会触发当前 Goroutine 的 defer
  • recover 只有在 defer 中调用才会生效;
  • panic 允许在 defer 中嵌套多次调用;

internal package #

type assertions 断言 #

package main

import "fmt"

func main() {
	var i interface{} = "hello"

	s := i.(string)
	fmt.Println(s)

	s, ok := i.(string)
	fmt.Println(s, ok)

	f, ok := i.(float64)
	fmt.Println(f, ok)

	f = i.(float64) // panic
	fmt.Println(f)
}

特殊注释 #

go:linkname #

//go:linkname 注释标签引导编译器在编译时将当前私有函数链接到指定的目标函数,也可以作用到变量上面

举例:

time.Sleep() 声明在 time 包,但是实现在 runtime 包的 runtime.timeSleep()

>>>>>>>>>> time/sleep.go <<<<<<<<<<<<<<
package time

// Sleep pauses the current goroutine for at least the duration d.
// A negative or zero duration causes Sleep to return immediately.
func Sleep(d Duration)

>>>>>>>> runtime/time.go <<<<<<<<<<<<<

// timeSleep puts the current goroutine to sleep for at least ns nanoseconds.
//go:linkname timeSleep time.Sleep
func timeSleep(ns int64) {
	if ns <= 0 {
		return
	}

	gp := getg()
	t := gp.timer
	if t == nil {
		t = new(timer)
		gp.timer = t
	}
	t.f = goroutineReady
	t.arg = gp
	t.nextwhen = nanotime() + ns
	gopark(resetForSleep, unsafe.Pointer(t), waitReasonSleep, traceEvGoSleep, 1)
}

注意:

这种方式大部分只在 go 源码中会用到,如果要自己写的话,需要引入 unsafe 包,同时因为go build默认加会加上-complete参数,这个参数检查到没有方法体,在同级文件夹中还需要增加一个空的.s文件才能绕过这个限制

go:noescape #

该指令指定下一个有声明但没有主体(意味着实现有可能不是 Go)的函数,不允许编译器对其做逃逸分析。

一般情况下,该指令用于内存分配优化。因为编译器默认会进行逃逸分析,会通过规则判定一个变量是分配到堆上还是栈上。但凡事有意外,一些函数虽然逃逸分析其是存放到堆上。但是对于我们来说,它是特别的。我们就可以使用 go:noescape 指令强制要求编译器将其分配到函数栈上。

named return value #

相对于匿名返回值,这个叫有名返回值。

defer 可以修改有名返回值。

func f() (i int, s string) {
    i = 17
    s = "abc"
    return // same as return i, s
}

https://tour.golang.org/basics/7

https://yourbasic.org/golang/named-return-values-parameters/

数学计算 #

位移计算 #

CGo #

举例

package rand

/*
#include <stdlib.h>
*/
import "C"

func Random() int {
    return int(C.random())
}

func Seed(i int) {
    C.srandom(C.uint(i))
}