Golang — 语言基础

变量声明 #

两种方式 var:=,没有let 关键字。

var a int
a := 0

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

变量类型 #

int #

进制 #

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

八进制 0 开头为八进制

rune #

rune 为 int32 的别名,它完全等价于 int32,习惯上用它来区别字符值和整数值,用 rune 表示字符的 Unicode 码值。

单引号 #

单引号在 Golang 表示一个字符,使用一个特殊类型 rune 表示字符型。

package main

import(
	"fmt"
)

func main(){
	var c rune = '你'
	fmt.Printf("c=%v ct=%T\n", c, c)
}
// c=20320 ct=int32

float64 #

string #

底层结构,自带了 Len 变量,len() 函数取的就是这个值。

type StringHeader struct {
	Data uintptr
	Len  int
}

需要注意的是,len 值的是底层的字节数,并不是看起来的长度,因为在 UTF-8 格式下有的中文会占用 3 个字节。

func main() {
	a := "你好"
	fmt.Println(len(a))
}
// 6

但是在 range 字符串时,轮序的也不是单字节,而是具体的 rune,Go 语言中使用符文(rune)类型来表示和区分字符串中的“字符”,rune 其实是 int32 的别称。

func main() {
	a := "你好"
	for i, v := range a {
		fmt.Println(i, v, a[i], string(v))
	}
}
//0 20320 228 你
//3 22909 229 好

字符串是Go语言中重要的数据结构,其只能被访问而不能被修改和扩容,但是可以通过拼接构造出一个新的字符串。

byte #

字节数组与字符串的相互转换并不是无损的指针引用,而是涉及了复制。因此,在频繁涉及字节数组与字符串相互转换的场景需要考虑转换的成本。

string 和 []byte 互转 #

string 不能直接和 byte 数组转换,但是 string 可以和 byte 的切片转换。注意:string 和 []byte 的类型转换涉及内存拷贝,一些情况下频繁使用会造成性能瓶颈。

参考:Golang 中 []byte 与 string 转换

string 转为[]byte

var str string = "test"
var data []byte = []byte(str)

[]byte 转为 string

var data [10]byte 
byte[0] = 'T'
byte[1] = 'E'
var str string = string(data[:])

数据结构 #

array #

Go 语言的数组有两种不同的创建方式,一种是显式的指定数组大小,另一种是使用 [...]T 声明数组,Go 语言会在编译期间通过源代码推导数组的大小。

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

slice #

相比 array,slice 又称为动态数组,可以方便的扩容,它的底层实现如下:

type SliceHeader struct {
	Data uintptr
	Len  int
	Cap  int
}

声明一个一个 slice:

a := []string{"hello", "world"}
b := make([]string, 1, 2)
c := make([]string, 1)

切片用法 #

slice 切片和数组 array 的区别 #

// 创建数组
var arr = []int{1,2,3}
// 或者
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 可以

map #

map 的实现没有互斥锁,并不支持并发的读写。协程并发的读写可能会报:fatal error:concurrent map read and map write,只支持并发读。

底层结构

type hmap struct {
	count     int //map 中元素的数量
	flags     uint8 //是否处于正在写入的状态
	B         uint8
	noverflow uint16
	hash0     uint32

	buckets    unsafe.Pointer
	oldbuckets unsafe.Pointer
	nevacuate  uintptr

	extra *mapextra
}

type mapextra struct {
	overflow    *[]*bmap
	oldoverflow *[]*bmap
	nextOverflow *bmap
}

type bmap struct {
    topbits  [8]uint8
    keys     [8]keytype
    values   [8]valuetype
    pad      uintptr
    overflow uintptr
}

使用内置函数 delete() 可以删除指定的键,如果键值不存在,也不会报错,相当于空操作。

struct #

空结构体不占用内存,使用场景参考 https://geektutu.com/post/hpg-empty-struct.html

package main

import (
	"fmt"
	"unsafe"
)

func main() {
	fmt.Println(unsafe.Sizeof(struct{}{}))
}
// 0

Go 编译器在内存分配时做的优化项,当通过 unsafe.Sizeof 发现 size 为 0 时,会直接返回变量 zerobase 的引用,该变量是所有 0 字节的基准地址,不占据任何宽度。

// runtime/malloc.go
var zerobase uintptr

func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
    // 此处省略一些代码
    if size == 0 {
        return unsafe.Pointer(&zerobase)
    }
}

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 带有互斥锁,一个管道同时仅允许被一个协程读写。

使用 select #

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

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

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

程序控制 #

for 循环 #

for range #

for range 时,range 的 list 如果要动态 append,不会影响 for 循环的次数。

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("开始定时任务")
    }
}

for 循环里的 goroutine 问题 #

踩坑:在 for 循环生产 goroutine 的时候,goroutine 中的值不一定是顺序的。

func main() {
	var wg sync.WaitGroup
	count := 10
	wg.Add(count)
	for i := 0; i < count; i++ {
		go func() {
			fmt.Println(i)
			wg.Done()
		}()
	}
	wg.Wait()
	fmt.Println("done")
}
// output:
10
3
10
10
10
10
10
10
10
10
done

switch #

package main

import (
	"fmt"
	"time"
)

func main() {

	i := 2
	fmt.Print("Write ", i, " as ")
	switch i {
	case 1:
		fmt.Println("one")
	case 2:
		fmt.Println("two")
	case 3:
		fmt.Println("three")
	}

	t := time.Now()
	switch {
	case t.Hour() < 12:
		fmt.Println("It's before noon")
	default:
		fmt.Println("It's after noon")
	}
}

如果 switch 后没有值,则会按顺序在 case 后面进行 if 校验,直到有一个为 true 为止。

fallthrough #

正常情况下过完一个 case 后,switch 就会退出,但是可以使用 fallthrough 强制执行下一个 case,不过一般不建议这么做。

select #

select 能够让 Goroutine 同时等待多个 Channel 可读或者可写,在多个文件或者 Channel状态改变之前,select 会一直阻塞当前线程或者 Goroutine。

没有任何 case 的 select 语句会被编译器转换为runtime.block()函数,永久阻塞。

goto #

defer #

defer 的语句在 return 之后执行

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

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

函数 #

函数的参数和返回值举例:

func add(a, b int) int {
	return a+b
}

在 Go 中可以为函数指定接收者,就变为了方法,接收者通常是一个结构体或结构体指针。

函数调用时传值还是传引用 #

引用类型: 引用类型变量的值为一串地址,变量存储在栈中,变量的数据存储在地址所指向的堆空间中。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]

运算符 #

列表里的 ... #

关键字 #

itoa #

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

++ 计算 #

与其他语言不一样,go 只有 i++,没有 ++i 。并且 i++ 表达式也不会返回 i 的值,需要单独获取。

内置函数 #

make 和 new #

new 只分配内存,可以分配任意类型的数据,并初始化零值,返回的是指针。

make 可以初始化 Slice、Map、Channel,返回的是实例的引用,即 Array、Map、Channel。在编译过程中,用 make 去初始化不同的类型会调用不同的底层函数:

  1. 初始化 map, 调用 runtime.makemap
  2. 初始化 slice, 调用 runtime.makeslice
  3. 初始化 channel,调用 runtime.makechan

Go 的数据类型可以分为值类型和引用类型两种。值类型的变量和变量的数据都是存储在栈中,Int、Float、Bool、Array、Sturct、String 等都是值类型。引用类型的变量为一串地址,存储在栈中,变量的数据存储在地址所指向的堆空间中 Slice、Map、Channel、Interface、指针、函数等是引用类型。

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,允许在 defer 中嵌套多次调用。

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

接口 interface #

type Duck interface {
	Quack()
}

type Cat struct{}

func (c *Cat) Quack() {
	fmt.Println("meow")
}

func main() {
	c := &Cat{}
	c.Quack()
}

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

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

当我们使用指针实现接口时,只有指针类型的变量才会实现该接口;当我们使用结构体实现接口时,指针类型和结构体类型都会实现该接口1

type Duck interface {
	Quack()
}

func (c *Cat) Quack() {
	fmt.Println("meow")
}

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

特殊注释 #

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 指令强制要求编译器将其分配到函数栈上。

go:embed #

比如当前文件下有个 hello.txt 的文件,文件内容为 hello,world!。通过 go:embed 指令,在编译后下面程序中的 s 变量的值就变为了hello,world!

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/

internal package #

Go语言1.4版本增加了 Internal packages 特征用于控制包的导入,即 internal package 只能被特定的包导入。

内部包的规范约定:导出路径包含 internal 关键字的包,只允许 internal 的父级目录及父级目录的子包导入,其它包无法导入。

数学计算 #

位移计算 #

阅读 go 的一些代码,发现很多开源库的枚举都喜欢这样:

const (
    FlagNone = 1 << iota
    FlagRed
    FlagGreen
    FlagBlue
)

// 结果分别是1,2,4,8

这样可以做防止重复。

CGo #

举例

package rand

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

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

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

  1. 指针和接口 - Go 语言设计与实现 ↩︎

本文共 4745 字,上次修改于 Dec 29, 2023
相关标签: Go