Golang小知识
内建接口
内建接口,builtin-interface,是go包内的接口,你可以实现它,这样go的官方库比如fmt在调用时就会使用你实现的方法。
Stringer
类似于Java的toString,打印自身信息,很多包如fmt通过这个打印信息。
type Stringer interface {
String() string
}
package main
import "fmt"
type Person struct {
Name string
Age int
}
func (p Person) String() string {
return fmt.Sprintf("%v (%v years)", p.Name, p.Age)
}
func main() {
a := Person{"Arthur Dent", 42}
z := Person{"Zaphod Beeblebrox", 9001}
// 在pirint时会自动调用你自己实现的
fmt.Println(a, z)
}
error
error是一个接口类型,实现error接口的struct可以作为error返回,可以做到错误包含指定的信息。fmt在打印错误时也会调用这个。
type error interface {
Error() string
}
package main
import (
"fmt"
)
type MyError struct {
name string
}
// 只有实现了接口,该struct才能作为接口类型返回
func (mr MyError) Error() string {
return mr.name
}
func Judge(x float64) (float64, error) {
if x > 0 {
return 1, nil
} else {
// 新建一个struct,作为error接口的返回
return -1, MyError{name: "my error"}
}
}
func main() {
fmt.Println(Judge(2))
fmt.Println(Judge(-2))
}
// 输出
// 0 <nil>
// -1 my error
Read
io.Reader 接口有一个 Read 方法。Read 用数据填充给定的字节切片并返回填充的字节数和错误值。在遇到数据流的结尾时,它会返回一个 io.EOF 错误。
func (T) Read(b []byte) (n int, err error)
以下例子展示了通过实现内建接口的Read方法,改变其他方法的实现形式。即改变了数据流。
package main
import (
"io"
"os"
"strings"
)
type rot13Reader struct {
r io.Reader
}
// 转换byte 前进13位/后退13位
func rot13(b byte) byte {
switch {
case 'A' <= b && b <= 'M':
b = b + 13
case 'M' < b && b <= 'Z':
b = b - 13
case 'a' <= b && b <= 'm':
b = b + 13
case 'm' < b && b <= 'z':
b = b - 13
}
return b
}
// 重写Read方法
func (mr rot13Reader) Read(b []byte) (int, error) {
// 使用这个字符串的Read方法读出并处理
n, e := mr.r.Read(b)
for i := 0; i < n; i++ {
b[i] = rot13(b[i])
}
return n, e
}
func main() {
s := strings.NewReader("Lbh penpxrq gur pbqr!")
// 为什么要封装一层的呢,因为这个reader不属于任何Type,无法使用这个Type作为Receiver实现Read方法
r := rot13Reader{s}
// Copy会自动调用这个rot13Reader的Read方法
io.Copy(os.Stdout, &r)
}
// 输出
// You cracked the code!
Make和New的区别
-
共同点:都是Go分配内存的一种方式
-
不同点:
-
make只适用于slice、map、chan的初始化;new可以分配任何类型
-
**重要:**make分配和初始化类型,返回的是类型本身(他们本身就是指针);new分配这种类型的0值的空间,并返回一个指针指向这块区域,返回的是指针
a:=new([]int) b:=make([]int,0) fmt.Printf("%d %d",len(*a),len(b)) //a需要取值
-
为什么slice不需要make就可以append?而map和chan需要?
因为他们本质上都是只是分配一个符号,其实都需要make创建一下,只不过slice的append会自动判断空间是否足够,只是var没有make的话被认为是空间(cap)不够会重新分配空间。
“…”操作符的用法
典型用法-可变参数
func sumMany(a ...int)int {
sum := 0
for _, v := range a {
sum += v
}
return sum
}
append赋值
func main() {
a := []int{1, 2, 3}
b := []int{4, 5, 6}
a = append(a, b...)
}
...运算符是个语法糖,它把前面的slice直接复制给可变参数,而不是先解包成独立的n个参数再传递
iota的用法
https://blog.wolfogre.com/posts/golang-iota/
- 不同 const 定义块互不干扰;
- 所有注释行和空行全部忽略;
- 没有表达式的常量定义复用上一行的表达式,例如
2*iota则后面是2、4、6、8,“str"后也是"str” - 从第一个没有iota开始,iota 从 0 逐行加一;
- 替换所有 iota。
对nil的理解
https://juejin.cn/post/6950053304650956807
变量仅仅是绑定到一个指定内存块的名字;
Go 从语言层面对程序员做了承诺,变量定义分配的内存一定是置 0 分配的,不像C;
并不是所有的类型能够赋值 nil,并且和 nil 进行对比判断。只有 slice、map、channel、interface、指针、函数 这 6 种类型,即指针类型
- 不要把
nil理解成一个特殊的值,而要理解成一个触发条件,编译器识别到代码里有nil之后,会对应做出处理和判断; channel,map类型的变量必须要make才能使用的原因(否则会出现空指针的 panic )在于 var 定义的变量仅仅是分配了一个指向hchan和hmap的指针变量而已,并且还是置 0 分配的。真正的管理结构只有 make 调用才能分配出来,对应的函数分别是makechan和makemap等。slice变量为什么var就能用是因为struct slice核心结构是定义的时候就分配出来了;- 以上 6 种变量赋值
nil的行为都是把变量本身置 0 ,仅此而已。slice的 24 字节管理结构,map的 8 字节指针,channel的 8 字节指针,interface的 16 字节,8 字节指针和函数指针也是如此; - 以上 6 种类型和
nil进行比较判断本质上都是和变量本身做判断,slice是判断管理结构的第一个指针字段,map,channel本身就是指针,interface也是判断管理结构的第一个指针字段,指针和函数变量本身就是指针; - var只是声明变量,创建一个指向值的指针。make和new才是让这个值分配和置零的操作,而new是返回指针(或者说是二级指针),make直接是返回一级指针
Init函数
- init()函数是用于程序执行前做包的初始化的函数,比如初始化包里的变量等;
- 一个包可以出现多个init()函数,一个源文件也可以包含多个init()函数;
- 同一个包中多个init()函数的执行顺序没有明确的定义,但是不同包的init函数是根据包导入的依赖关系决定的;
- init函数在代码中不能被显式调用、不能被引用(赋值给函数变量),否则出现编译失败;
- 一个包被引用多次,如A import B,C import B,A import C,B被引用多次,但B包只会初始化一次;
- 引入包,不可出现死循环。即A import B,B import A,这种情况下编译失败;
导入包的四种方法
-
import "fmt" -
import F "fmt" //以F作为别名 -
import . "fmt" //不需要加fmt前缀即可直接使用 -
import _ "fmt" //仅运行该包里的init方法,而不使用
defer的用法
- defer需要先注册才能生效,即运行到位置
- defer会缓存当前状态到函数参数或值,若是函数参数地址类型,这个地址的内容变了,函数的传入参数也会变,若是形参则缓存下来就不会变
- 即使defer函数参数里有函数运算,这个函数也会因为先算出参数值而运行
panic和error
https://zhuanlan.zhihu.com/p/87345297
Go 的 error 就相当于原先的 “Exception”,而那些异常严重的 “Exception” 叫 panic
panic:
- 内建函数
- 假如函数F中书写了panic语句,会终止其后要执行的代码,在panic所在函数F内如果存在要执行的defer函数列表,按照defer的逆序执行
- 返回函数F的调用者G,在G中,调用函数F语句之后的代码不会执行,假如函数G中存在要执行的defer函数列表,按照defer的逆序执行
- 直到goroutine整个退出,并报告错误
recover:
- 内建函数
- 用来控制一个goroutine的panicking行为,捕获panic,从而影响应用的行为
- 一般的调用建议
- 在defer函数中,通过recover来终止一个goroutine的panicking过程,从而恢复正常代码的执行
- 可以获取通过panic传递的error
简单来讲:go中可以抛出一个panic的异常,然后在defer中通过recover捕获这个异常,然后正常处理。
例子:
func main() {
defer func() {
p:=recover()
if p!=nil{
fmt.Println(p)
}
}()
panic("tong")
}
读写文件方式总结
读文件
最简单流程是:打开文件->读取文件到指定位置->关闭文件
func main() {
f, _ := os.Open("main/testF.txt")
defer f.Close()
buf := make([]byte, 1024)
n,_:=f.Read(buf)
fmt.Println(string(buf[:n]))
}
这样做的是读取的时候不知道有多大,设置多大的buf字节切片,可以考虑使用for循环读入然后存储。也可考虑使用另外一种,直接读取全部,无需创建切片使用ioutil
func main() {
f, _ := os.Open("main/testF.txt")
defer f.Close()
buf, _ := ioutil.ReadAll(f)
fmt.Println(string(buf))
}
甚至可以直接读取,小文件常用
func main() {
buf, _ := ioutil.ReadFile("main/testF.txt")
fmt.Println(string(buf))
}
这样可能会造成频繁的内存和磁盘的io,可以使用带缓冲的io,提前将文件从磁盘读到内存当中
func main() {
f, _ := os.Open("main/testF.txt")
defer f.Close()
r := bufio.NewReader(f)
buf := make([]byte, 1024)
n, _ := r.Read(buf)
fmt.Println(string(buf[:n]))
}
写文件
最简单流程是:打开文件(注意打开模式)->从指定位置写入文件->关闭文件
func main() {
f, _ := os.OpenFile("main/testF.txt", os.O_APPEND, 0666)
defer f.Close()
f.Write([]byte("Hello, world!"))
}
Goroutine
轻量级进程,是非抢占式的调度
- 独立栈空间
- 共享堆空间
- 用户控制调度
MPG模型
https://www.cnblogs.com/secondtonone1/p/11803961.html

- G: Goroutine,即我们在 Go 程序中使用
go关键字创建的执行体; - M: Machine,或 worker thread,即传统意义上进程的线程;
- P: Processor,即一种人为抽象的、用于执行 Go 代码的局部资源。只有当 M 与一个 P 关联后才能执行 Go 代码。除非 M 发生阻塞或在进行系统调用时间过长时,没有与之关联的 P。
channel
channel各种情况下的操作情况
- nil channel
- 读写 阻塞
- close panic 非法操作内存
- closed channel
- 读 零值,或者是其中残存的数据
- 写 panic 非法操作内存
- 关闭 panic 非法操作内存
用于在数据流动方向。
func main() {
var c chan int
//如果去掉下面的make,会死锁。此时相当于c是一个没有缓冲的channel
c = make(chan int, 1)
for i := 0; i < 2; i++ {
if i%2 == 0 {
c <- 1
} else {
_ = <-c
}
}
}
select默认是阻塞的,只有当监听的channel中有发送或接收可以进行时才会运行,当多个channel都准备好的时候,select是随机的选择一个执行的。
使用必须make,可以声明为只读或只写
channel 死锁
map
用法
首先需要make,var出来的不可以使用
添加直接m[k]=v即可,不存在则创建,存在则修改
删除delete(m,k),不存在则不报错,存在则删除
查找value, ok = m[k],若不存在则ok为false,value为默认值
原理
大致流程:传入key经过hash后得到值,根据值找map里的buckets(bmap切片)会找到里面的一个bmap,再根据hash值的高8位寻找这个bmap里的topbits获得索引后匹配keys和elems

为什么线程不安全?
因为hash map 的内存是按照2的倍数开辟的,当前面开辟的内存不够的时候,会新开辟一段内存,将原来内存的数据转移到新的内存块中,这个过程是没有加锁的,如果这个时候同时有个读的线程过来获取这块内存数据,就会出现安全问题。切片也有这样的问题,但是人们通常认为切片出现这样的问题是使用者问题,于是不报错。
Time包的使用
获取时间戳
//返回Int64类型
time.Now().Unix() //秒级时间戳
time.Now().UnixNano() //纳秒级时间戳
//转换成字符串
strconv.FormatInt(time.Now().Unix(), 10)
获取当前时间
now := time.Now() //获取当前时间
year := now.Year() //年
month := now.Month() //月
day := now.Day() //日
hour := now.Hour() //小时
minute := now.Minute() //分钟
second := now.Second() //秒
//转换成字符串
now.String()
int8溢出问题
func main() {
count := 0
for i := range [256]struct{}{} {
m, n := byte(i), int8(i)
if n == -n {
/*
补码表示法中,正数首位为0, 而负数首位为1.
int8 1位符号位+7位数值 范围+127~-128
1+2+4+8+16+32+64=127
n=127
原码: 0 111 1111 127
理论上加1后变成1 000 0000 溢出了
n=-128
原码: 1 000 0000 128
反码: 0 111 1111
补码: 1 000 0000 -128
和-128编码相同,即为-128
即+127+1后溢出变为-128
*/
fmt.Println("n:", n)
count++
}
if m == -m {
/*
uint 8位数值 范围0~+255
m=128
原码 1000 0000 128
m=-128 溢出,但是仍用补码计数法表示
原码: 1000 0000 128
反码: 0111 1111
补码: 1000 0000 -128 溢出了
*/
fmt.Println("m:", m)
count++
}
}
fmt.Println(count)
}
结果:
n: 0
m: 0
n: -128
m: 128
4
reflect.DeepEqual
判断两个interface是否相同,甚至可以直接比较基本数据类型比如int
func main() {
type mys struct {
name string
}
a := mys{name: "a"}
b := mys{name: "a"}
fmt.Println(reflect.DeepEqual(a, b))//true
}
变量的简短模式
形如:x := 100 。这种声明方式有限制:
- 必须使用显式初始化;
- 不能提供数据类型,编译器会自动推导;
- 只能在函数内部使用简短模式;
错误的位置
//全局变量
var x:=1
//常量
const x := 1
slice切片的第三个参数
slice[i:j:k]从i开始到j-1,容量(cap)为k-i
结构体嵌套
type People struct {
name string
}
type Teacher struct {
People
}
func (t Teacher) say() {
fmt.Println("I am a teacher" + t.name)
}
func main() {
t := Teacher{People{
name: "",
}}
t.name = "zhangsan"
t.say()
}
在嵌套结构体中,People 称为内部类型,Teacher 称为外部类型;通过嵌套,内部类型的属性、方法,可以为外部类型所有,就好像是外部类型自己的一样。此外,外部类型还可以定义自己的属性和方法,甚至可以定义与内部相同的方法,这样内部类型的方法就会被“屏蔽”。
运行时获取命令行参数
使用os.Args,这是一个字符串切片
或者使用flag包获取
先绑定再解析
func main() {
var u string
var p int
var b bool
//绑定地址,名称,默认值,解释
flag.StringVar(&u, "u", "", "username")
flag.IntVar(&p, "p", 0, "password")
flag.BoolVar(&b, "b", false, "bool")
flag.Parse()
fmt.Println(u, p, b)
}
自动解引用
C对于指针一定是要对指针取值才能访问的例如
*int a;
a=(*int)malloc(sizeof(int))
printf("%d",*a);
而go对于使用type自定义的类型,可以做到自动取值,而内置类型不可以,比如*[]int,切片指针就需要手动取值。
func main() {
type s struct {
a int
b *int
c string
d []int
}
e := &s{
a: 1,
b: new(int),
c: "ok",
d: []int{1, 2, 3},
}
fmt.Println(e.c, e.b, *e.b, e.c, e.d[1])
fmt.Println((*e).c, (*e).b, *(*e).b, (*e).c, (*e).d[1])
/*
二者输出相同
ok 0xc00000a2e0 0 ok 2
ok 0xc00000a2e0 0 ok 2
*/
f := new(string)
*f = "ok"
fmt.Println(f)
fmt.Println(*f)
/*
二者输出不同
0xc000044570
ok
*/
}
另外,函数的接收者作为自定义类型,也是能够实现自动解引用的,这时如果传入指针,则对接收者做出的修改将保存。
类型别名
基本介绍
Go定义类型别名有以下两种方式
//1
type MyType = Type
//2
type MyType Type
第一种方式是真正的起别名,第二种方式是定义了一个新的类型。
第二种方式MyType本质上仍是Type,如下程序说明
type MyType = Type
var a MyType
fmt.Printf("%T", a)
//输出Type
而第二种方式则是创建了新的类型,如下程序说明
type MyType Type
var a MyType
fmt.Printf("%T", a)
//输出MyType
使用区别
只需记住以下几点
-
go具有包管理机制,所以不能在当前包定义其他包的类型的函数,因为别名本质上还是别的包的类型,不是新类型,所以需要在本包创建新的类型,将
type MyType = OtherPkg.Type改为type MyType OtherPkg.Type即可。import "OtherPkg" type MyType = OtherPkg.Type func (m MyType)MyFunc(){ } //无法在非局部类型 'OtherPkg.Type' 上定义新方法 -
go具有强类型,所以两个包里即使相同的类型定义但是不能相等,需要类型转换或类型别名
///package other package other type ListNode{ Val int, ListNode *ListNode } ///package main package main import "other" type ListNode other.ListNode func main(){ var a ListNode var b *ListNode = a.Next //出错,此ListNode是main.ListNode a.Next是other.ListNode }此时将
type ListNode other.ListNode替换成type ListNode = other.ListNode,或者将var b ListNode = a.Next替换成var b ListNode = (*other.ListNode)a.Next。 -
别名本质上还是原类型,所以在表现多态性质的时候不能是做两个类型
垃圾回收
垃圾回收器有两种思路
- 引用计数:引用计数无法解决循环引用问题
- 可达性分析:主流垃圾回收器都使用这种,Golang使用可达性分析
垃圾回收器的工作流程大体如下:
- 标记出哪些对象是存活的,哪些是可回收的。
- 标记 GC ROOT 能关联到的对象。这里会 STW。
- 从 GCRoots 的直接关联对象开始遍历整个对象图。这里不会STW。
- 进行回收(清除/复制/整理)。如果在回收期间有移动过的对象(复制/整理),还需要更新引用。
垃圾回收算法主要做的就是1.2,三色标记法也不例外,它将从GC Roots 开始遍历的对象标记为以下三种颜色:
- 白色,初始值。本次回收没被扫描过的对象默认都是白色的。而确认不可达的对象也是白色,但是会被标记「不可达」。
- 灰色,中间状态。本对象有被外部引用,但是本对象引用的其它对象尚未全部检测完。
- 黑色,本对象有被其它对象引用,且已检测完本对象引用的其它对象。
问题:多标白色,回收掉不该回收的
A-B C,C没有标记到,然后清除时A又连上了C
问题:漏标白色,少回收了
A-B,然后清除时A和B断开了,B没有被清理
Go解决问题:
- 将对象分为堆上的对象和栈上的对象。
- GC 开始将栈上的对象全部扫描并标记为黑色,无需 STW。并且之后不再进行第二次重复扫描
- 在 GC 期间,任何在栈上创建的新对象,均为黑色。
- 在 GC 期间,在堆上被删除或者添加的对象都标记为灰色。后续继续扫描。