命令记不住

登陆

2021 面试题准备

面试题准备

1.new和make的区别

new的作用是初始化一个指向类型的指针(*T)

new函数是内建函数,函数定义:func new(Type) *Type

使用new函数来分配空间,传递给new函数的是一个类型,不是一个值,返回值是指向这个新分配的零值的指针

make的作用是为slice,map或chan初始化并返回引用(T)

make函数是内建函数,函数定义:func make(Type,size IntegerType) Type

第一个参数是一个类型,第二个参数是长度,返回值是一个类型

make(T,args)函数的目的与new(T)不同,它仅仅用于创建Slice,Map和Channel,并且返回类型是T(不是T*)的一个初始化的(不是零值)的实例。

2.Printf(),Sprintf(),Fprintf()函数的区别用法是什么

都是把格式字符串输出到标准输出,一般是屏幕,可以重定向,Printf()是和标准输出文件stdout关联的,Fprintf则没有这个限制

Sprintf()是把格式字符串输出到指定字符串中,所以参数比printf多一个char*,那就是目标字符串地址

Fprintf()是把格式字符串输出到指定文件设备中,所以参数比printf多一个文件指针FILE*,主要用于文件操作,Fprintf()是格式化输出到一个stream,通常是到文件

3.数据和切片的区别

数组是具有固定长度且拥有零个或者多个相同数据类型元素的序列,数组的长度是数据类型的一部分,所以[3]int和[4]int是两种不同的数组类型

数组需要指定大小,不指定也会根据初始化的自动推算出大小,不可改变。数据是值传递,数据是内置类型,是一组同类型数据的集合,它是值类型,通过从0开始的下表索引访问元素值,在初始化后长度是固定的,无法修改其长度,当作为方法的参数传入时将复制一份数组而不是引用同一指针,数组的长度也是其类型的一部分,通过内置函数len(array)获取其长度。

切片表示一个拥有相同类型元素的可变长度的序列。切片是一种轻量级的数据结构,他有三个属性:指针,长度和容量。 切片不需要指定大小,切片是地址传递,切片可以通过数据来初始化,也可以通过内置函数make()初始化,初始化时len=cap,在追加元素时,如果容量cap不足时将按len的2倍扩容 var slice []type =make([]type,len)

4.go命令解释

go env 用于查看go环境变量

go run 用于编译运行go源文件

go build 用于编译源码文件、代码包、依赖包

go get 用于动态获取远程代码包

go install 用于动态获取远程代码包

go clean 用于清理工作目录,删除编译和安装遗留的目标文件

go version 用于查看go的版本信息

5.go协程

协程和线程都可以实现程序的并发执行;通过channel来进行协程的通信,只需要在函数调用钱添加go关键字即可试下go的协程,创建并发任务,关键字go并非执行并发任务,而是创建了一个并发任务单元

6.for循环

for循环支持continue和break来控制循环,但是它提供了一个更高级的break,可以选择中断哪一个for循环,不支持以逗号为间隔的多个赋值语句,必须使用平行赋值的方式初始化多个变量

7.switch语句

单个case中,可以出现多个结果选项,只有再case中明确添加fallthrough关键字,才会继续执行紧跟的下一个case

8.go语言中没有隐藏的this指针,这句话是什么意思

方法施加 的对象是显式传递,没有被隐藏起来,golang的面对对象表达更直观,对于面向过程只是换了一种语法形式来表达,方法施加的对象不需要非得是指针,也不用非得叫this

9.引用类型包含哪些

数组切片,字典map,通道channel,接口interface

10.指针运算

可以通过 & 取指针的地址

可以通过 * 取指针的数据

11.main函数

man函数不能带参数

main函数不能定义返回值

main函数所在的包必须为main包

main函数可以使用flag包来获取和解析命令行参数

12.同步锁

当一个goroutine获得了Mutex后,其他goroutine就只能等待,除非该goroutine释放这个Mutex

RWMutex在读锁占用的情况下,会阻止写,不会阻止读

RWMutex在写锁占用的情况下,会阻止任何其他goroutine进来,整个锁相当于该goroutine独占

13.channel特性

1.给一个nil channel发送数据,造成永远阻塞

2.从一个nil channel接收数据,造成永远阻塞

3.给一个已经关闭的channel发送数据,引起panic

4.从一个已经关闭的channel接收数据,如果缓冲区中为空,则返回一个零值

5.无缓冲的channel是同步的,而有缓冲的channel是非同步的

14.触发异常的场景

1.空指针解析

2.下标越界

3.除数为0

4.调用panic函数

15.beego框架

是一个轻量级的HTTP框架

可以通过注释路由、正则路由等多种方式完成url路由注入

可以使用bee new工具生成空工程,然后使用bee run命令自动热编译

16.goconvey框架

是一个支持golang的单元测试框架

能够自动监控文件修改并启动测试,并可以将测试结果实时输出到web界面

提供了丰富的断言简化测试用例的编写

17.GoStub的作用

可以对全局变量打桩

可以对函数打桩

可以打动态桩,比如对一个函数打桩后,多次调用该函数会有不同的行为

不可与对类的成员方法打桩

18.select机制

用来处理异步IO问题

最大一条限制就是每个case语句里必须是一个IO操作

在语言级别支持select关键字

19.进程、线程、协程之间的区别

进程是资源的分配和调度的一个独立单元,而线程是CPU调度的基本单元;
同一个进程中可以包括多个线程;
进程结束后它拥有的所有线程都将销毁,而线程的结束不会影响同个进程中的其他线程的结束;
线程共享整个进程的资源(寄存器、堆栈、上下文),一个进程至少包括一个线程;
进程的创建调用fork或者vfork,而线程的创建调用pthread_create;
线程中执行时一般都要进行同步和互斥,因为他们共享同一进程的所有资源;
进程是资源分配的单位
线程是操作系统调度的单位
进程切换需要的资源很最大,效率很低 线程切换需要的资源一般,效率一般 协程切换任务资源很小,效率高 多进程、多线程根据cpu核数不一样可能是并行的 也可能是并发的。协程的本质就是使用当前进程在不同的函数代码中切换执行,可以理解为并行。 协程是一个用户层面的概念,不同协程的模型实现可能是单线程,也可能是多线程。
进程拥有自己独立的堆和栈,既不共享堆,亦不共享栈,进程由操作系统调度,(全局变量保存在堆中,局部变量及函数保存在栈中)
线程拥有自己独立的栈和共享的堆,共享堆,不共享栈,线程亦由操作系统调度(标准线程是这样的)。
协程和线程一样共享堆,不共享栈,协程由程序员在协程的代码里显示调度。
一个应用程序一般对应一个进程,一个进程一般有一个主线程,还有若干个辅助线程,线程之间是平行运行的,在线程里面可以开启协程,让程序在特定的时间内运行。
协程和线程的区别是:协程避免了无意义的调度,由此可以提高性能,但也因此,程序员必须自己承担调度的责任,同时,协程也失去了标准线程使用多CPU的能力。

20.写出代码输出内容
package main

import (
	"fmt"
)

func main() {
	defer_call()
}
func defer_call() {
	defer func() {
		fmt.Println("打印前")
	}()
	defer func() {
		fmt.Println("打印中")
	}()
	defer func() {
		fmt.Println("打印后")
	}()
	panic("触发异常")
}

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

21.说出以下代码的问题
type student struct {
    Name string Age int
}
func pase_student() {
    m := make(map[string] * student)
    stus := [] student {
        {
            Name: "zhou",
            Age: 24
        }, {
            Name: "li",
            Age: 23
        }, {
            Name: "wang",
            Age: 22
        },
    }
    for _, stu := range stus {
        m[stu.Name] = &stu
    }
}

m[stu.Name]=&stu实际上一致指向同一个指针,最终该指针的值为遍历的最后一个struct的值拷贝。就像想修改切片元素的属性:

// 错误写法   
    for _, stu := range stus {
        m[stu.Name] = & stu
    }
    for k, v := range m {
        println(k, "=>", v.Name)
    }
// 正确写法
    for i := 0; i < len(stus); i++ {
        m[stus[i].Name] = & stus[i]
    }
    for k, v := range m {
        println(k, "=>", v.Name)
    }
}
22.下面代码会输出什么,并说明原因
func main() {
    runtime.GOMAXPROCS(1)
    wg := sync.WaitGroup {}
    wg.Add(20)
    for i := 0; i < 10; i++ {
        go func() {
            fmt.Println("A: ", i) 
            wg.Done()
        }()
    }
    for i := 0; i < 10; i++ {
        go func(i int) {
            fmt.Println("B: ", i)
            wg.Done()
        }(i)
    }
    wg.Wait()

go执行的随机性,谁也不知道执行后的打印顺序是什么,只能说是随机数字。但A均为输出10,B从0-9输出(顺序不一定)。第一个go func的i是外部for的一个变量,地址不变化,遍历完后,最终i=10,故go func执行时,i的值始终是10。

第二个go func中的i是函数参数,与外部for中的i完全是两个变量,尾部i将发生值拷贝,go func内部指向值拷贝地址

23.下面代码会输出什么
type People struct{}

func (p *People) ShowA() {
	fmt.Println("showA")
	p.ShowB()
}
func (p *People) ShowB() {
	fmt.Println("showB")
}

type Teacher struct {
	People
}

func (t *Teacher) ShowB() {
	fmt.Println("teacher showB")
}
func main() {
	t := Teacher{}
	t.ShowA()
}

go的组合模式,可以实现OOP的继承,被组合的类型People所包含的方法虽然升级了外部类型Teacher这个组合类型的方法(一定要匿名字段),但是它们的方法ShowA()调用时接收者没有发生变化,此时People类型并不知道自己会被什么类型组合,当然也就无法调用方法使用未知的组合者Teacher类型的功能

showAshowB
24.下面代码触发异常的原因
func main() {
	runtime.GOMAXPROCS(1)
	int_chan := make(chan int, 1)
	string_chan := make(chan string, 1)
	int_chan <- 1
	string_chan <- "hello"
	select {
	case value := <-int_chan:
		fmt.Println(value)
	case value := <-string_chan:
		panic(value)
	}
}

select会随机选择一个可用通用做收发操作,所以代码是有肯触发异常,也有可能不会,单个chan如果无缓冲时,将会阻塞,但结合select可以在多个chan间等待执行,有三点原则

  • select中只要有一个case能return,则立刻执行
  • 如果同一时间有多个case均能return则伪随机方式抽取任意一个执行
  • 如果没有一个case能return则可以执行default块
25.以下代码输出结果
package main

import "fmt"

func calc(index string, a, b int) int {
	ret := a + b
	fmt.Println(index, a, b, ret)
	return ret
}
func main() {
	a := 1
	b := 2
	defer calc("1", a, calc("10", a, b))
	a = 0
	defer calc("2", a, calc("20", a, b))
	b = 1
}

这道题类似第一题需要注意到defer执行顺序喝值传递index:1肯定是最后执行的,但是index:1的第三个参数是一个函数,所以最先被调用calc("10",1,2)==>10,1,2,3 执行index:2时,与之前一样,需要先调用calc("20",0,2)==>20,0,2,2执行到b=1时候开始调用,index:2==>calc("2",0,2)==>2,0,2,2,最后执行index:1==>calc("1",1,3)==>1,2,3,4

10 1 2 3
20 0 2 2
2 0 2 2 
1 1 3 4
26.写出以下输出内容
func main() {
	s := make([]int, 5)
	s = append(s, 1, 2, 3)
	fmt.Println(s)
}

make初始化是由默认值的

27.以下代码问题在哪里
type UserAges struct {
	ages map[string]
	int sync.Mutex
}
func(ua * UserAges) Add(name string, age int) {
	ua.Lock()
	defer ua.Unlock()
	ua.ages[name] = age
}
func(ua * UserAges) Get(name string) int {
	if age, ok := ua.ages[name]; ok {
		return age
	}
	return -1
}

可能会出现fatal error: concurrent map read and map write.

修改为:

func(ua * UserAges) Get(name string) int {
    ua.Lock() 
    defer ua.Unlock() 
    if age, ok := ua.ages[name]; ok {
        return age
    }
    return -1
}
28.下面代码是否可以运行
func (set *threadSafeSet) Iter() <-chan interface{} {
	ch := make(chan interface{})
	go func() {
		set.RLock()
		for elem := range set.s {
			ch <- elem
		}
		close(ch)
		set.RUnlock()
	}()
	return ch
}

考点:chan缓存池

解答:看到这道题,我也在猜想出题者的意图在哪里。 chan?sync.RWMutex?go?chan缓存池?迭代? 所以只能再读一次题目,就从迭代入手看看。既然是迭代就会要求set.s全部可以遍历一次。但是chan是为缓存的,那就代表这写入一次就会阻塞。我们把代码恢复为可以运行的方式,看看效果

package main
import(
	"sync"
	"fmt"
)
//下面的迭代会有什么问题?
type threadSafeSet struct {
	sync.RWMutex
	s []interface {}
}
func(set * threadSafeSet) Iter() <-chan interface {} {
	//ch := make(chan interface{})
	// 解除注释看看!
	ch := make(chan interface {}, len(set.s))
	go func() {
		set.RLock()
		for elem, value := range set.s {
			ch <-elem
			println("Iter:", elem, value)
		}
		close(ch)
		set.RUnlock()
	}()
	return ch
}
func main() {
	th := threadSafeSet {
		s :[]interface {} {
			"1","2"
		},
	}
	v := <-th.Iter()
	fmt.Sprintf("%s%v", "ch", v)
}
29.下面代码输出内容是什么
package main

import "fmt"

type People interface {
	Show()
}
type Student struct {
}

func (stu *Student) Show() {}

func live() People {
	var stu *Student
	return stu
}

func main() {
	if live() == nil {
		fmt.Println("AAAAAAA")
	} else {
		fmt.Println("BBBBBBB")
	}
}

考点:interface内部结构

解答:很经典的题!这个考点是很多人忽略的interface内部结构。 go中的接口分为两种一种是空的接口类似这样:

var in interface{}

另一种如题目:

type People interface {
	Show()
}

他们的底层结构如下:

type eface struct {  
    //空接口   
    _type *_type         //类型信息  
    data  unsafe.Pointer //指向数据的指针(go语言中特殊的指针类型unsafe.Pointer类似于c语言中的void*)
}
type iface struct {  
    //带有方法的接口  
    tab  *itab           //存储type信息还有结构实现方法的集合  
    data unsafe.Pointer  //指向数据的指针(go语言中特殊的指针类型unsafe.Pointer类似于c语言中的void*)}
type _type struct {   
    size       uintptr  //类型大小  
    ptrdata    uintptr  //前缀持有所有指针的内存大小  
    hash       uint32   //数据hash值  
    tflag      tflag    align      uint8    //对齐  
    fieldalign uint8    //嵌入结构体时的对齐  
    kind       uint8    //kind 有些枚举值kind等于0是无效的  
    alg        *typeAlg //函数指针数组,类型实现的所有方法  
    gcdata    *byte    str       nameOff    ptrToThis typeOff
}
type itab struct {  
    inter  *interfacetype  //接口类型   
    _type  *_type          //结构类型  
    link   *itab    bad    int32    inhash int32    fun    [1]uintptr      //可变大小 方法集合
}

可以看出iface比eface 中间多了一层itab结构。 itab 存储_type信息和[]fun方法集,从上面的结构我们就可得出,因为data指向了nil 并不代表interface 是nil,所以返回值并不为空,这里的fun(方法集)定义了接口的接收规则,在编译的过程中需要验证是否实现接口结果:

29.手写2个协程交替打印1-100的奇数偶数
package main

import (
	"fmt"
	"time"
)

var POOL = 100

func groutine1(p chan int) {
	for i := 1; i <= POOL; i++ {
		p <- i
		if i%2 == 1 {
			fmt.Println("groutine-1:", i)
		}
	}
}
func groutine2(p chan int) {
	for i := 1; i <= POOL; i++ {
		<-p
		if i%2 == 0 {
			fmt.Println("groutine-2:", i)
		}
	}
}
func main() {
	msg := make(chan int)
	go groutine1(msg)
	go groutine2(msg)
	time.Sleep(time.Second * 1)
}
30.互斥锁的两种实现

1.用Mutex实现

package main

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

var num int
var mtx sync.Mutex
var wg sync.WaitGroup

func add() {
	mtx.Lock()
	defer mtx.Unlock()
	defer wg.Done()
	num += 1
	fmt.Println("num---", num)
}

func main() {
	for i := 0; i < 100; i++ {
		wg.Add(1)
		go add()
	}
	wg.Wait()
	fmt.Println("NUM:", num)

	time.Sleep(time.Second * 2)
}

2.用chan实现

package main

import (
	"fmt"
	"sync"
)

var num int

func add(h chan int, wg *sync.WaitGroup) {
	defer wg.Done()
	h <- 1
	num += 1
	<-h
}
func main() {
	ch := make(chan int, 1)
	wg := &sync.WaitGroup{}
	for i := 0; i < 100; i++ {
		wg.Add(1)
		go add(ch, wg)
	}
	wg.Wait()
	fmt.Println("num:", num)
}
31.对map进行并发访问时需增加同步机制
// 通过匿名结构体声明了一个变量counter,变量中包含了map和sync.RWMutex
var counter = struct {
	sync.RWMutex
	m map[string]int
}{m: make(map[string]int)}
// 读取数据的时候使用读锁
counter.RLock()
n := counter.m["Tony"]
counter.RUnlock()
// 写数据的使用使用写锁
counter.Lock()
counter.m["Tony"]++
counter.Unlock()
32.语法规范-函数体

在其他大多数语言中, { 的位置你自行决定。Go 比较特别,遵守分号注入规则(automatic semicolon injection):编译器会在每行代码尾部特定分隔符后加 ; 来分隔多条语句,比如会在 ) 后加分号:

// 错误示例 
func main() 
{
 println("hello world") 
} 
// 等效于 
func main(); // 无函数体 
{
 println("hello world") 
}
33.语法规范-import

如果你 import 一个包,但包中的变量、函数、接口和结构体一个都没有用到的话,将编译失败。 可以使用 _ 下划线符号作为别名来忽略导入的包,从而避免编译错误,这只会执行 package 的 init()

// 错误示例 
import ( 
"fmt" // imported and not used: "fmt" 
"log" // imported and not used: "log" 
"time" // imported and not used: "time" 
) 
func main() { 
} 

// 正确示例 
// 可以使用 goimports 工具来注释或移除未使用到的包 
import (
 _ "fmt" 
"log" 
"time" 
) 
func main() {
 _ = log.Println _ = time.Now 
}
34.简短声明的变量只能在函数内部使用
// 错误示例 
myvar := 1 // syntax error: non-declaration statement outside function body 
func main() {
 
} 
// 正确示例 
var myvar = 1 
func main() { 
}
35.使用简短声明来重复声明变量

不能使用简短声明方式单独为一个变量重复声明

// 错误示例 
func main() { 
one := 0 
one := 1 // error: no new variables on left side of := 
} 
// 正确示例 
func main() {
one := 0 
one, two := 1, 2 // two 是新变量,允许 one 的重复声明。比如 error 处理经常用同名变量 
one, two = two, one // 交换两个变量值的简写 }
36.不能使用简短声明来设置字段的值

struct的变量字段不能呢个使用:=来赋值以使用预定义的变量

//错误示例
type info struct {
	result int
}

func work() (int, error) {
	return 3, nil
}
func main() {
	var data info

	data.result, err := work() //: non-name data.result on left side of :=
	fmt.Println("info : %+v\n", data)
}
//正确示例
 
37.不小心覆盖了变量

对从动态语言转过来的开发者来说,简短声明很好用,这可能会让人误会 := 是一个赋值操作符。 如果你在新的代码块中像下边这样误用了 := ,编译不会报错,但是变量不会按你的预期工作:

func main() {
	x := 1
	println(x) // 1
	{
		println(x) // 1
		x := 2
		println(x) // 2 // 新的 x 变量的作用域只在代码块内部
	}
	println(x) // 1
}

这是 Go 开发者常犯的错,而且不易被发现。 可使用 vet 工具来诊断这种变量覆盖,Go 默认不做覆盖检查,添加 -shadow 选项来启用:

> go tool vet -shadow main.go 
main.go:9: declaration of "x" shadows declaration at main.go:5

注意 vet 不会报告全部被覆盖的变量,可以使用 go‑nyet 来做进一步的检测:

> $GOPATH/bin/go-nyet main.go main.go:10:3:Shadowing variable `x`
38.显示类型的变量无法使用nil来初始化

nil是interface、function、pointer、map、slice和channel类型变量的默认初始值,但声明时不指定类型,编译器也无法推断出变量的具体类型。

// 错误示例
func main() {
	var x = nil // error: use of untyped nil
	_ = x
}

// 正确示例
func main() {
	var x interface{} = nil
	_ = x
}
39.直接使用值为Nil的slice、map

允许对值为nil的slice添加元素,但对值为nil的map添加元素会造成运行时panic

// map 错误示例 
func main() { 
var m map[string]int 
m["one"] = 1 // error: panic: assignment to entry in nil map 
// m := make(map[string]int)// map 的正确声明,分配了实际的内存 } 

// slice 正确示例 
func main() { 
var s []int s = append(s, 1) 
}
39.map容量

在创建map类型的变量时可以指定容量,但不能像slice一样使用cap()来检测分配空间的大小:

func main() {
	m := make(map[string]int, 99)
	fmt.Println(m)
	fmt.Println(cap(m)) //./1.go:24:17: invalid argument m (type map[string]int) for cap
}
40.string类型的变量值不能为nil

对那些喜欢用 nil 初始化字符串的人来说,这就是坑:

// 错误示例
func main() {
	var s string = nil // cannot use nil as type string in assignment
	if s == nil {
		// invalid operation: s == nil (mismatched types string and nil
		s = "default"
	}
}

// 正确示例
func main() {
	var s string // 字符串类型的零值是空串 ""
	if s == "" {
		s = "default"
	}
}

41.Array类型的值作为函数参数

在Go中,数组是值。作为参数传进函数时,传递的是数组的原始值拷贝,此时在函数内部是无法更新该数组的

//数组使用值拷贝传参
func main() {
	x := [3]int{1, 2, 3}

	func(arr [3]int) {
		arr[0] = 7
		fmt.Println(arr)
	}(x)
	fmt.Println(x)
}

如果想修改参数数组,直接传递指向这个数组的指针类型

func main() {
	x := [3]int{1, 2, 3}

	func(arr *[3]int) {
		(*arr)[0] = 7
		fmt.Println(*arr)
	}(&x)
	fmt.Println(x)
}

直接使用slice,即使函数内部得到的是slice的值拷贝,但依旧会更新slice的原始数据,底层s是array

func main() {
	x := []int{1, 2, 3}
	func(arr []int) {
		arr[0] = 7
		fmt.Println(x)
	}(x)
	fmt.Println(x)
}
42.range遍历slice和array时混淆了返回值

go在range遍历时会生成两个值,第一个是元素索引,第二个是元素的值

func main() {
	x := []string{"a", "b", "c"}
	for k, v := range x {
		fmt.Println("k= ", k, "v= ", v)
	}
}
43.slice和array其实是一维数据

看起来go支持多维的array和slice,可以创建数组的数组、切片的切片,但其实并不是

对依赖动态计算多维数组值的应用来说,就性能和复杂度而言,用go实现的效果并不理想

可以使用原始的一维数组、“独立“的切片、”共享底层数组“的切片来创建动态的多维数组

1.使用原始的一维数组:要做好索引检查、溢出检测、以及当数组满时再添加值时要重新做内存分配

2.使用独立的切片分两步:

创建外部的slice

  • 对每个内部slice进行内存分配
  • 注意内部的slice相互独立,使得任一内部slice曾缩都不会影响到其他的slice
func main() {
	//使用各自独立的6个slice来创建[2][3]的动态多维数组
	x := 2
	y := 4
	table := make([][]int, x)
	for i := range table {
		table[i] = make([]int, y)
	}
	fmt.Println(table)
}

使用“共享底层数组”的切片

共3步骤

  • 创建一个存放原始数据的容器slice
  • 创建其他的slice
  • 切割原始slice来初始化其他的slice
func main() {
	h, w := 2, 4
	raw := make([]int, h*w)

	for i := range raw {
		raw[i] = i
	}
	//初始化原始slice
	fmt.Println(raw, &raw[4])

	table := make([][]int, h)
	for i := range table {
		// 等间距切割原始 slice ,创建动态多维数组 table
		//0: raw[0*4: 0*4 + 4]
		//1: raw[1*4: 1*4 + 4]
		table[i] = raw[i*w : i*w+w]

	}
	fmt.Println(table, &table[1][0])
}

更多关于多维数组的参考

go‑how‑is‑two‑dimensional‑arrays‑memory‑representation

what‑is‑a‑concise‑way‑to‑create‑a‑2d‑slice‑in‑go

44.访问map中不存在的key

Go会返回元素对应数据类型的零值,比如nil、''、false和0,取值操作总有值返回,故不能通过取出来的值判断key是不是在map中

检查key是否存在可以用map直接访问,检查返回的第二个参数即可

//错误的key检测方式
func main() {
	x := map[string]string{"one": "2", "two": "", "three": "3"}

	if v := x["two"]; v == "" {
		fmt.Println("key  two  is no  entry")
	}
}

// 正确检测
func main() {
	x := map[string]string{"one": "2", "two": "", "three": "3"}

	if _, ok := x["two"]; !ok {
		fmt.Println("key  two  is no  entry")
	}
}

45.string类型的值是常量,不可以更改

尝试使用索引遍历字符串,来更新字符串中的个别字符,是不允许

string类型的值是只读的二进制byte slice,如果真要修改字符串中的字符,将string转换为[]byte修改后,再转为string即可。

// 修改字符串的错误示例
func main() {
	x := "text"
	x[0] = "T" // error: cannot assign to x[0]
	fmt.Println(x)
}

// 修改示例
func main() {
	x := "text"
	xBytes := []byte(x)
	xBytes[0] = 'T'
	// 注意此时的 T 是 rune 类型
	x = string(xBytes)
	fmt.Println(x) // Text
}

注意: 上边的示例并不是更新字符串的正确姿势,因为一个 UTF8 编码的字符可能会占多个字节,比如汉字 就需要 3~4 个字节来存储,此时更新其中的一个字节是错误的。

更新字串的正确姿势:将 string 转为 rune slice(此时 1 个 rune 可能占多个 byte),直接更新 rune 中的字符

func main() {
	x := "text"
	xRunes := []rune(x)
	xRunes[0] = '我'
	x = string(xRunes)
	fmt.Println(x)
}
46.string与byte slice之间的转换

当进行string和byte slice相互转换时,参与转换的是拷贝的原始值。

Go在string与byte slice相互转换时优化了两点,避免了额外的内存分配

  • 在map[sting]中查找key时,使用了对应[]byte,避免作m[string(key)]的内存分配
  • 使用for range 迭代string转换为[]byte的迭代,for i,v := range []byte(str) {...}
47.string与索引操作符

对字符串索引访问返回的不是字符,而是一个byte值

func main() {
	str := "alibaba%1688"

	for k, v := range str {
		fmt.Printf("str[%v] = %v\n", k, string(v))
	}

	fmt.Println(string(str[1]))
	fmt.Println(string(str[9]))
	fmt.Printf("str1 = %T\n", str[1])
	fmt.Printf("str9 = %T\n", str[9])
}

如果需要使用 for range 迭代访问字符串中的字符(unicode code point / rune),标准库中有 "unicode/utf8" 包来做 UTF8 的相关解码编码。另外 utf8string 也有像 func (s *String) At(i int) rune 等很方便的库函数。

48.字符串不都是utf8文本

string 的值不必是 UTF8 文本,可以包含任意的值。只有字符串是文字字面值时才是 UTF8 文本,字串可以通 过转义来包含其他数据。 判断字符串是否是 UTF8 文本,可使用 "unicode/utf8" 包中的 ValidString() 函数:

func main() {
	str1 := "ABC"
	fmt.Println(utf8.ValidString(str1)) // true
	str2 := "A\xfeC"
	fmt.Println(utf8.ValidString(str2)) // false
	str3 := "A\\xfeC"
	fmt.Println(utf8.ValidString(str3)) // true
	// 把转义字符转义成字面值
}
49.字符串长度
func main() {
	char := "♥"
	fmt.Println(len(char))
}

Go 的内建函数 len() 返回的是字符串的 byte 数量,而不是像 Python 中那样是计算 Unicode 字符数。 如果要得到字符串的字符数,可使用 "unicode/utf8" 包中的 RuneCountInString(str string) (n int)

func main() {
	char := "♥"
	fmt.Println(utf8.RuneCountInString(char))
}

注意: RuneCountInString 并不总是返回我们看到的字符数,因为有的字符会占用 2 个 rune:

func main() {
	char := "é"
	fmt.Println(len(char))                    // 3
	fmt.Println(utf8.RuneCountInString(char)) // 2
	fmt.Println("cafe\u0301")                 // café
	// 法文的 cafe,实际上是两个 rune 的组合
}
50.在多行 array、slice、map 语句中缺少 , 号
func main(){
	x :=[]int{
		1,
		2   //这里没加,会报错
	}
	y :=[]int{1,2}
	z :=[]int{1,2,}
	fmt.Println(x)
	fmt.Println(y)
	fmt.Println(z)
}

声明语句中 } 折叠到单行后,尾部的 , 不是必需的。

51.log.Fatal 和 log.Panic 不只是 log

log 标准库提供了不同的日志记录等级,与其他语言的日志库不同,Go 的 log 包在调用 Fatal*() 、 Panic*() 时能做更多日志外的事,如中断程序的执行等:

func main() {
	log.Fatal("Fatal level log: log entry") // 输出信息后,程序终止执行
	log.Println("Nomal level log: log entry")
}
52.对内建数据结构的操作并不是同步的

尽管 Go 本身有大量的特性来支持并发,但并不保证并发的数据安全,用户需自己保证变量等数据以原子操作 更新。

goroutine 和 channel 是进行原子操作的好方法,或使用 "sync" 包中的锁。

53.range迭代string得到的值

range 得到的索引是字符值(Unicode point / rune)第一个字节的位置,与其他编程语言不同,这个索引并 不直接是字符在字符串中的位置。

注意一个字符可能占多个 rune,比如法文单词 café 中的 é。操作特殊字符可使用norm 包。

for range 迭代会尝试将 string 翻译为 UTF8 文本,对任何无效的码点都直接使用 0XFFFD rune(�) UNicode 替代字符来表示。如果 string 中有任何非 UTF8 的数据,应将 string 保存为 byte slice 再进行操 作。

func main() {
	data := "A\xfe\x02\xff\x04"
	for _, v := range data {
		fmt.Printf("%#x ", v)
	} // 0x41 0xfffd 0x2 0xfffd 0x4 // 错误

	for _, v := range []byte(data) {
		fmt.Printf("%#x ", v) // 0x41 0xfe 0x2 0xff 0x4 // 正确
	}
}
54.range迭代map

如果你希望以特定的顺序(如按 key 排序)来迭代 map,要注意每次迭代都可能产生不一样的结果。

Go 的运行时是有意打乱迭代顺序的,所以你得到的迭代结果可能不一致。但也并不总会打乱,得到连续相同 的 5 个迭代结果也是可能的,如:

func main() {
	m := map[string]int{"one": 1, "two": 2, "three": 3, "four": 4}
	for k, v := range m {
		fmt.Println(k, v)
	}
}

如果你去 Go Playground 重复运行上边的代码,输出是不会变的,只有你更新代码它才会重新编译。重新编 译后迭代顺序是被打乱的:

55.switch 中的 fallthrough 语句

switch 语句中的 case 代码块会默认带上 break,但可以使用 fallthrough 来强制执行下一个 case 代码块。

func main() {
	isSpace := func(char byte) bool {

		switch char {
		case ' ': // 空格符会直接 break,返回 false // 和其他语言不一样
		// fallthrough // 返回 true
		case '\t':
			return true
		}
		return false
	}
	fmt.Println(isSpace('\t')) // true
	fmt.Println(isSpace(' '))  // false

}

不过你可以在 case 代码块末尾使用 fallthrough ,强制执行下一个 case 代码块。 也可以改写 case 为多条件判断:

func main() {
	isSpace := func(char byte) bool {
		switch char {
		case ' ', '\t':
			return true
		}
		return false
	}
	fmt.Println(isSpace('\t')) // true
	fmt.Println(isSpace(' '))  // true
}
56.自增和自减运算

很多编程语言都自带前置后置的 ++ 、 -- 运算。但 Go 特立独行,去掉了前置操作,同时 ++ 、 — 只作为运 算符而非表达式。

// 正确示例 
func main() {
	data := []int{1, 2, 3}
	i := 0
	i++
	fmt.Println(data[i]) // 2 
}

// 错误示例
func main() {
	data := []int{1, 2, 3}
	i := 0
	++i
	fmt.Println(data[i]) // 2
}
57.按位取反

很多编程语言使用 ~ 作为一元按位取反(NOT)操作符,Go 重用 ^ XOR 操作符来按位取反:

func main() {
	var d uint8 = 2
	fmt.Printf("%08b\n", d)  // 00000010
	fmt.Printf("%08b\n", ^d) // 11111101
}

// 错误取反操作
func main() {
	fmt.Prinln(~2)
}

同时 ^ 也是按位异或(XOR)操作符。

一个操作符能重用两次,是因为一元的 NOT 操作 NOT 0x02 ,与二元的 XOR 操作 0x22 XOR 0xff 是一致 的。

Go 也有特殊的操作符 AND NOT &^ 操作符,不同位才取1。

func main() {
	var a uint8 = 0x82
	var b uint8 = 0x02
	fmt.Printf("%08b [A]\n", a)
	fmt.Printf("%08b [B]\n", b)
	fmt.Printf("%08b (NOT B)\n", ^b)
	fmt.Printf("%08b ^ %08b = %08b [B XOR 0xff]\n", b, 0xff, b^0xff)
	fmt.Printf("%08b ^ %08b = %08b [A XOR B]\n", a, b, a^b)
	fmt.Printf("%08b & %08b = %08b [A AND B]\n", a, b, a&b)
	fmt.Printf("%08b &^%08b = %08b [A 'AND NOT' B]\n", a, b, a&^b)
	fmt.Printf("%08b&(^%08b)= %08b [A AND (NOT B)]\n", a, b, a&(^b))
}

//运行结果
10000010 [A]
00000010 [B]
11111101 (NOT B)
00000010 ^ 11111111 = 11111101 [B XOR 0xff]
10000010 ^ 00000010 = 10000000 [A XOR B]
10000010 & 00000010 = 00000010 [A AND B]
10000010 &^00000010 = 10000000 [A 'AND NOT' B]
10000010&(^00000010)= 10000000 [A AND (NOT B)]

58.运算符的优先级

除了位清除(bit clear)操作符,Go 也有很多和其他语言一样的位操作符,但优先级另当别论。

优先级列表

Precedence Operator
5 * / % << >> & &^
4 + -
3 == != < <= > >=
2 &&
1 II
59.不导出的 struct 字段无法被 encode

以小写字母开头的字段成员是无法被外部直接访问的,所以 struct 在进行 json、xml、gob 等格式的 encode 操作时,这些私有字段会被忽略,导出时得到零值:

func main() {
	in := MyData{1, "two"}
	fmt.Printf("%#v\n", in) // main.MyData{One:1, two:"two"}
	encoded, _ := json.Marshal(in)
	fmt.Println(string(encoded)) // {"One":1} // 私有字段 two 被忽略了
	var out MyData
	json.Unmarshal(encoded, &out)
	fmt.Printf("%#v\n", out) // main.MyData{One:1, two:""}
}
60.程序退出时还有 goroutine 在执行

程序默认不等所有 goroutine 都执行完才退出,这点需要特别注意:

func main() {
	workerCount := 2
	for i := 0; i < workerCount; i++ {
		go doIt(i)
	}
	time.Sleep(1 * time.Second)
	fmt.Println("all done!")
}
func doIt(workerID int) {
	fmt.Printf("[%v] is running\n", workerID)
	time.Sleep(3 * time.Second)
	fmt.Printf("[%v] is done\n", workerID)
}

常用解决办法:使用 "WaitGroup" 变量,它会让主程序等待所有 goroutine 执行完毕再退出。

如果你的 goroutine 要做消息的循环处理等耗时操作,可以向它们发送一条 kill 消息来关闭它们。或直接 关闭一个它们都等待接收数据的 channel:

//报错死锁
func main() {
	var wg sync.WaitGroup
	done := make(chan struct{})

	workerCount := 2
	for i := 0; i < workerCount; i++ {
		wg.Add(1)
		go doIt(i, done, wg)
	}
	close(done)
	wg.Wait()
	fmt.Println("all done!")
}
func doIt(workerID int, done <-chan struct{}, wg sync.WaitGroup) {
	fmt.Printf("[%v] is running\n", workerID)
	defer wg.Done()
	<-done
	fmt.Printf("[%v] is done\n", workerID)
}

为什么会发生死锁?goroutine 在退出前调用了 wg.Done() ,程序应该正常退出的。

原因是 goroutine 得到的 "WaitGroup" 变量是 var wg WaitGroup 的一份拷贝值,即 doIt() 传参只传值。

所以哪怕在每个 goroutine 中都调用了 wg.Done() , 主程序中的 wg 变量并不会受到影响。

func doIt(workerID int, ch <-chan interface{}, done <-chan struct{}, wg *sync.WaitGroup) {
	fmt.Printf("[%v] is running\n", workerID)
	defer wg.Done()
	for {
		select {
		case m := <-ch:
			fmt.Printf("[%v] m => %v\n", workerID, m)
		case <-done:
			fmt.Printf("[%v] is done\n", workerID)
			return
		}
	}
}
61.向无缓冲的 channel 发送数据,只要 receiver 准备好了就会立刻返回

只有在数据被 receiver 处理时,sender 才会阻塞。因运行环境而异,在 sender 发送完数据后,receiver 的 goroutine 可能没有足够的时间处理下一个数据。如:

func main() {
	ch := make(chan string)

	go func() {
		for m := range ch {
			fmt.Println("Process :", m)
			time.Sleep(time.Second * 1)
		}
	}()
	ch <- "cmd.1"
	ch <- "cmd.2"
	ch <- "cmd.3"
}
63.向已关闭的 channel 发送数据会造成 panic

从已关闭的 channel 接收数据是安全的:

接收状态值 ok 是 false 时表明 channel 中已没有数据可以接收了。类似的,从有缓冲的 channel 中接收数据,缓存的数据获取完再没有数据可取时,状态值也是 false

向已关闭的 channel 中发送数据会造成 panic:

func main() {
	ch := make(chan int)
	for i := 0; i < 3; i++ {
		go func(idx int) {
			ch <- idx
		}(i)
	}
	fmt.Println(<-ch)
close(ch)
	time.Sleep(time.Second *2)
}

执行结果:
/*
0
panic: send on closed channel

goroutine 20 [running]:
main.main.func1(0xc000082060, 0x2)
        /home/administrator/project/20210302/00.go:12 +0x3f
created by main.main
        /home/administrator/project/20210302/00.go:11 +0x6f
exit status 2
*/

针对上边有 bug 的这个例子,可使用一个废弃 channel done 来告诉剩余的 goroutine 无需再向 ch 发送数 据。此时 <- done 的结果是 {} :

func main() {
	ch := make(chan int)
	done := make(chan struct{})
	for i := 0; i < 3; i++ {
		go func(idx int) {
			select {
			case ch <- (idx + 1) * 2:
				fmt.Println(idx, "Send Rsult")
			case <-done:
				fmt.Println(idx, "Exiting")
			}
		}(i)
	}

	fmt.Println("Result: ", <-ch)
	close(done)
	time.Sleep(time.Second * 3)
}
64.使用了值为 nil 的 channel

在一个值为 nil 的 channel 上发送和接收数据将永久阻塞:

runtime 死锁错误:

fatal error: all goroutines are asleep ‑ deadlock! 
goroutine 1 [chan receive (nil chan)]

利用这个死锁的特性,可以用在 select 中动态的打开和关闭 case 语句块:

func main() {
	inCh := make(chan int)
	outCh := make(chan int)

	go func() {
		var in <-chan int = inCh
		var out chan<- int
		var val int

		for {
			select {
			case out <- val:
				println("--------")
				out = nil
				in = inCh
			case val = <-in:
				println("++++++++++")
				out = outCh
				in = nil
			}
		}
	}()

	go func() {
		for r := range outCh {
			fmt.Println("Result: ", r)
		}
	}()

	time.Sleep(0)
	inCh <- 1
	inCh <- 2
	time.Sleep(3 * time.Second)
}

65.若函数 receiver 传参是传值方式,则无法修改参数的原有值

方法 receiver 的参数与一般函数的参数类似:如果声明为值,那方法体得到的是一份参数的值拷贝,此时对 参数的任何修改都不会对原有值产生影响。

除非 receiver 参数是 map 或 slice 类型的变量,并且是以指针方式更新 map 中的字段、slice 中的元素的, 才会更新原有值:

func main() {
	inCh := make(chan int)
	outCh := make(chan int)

	go func() {
		var in <-chan int = inCh
		var out chan<- int
		var val int

		for {
			select {
			case out <- val:
				println("--------")
				out = nil
				in = inCh
			case val = <-in:
				println("++++++++++")
				out = outCh
				in = nil
			}
		}
	}()

	go func() {
		for r := range outCh {
			fmt.Println("Result: ", r)
		}
	}()

	time.Sleep(0)
	inCh <- 1
	inCh <- 2
	time.Sleep(3 * time.Second)
}
66.关闭 HTTP 的响应体

使用 HTTP 标准库发起请求、获取响应时,即使你不从响应中读取任何数据或响应为空,都需要手动关闭响应体。新手很容易忘记手动关闭,或者写在了错误的位置:

func main() {
	args := os.Args
	str := args[1]
	fmt.Println("str : ", str)
	resp, err := http.Get(str)
	defer resp.Body.Close()
	if err != nil {
		fmt.Println("resp: ", err)
		return
	}
	body, err := ioutil.ReadAll(resp.Body)
	checkErr(err)
	fmt.Println("body: ", string(body))
}
func checkErr(err error) {
	if err != nil {
		log.Fatal("logFatal: ", err)
	}

}

上边的代码能正确发起请求,但是一旦请求失败,变量 resp 值为 nil ,造成 panic:

[administrator@administrator-dev 20210302]$ go run   01.go    https://api.ipify.org?format=json
str :  https://api.ipify.org?format=json
body:  {"ip":"120.245.132.17"}
[administrator@administrator-dev 20210302]$ 
[administrator@administrator-dev 20210302]$ go run   01.go    www.aliyun.comstr :  www.aliyun.com
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x40 pc=0x648822]

goroutine 1 [running]:
main.main()
        /home/administrator/project/20210302/01.go:16 +0x122
exit status 2

应该先检查 HTTP 响应错误为 nil ,再调用 resp.Body.Close() 来关闭响应体:

resp, err := http.Get(str)
checkErr(err)

上边的代码能正确发起请求,但是一旦请求失败,变量 resp 值为 nil ,造成 panic:

[administrator@administrator-dev 20210302]$ go run   01.go    www.aliyun.com
str :  www.aliyun.com
2021/03/03 00:22:36 logFatal: Get "www.aliyun.com": unsupported protocol scheme ""
exit status 1

应该先检查 HTTP 响应错误为 nil ,再调用 resp.Body.Close() 来关闭响应体:

// 大多数情况正确的示例
func main() {
	resp, err := http.Get("https://api.ipify.org?format=json")
	checkError(err)

	defer resp.Body.Close() // 绝大多数情况下的正确关闭方式
	body, err := ioutil.ReadAll(resp.Body)
	checkError(err)

	fmt.Println(string(body))
}

输出

Get https://api.ipify.org?format=json: x509: certificate signed by unknown authority

绝大多数请求失败的情况下,resp 的值为 nil 且 err 为 non-nil。但如果你得到的是重定向错误,那它俩的值都是 non-nil,最后依旧可能发生内存泄露。2 个解决办法:

  • 可以直接在处理 HTTP 响应错误的代码块中,直接关闭非 nil 的响应体。
  • 手动调用 defer 来关闭响应体:
// 正确示例
func main() {
	resp, err := http.Get("http://www.baidu.com")

	// 关闭 resp.Body 的正确姿势
	if resp != nil {
		defer resp.Body.Close()
	}

	checkError(err)

	body, err := ioutil.ReadAll(resp.Body)
	checkError(err)

	fmt.Println(string(body))
}

resp.Body.Close() 早先版本的实现是读取响应体的数据之后丢弃,保证了 keep-alive 的 HTTP 连接能重用处理不止一个请求。但 Go 的最新版本将读取并丢弃数据的任务交给了用户,如果你不处理,HTTP 连接可能会直接关闭而非重用,参考在 Go 1.5 版本文档。

如果程序大量重用 HTTP 长连接,你可能要在处理响应的逻辑代码中加入:

_, err = io.Copy(ioutil.Discard, resp.Body) // 手动丢弃读取完毕的数据

如果你需要完整读取响应,上边的代码是需要写的。比如在解码 API 的 JSON 响应数据:

json.NewDecoder(resp.Body).Decode(&data)  

67.关闭 HTTP 连接

一些支持 HTTP1.1 或 HTTP1.0 配置了 connection: keep-alive 选项的服务器会保持一段时间的长连接。但标准库 “net/http” 的连接默认只在服务器主动要求关闭时才断开,所以你的程序可能会消耗完 socket 描述符。解决办法有 2 个,请求结束后:

  • 直接设置请求变量的 Close 字段值为 true,每次请求结束后就会主动关闭连接。
  • 设置 Header 请求头部选项 Connection: close,然后服务器返回的响应头部也会有这个选项,此时 HTTP 标准库会主动断开连接。
// 主动关闭连接
func main() {
	req, err := http.NewRequest("GET", "http://golang.org", nil)
	checkError(err)

	req.Close = true
	//req.Header.Add("Connection", "close")	// 等效的关闭方式

	resp, err := http.DefaultClient.Do(req)
	if resp != nil {
		defer resp.Body.Close()
	}
	checkError(err)

	body, err := ioutil.ReadAll(resp.Body)
	checkError(err)

	fmt.Println(string(body))
}

func checkError(err error) {
	if err != nil {
		log.Fatal("logFatal: ", err)
	}
}

你可以创建一个自定义配置的 HTTP transport 客户端,用来取消 HTTP 全局的复用连接:

func main() {
	tr := http.Transport{DisableKeepAlives: true}
	client := http.Client{Transport: &tr}

	resp, err := client.Get("https://golang.google.cn/")
	if resp != nil {
		defer resp.Body.Close()
	}
	checkError(err)

	fmt.Println(resp.StatusCode) // 200

	body, err := ioutil.ReadAll(resp.Body)
	checkError(err)

	fmt.Println(len(string(body)))
}

根据需求选择使用场景:

  • 若你的程序要向同一服务器发大量请求,使用默认的保持长连接。
  • 若你的程序要连接大量的服务器,且每台服务器只请求一两次,那收到请求后直接关闭连接。或增加最大文件打开数 fs.file-max 的值。
68.将 JSON 中的数字解码为 interface 类型

在 encode/decode JSON 数据时,Go 默认会将数值当做 float64 处理,比如下边的代码会造成 panic:panic: interface conversion: interface {} is float64, not int

func main() {
	var data = []byte(`{"status": 200}`)
	var result map[string]interface{}

	if err := json.Unmarshal(data, &result); err != nil {
		log.Fatalln(err)
	}

	fmt.Printf("%T\n", result["status"]) // float64
	var status = result["status"].(int)  // 类型断言错误
	fmt.Println("Status value: ", status)
}


如果你尝试 decode 的 JSON 字段是整型,你可以:

  • 将 int 值转为 float 统一使用
  • 将 decode 后需要的 float 值转为 int 使用
// 将 decode 的值转为 int 使用
func main() {
	var data = []byte(`{"status": 200}`)
	var result map[string]interface{}

	if err := json.Unmarshal(data, &result); err != nil {
		log.Fatalln(err)
	}

	var status = uint64(result["status"].(float64))
	fmt.Println("Status value: ", status)
}


  • 使用 Decoder 类型来 decode JSON 数据,明确表示字段的值类型
// 指定字段类型
func main() {
    var data = []byte(`{"status": 200}`)
    var result map[string]interface{}
 
    var decoder = json.NewDecoder(bytes.NewReader(data))
    decoder.UseNumber()
 
    if err := decoder.Decode(&result); err != nil {
        log.Fatalln(err)
    }
 
    var status, _ = result["status"].(json.Number).Int64()
    fmt.Println("Status value: ", status)
}
 
 // 你可以使用 string 来存储数值数据,在 decode 时再决定按 int 还是 float 使用
 // 将数据转为 decode 为 string
 func main() {
     var data = []byte({"status": 200})
      var result map[string]interface{}
      var decoder = json.NewDecoder(bytes.NewReader(data))
      decoder.UseNumber()
      if err := decoder.Decode(&result); err != nil {
          log.Fatalln(err)
      }
    var status uint64
      err := json.Unmarshal([]byte(result["status"].(json.Number).String()), &status);
    checkError(err)
       fmt.Println("Status value: ", status)
}


使用 struct 类型将你需要的数据映射为数值型

// struct 中指定字段类型
func main() {
	var data = []byte(`{"status": 200}`)
	var result struct {
		Status uint64 `json:"status"`
	}

	err := json.NewDecoder(bytes.NewReader(data)).Decode(&result)
	checkError(err)
	fmt.Printf("Result: %+v", result)
}


  • 可以使用 struct 将数值类型映射为 json.RawMessage 原生数据类型 适用于如果 JSON 数据不着急 decode 或 JSON 某个字段的值类型不固定等情况:
// 状态名称可能是 int 也可能是 string,指定为 json.RawMessage 类型
func main() {
    records := [][]byte{
        []byte(`{"status":200, "tag":"one"}`),
        []byte(`{"status":"ok", "tag":"two"}`),
    }
 
    for idx, record := range records {
        var result struct {
            StatusCode uint64
            StatusName string
            Status     json.RawMessage `json:"status"`
            Tag        string          `json:"tag"`
        }
 
        err := json.NewDecoder(bytes.NewReader(record)).Decode(&result)
        checkError(err)
 
        var name string
        err = json.Unmarshal(result.Status, &name)
        if err == nil {
            result.StatusName = name
        }
 
        var code uint64
        err = json.Unmarshal(result.Status, &code)
        if err == nil {
            result.StatusCode = code
        }
 
        fmt.Printf("[%v] result => %+v\n", idx, result)
    }


69.struct、array、slice 和 map 的值比较

可以使用相等运算符 == 来比较结构体变量,前提是两个结构体的成员都是可比较的类型:

type data struct {
	num     int
	fp      float32
	complex complex64
	str     string
	char    rune
	yes     bool
	events  <-chan string
	handler interface{}
	ref     *byte
	raw     [10]byte
}

func main() {
	v1 := data{}
	v2 := data{}
	fmt.Println("v1 == v2: ", v1 == v2) // true
}


如果两个结构体中有任意成员是不可比较的,将会造成编译错误。注意数组成员只有在数组元素可比较时候才可比较。invalid operation: v1 == v2 (struct containing [10]func() bool cannot be compared)

type data struct {
	num    int
	checks [10]func() bool   // 无法比较
	doIt   func() bool       // 无法比较
	m      map[string]string // 无法比较
	bytes  []byte            // 无法比较
}

func main() {
	v1 := data{}
	v2 := data{}

	fmt.Println("v1 == v2: ", v1 == v2)
}


Go 提供了一些库函数来比较那些无法使用 == 比较的变量,比如使用 "reflect" 包的 DeepEqual() :

// 比较相等运算符无法比较的元素
func main() {
	v1 := data{}
	v2 := data{}
	fmt.Println("v1 == v2: ", reflect.DeepEqual(v1, v2)) // true

	m1 := map[string]string{"one": "a", "two": "b"}
	m2 := map[string]string{"two": "b", "one": "a"}
	fmt.Println("v1 == v2: ", reflect.DeepEqual(m1, m2)) // true

	s1 := []int{1, 2, 3}
	s2 := []int{1, 2, 3}
	// 注意两个 slice 相等,值和顺序必须一致
	fmt.Println("v1 == v2: ", reflect.DeepEqual(s1, s2)) // true
}


这种比较方式可能比较慢,根据你的程序需求来使用。DeepEqual() 还有其他用法:go

func main() {
	var b1 []byte = nil
	b2 := []byte{}
	fmt.Println("b1 == b2: ", reflect.DeepEqual(b1, b2)) // false
}


注意:

  • DeepEqual() 并不总适合于比较 slice
func main() {
	var str = "one"
	var in interface{} = "one"
	fmt.Println("str == in: ", reflect.DeepEqual(str, in)) // true

	v1 := []string{"one", "two"}
	v2 := []string{"two", "one"}
	fmt.Println("v1 == v2: ", reflect.DeepEqual(v1, v2)) // false

	data := map[string]interface{}{
		"code":  200,
		"value": []string{"one", "two"},
	}
	encoded, _ := json.Marshal(data)
	var decoded map[string]interface{}
	json.Unmarshal(encoded, &decoded)
	fmt.Println("data == decoded: ", reflect.DeepEqual(data, decoded)) // false
}


如果要大小写不敏感来比较 byte 或 string 中的英文文本,可以使用 "bytes" 或 "strings" 包的 ToUpper() 和 ToLower() 函数。比较其他语言的 byte 或 string,应使用 bytes.EqualFold() 和 strings.EqualFold()

如果 byte slice 中含有验证用户身份的数据(密文哈希、token 等),不应再使用 reflect.DeepEqual()、bytes.Equal()、 bytes.Compare()。这三个函数容易对程序造成 timing attacks,此时应使用 "crypto/subtle" 包中的 subtle.ConstantTimeCompare() 等函数

  • reflect.DeepEqual() 认为空 slice 与 nil slice 并不相等,但注意 byte.Equal() 会认为二者相等:
func main() {
	var b1 []byte = nil
	b2 := []byte{}

	// b1 与 b2 长度相等、有相同的字节序
	// nil 与 slice 在字节上是相同的
	fmt.Println("b1 == b2: ", bytes.Equal(b1, b2)) // true
}


70.从 panic 中恢复

在一个 defer 延迟执行的函数中调用 recover() ,它便能捕捉 / 中断 panic

// 错误的 recover 调用示例
func main() {
	recover()         // 什么都不会捕捉
	panic("not good") // 发生 panic,主程序退出
	recover()         // 不会被执行
	println("ok")
}

// 正确的 recover 调用示例
func main() {
	defer func() {
		fmt.Println("recovered: ", recover())
	}()
	panic("not good")
}


从上边可以看出,recover() 仅在 defer 执行的函数中调用才会生效。

// 错误的调用示例
func main() {
	defer func() {
		doRecover()
	}()
	panic("not good")
}

func doRecover() {
	fmt.Println("recobered: ", recover())
}


71.在 range 迭代 slice、array、map 时通过更新引用来更新元素

在 range 迭代中,得到的值其实是元素的一份值拷贝,更新拷贝并不会更改原来的元素,即是拷贝的地址并不是原有元素的地址:

func main() {
	data := []int{1, 2, 3}
	for _, v := range data {
		v *= 10 // data 中原有元素是不会被修改的
	}
	fmt.Println("data: ", data) // data:  [1 2 3]
}


如果要修改原有元素的值,应该使用索引直接访问:

func main() {
	data := []int{1, 2, 3}
	for i, v := range data {
		data[i] = v * 10
	}
	fmt.Println("data: ", data) // data:  [10 20 30]
}


如果你的集合保存的是指向值的指针,需稍作修改。依旧需要使用索引访问元素,不过可以使用 range 出来的元素直接更新原有值:

func main() {
	data := []*struct{ num int }{{1}, {2}, {3}}
	for _, v := range data {
		v.num *= 10 // 直接使用指针更新
	}
	fmt.Println(data[0], data[1], data[2]) // &{10} &{20} &{30}
}


72.slice 中隐藏的数据

从 slice 中重新切出新 slice 时,新 slice 会引用原 slice 的底层数组。如果跳了这个坑,程序可能会分配大量的临时 slice 来指向原底层数组的部分数据,将导致难以预料的内存使用。

func get() []byte {
	raw := make([]byte, 10000)
	fmt.Println(len(raw), cap(raw), &raw[0]) // 10000 10000 0xc420080000
	return raw[:3]                           // 重新分配容量为 10000 的 slice
}

func main() {
	data := get()
	fmt.Println(len(data), cap(data), &data[0]) // 3 10000 0xc420080000
}


可以通过拷贝临时 slice 的数据,而不是重新切片来解决:

func get() (res []byte) {
	raw := make([]byte, 10000)
	fmt.Println(len(raw), cap(raw), &raw[0]) // 10000 10000 0xc420080000
	res = make([]byte, 3)
	copy(res, raw[:3])
	return
}

func main() {
	data := get()
	fmt.Println(len(data), cap(data), &data[0]) // 3 3 0xc4200160b8
}


73.Slice中数据的误用,重写文件路径

举个简单例子,重写文件路径(存储在 slice 中)

分割路径来指向每个不同级的目录,修改第一个目录名再重组子目录名,创建新路径:

// 错误使用 slice 的拼接示例
func main() {
	path := []byte("AAAA/BBBBBBBBB")
	sepIndex := bytes.IndexByte(path, '/') // 4
	println(sepIndex)

	dir1 := path[:sepIndex]
	dir2 := path[sepIndex+1:]
	println("dir1: ", string(dir1)) // AAAA
	println("dir2: ", string(dir2)) // BBBBBBBBB

	dir1 = append(dir1, "suffix"...)
	println("current path: ", string(path)) // AAAAsuffixBBBB

	path = bytes.Join([][]byte{dir1, dir2}, []byte{'/'})
	println("dir1: ", string(dir1)) // AAAAsuffix
	println("dir2: ", string(dir2)) // uffixBBBB

	println("new path: ", string(path)) // AAAAsuffix/uffixBBBB    // 错误结果
}


拼接的结果不是正确的 AAAAsuffix/BBBBBBBBB,因为 dir1、 dir2 两个 slice 引用的数据都是 path 的底层数组,第 13 行修改 dir1 同时也修改了 path,也导致了 dir2 的修改

解决方法:

  • 重新分配新的 slice 并拷贝你需要的数据
  • 使用完整的 slice 表达式:input[low:high:max],容量便调整为 max - low
// 使用 full slice expression
func main() {

	path := []byte("AAAA/BBBBBBBBB")
	sepIndex := bytes.IndexByte(path, '/') // 4
	dir1 := path[:sepIndex:sepIndex]       // 此时 cap(dir1) 指定为4, 而不是先前的 16
	dir2 := path[sepIndex+1:]
	dir1 = append(dir1, "suffix"...)

	path = bytes.Join([][]byte{dir1, dir2}, []byte{'/'})
	println("dir1: ", string(dir1))     // AAAAsuffix
	println("dir2: ", string(dir2))     // BBBBBBBBB
	println("new path: ", string(path)) // AAAAsuffix/BBBBBBBBB
}


第 6 行中第三个参数是用来控制 dir1 的新容量,再往 dir1 中 append 超额元素时,将分配新的 buffer 来保存。而不是覆盖原来的 path 底层数组

74.旧 slice

当你从一个已存在的 slice 创建新 slice 时,二者的数据指向相同的底层数组。如果你的程序使用这个特性,那需要注意 "旧"(stale) slice 问题。

某些情况下,向一个 slice 中追加元素而它指向的底层数组容量不足时,将会重新分配一个新数组来存储数据。而其他 slice 还指向原来的旧底层数组。

// 超过容量将重新分配数组来拷贝值、重新存储
func main() {
	s1 := []int{1, 2, 3}
	fmt.Println(len(s1), cap(s1), s1) // 3 3 [1 2 3 ]

	s2 := s1[1:]
	fmt.Println(len(s2), cap(s2), s2) // 2 2 [2 3]

	for i := range s2 {
		s2[i] += 20
	}
	// 此时的 s1 与 s2 是指向同一个底层数组的
	fmt.Println(s1) // [1 22 23]
	fmt.Println(s2) // [22 23]

	s2 = append(s2, 4) // 向容量为 2 的 s2 中再追加元素,此时将分配新数组来存

	for i := range s2 {
		s2[i] += 10
	}
	fmt.Println(s1) // [1 22 23]    // 此时的 s1 不再更新,为旧数据
	fmt.Println(s2) // [32 33 14]
}

75.类型声明与方法

从一个现有的非 interface 类型创建新类型时,并不会继承原有的方法:

// 定义 Mutex 的自定义类型
type myMutex sync.Mutex
 
func main() {
    var mtx myMutex
    mtx.Lock()
    mtx.UnLock()
}


mtx.Lock undefined (type myMutex has no field or method Lock)...

如果你需要使用原类型的方法,可将原类型以匿名字段的形式嵌到你定义的新 struct 中:

MySQL面试

1.事物的隔离级别有哪些2.mysql innodb 事物的隔离级别有那一种
3.索引相关
4.锁
5.表结构设计
6.数据优化

Redis面试

1.redis常用的数据结构底层实现是什么?ziplist、跳跃表

2.缓冲击穿,缓冲雪崩,缓冲穿透

3.redis优化

key的优化
key尽量都要设置过期时间
一级Key数量不要太多
bigkey尽量少

skey分级
shop:userinfo:1000
shop:userinfo:2000
集群
持久化
AOF 和 RDBs

推荐阅读