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的区别

为什么slice不需要make就可以append?而map和chan需要?

因为他们本质上都是只是分配一个符号,其实都需要make创建一下,只不过slice的append会自动判断空间是否足够,只是var没有make的话被认为是空间(cap)不够会重新分配空间。

img

“…”操作符的用法

典型用法-可变参数

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/

  1. 不同 const 定义块互不干扰;
  2. 所有注释行和空行全部忽略;
  3. 没有表达式的常量定义复用上一行的表达式,例如2*iota则后面是2、4、6、8,“str"后也是"str”
  4. 从第一个没有iota开始,iota 从 0 逐行加一;
  5. 替换所有 iota。

对nil的理解

https://juejin.cn/post/6950053304650956807

变量仅仅是绑定到一个指定内存块的名字;

Go 从语言层面对程序员做了承诺,变量定义分配的内存一定是置 0 分配的,不像C;

并不是所有的类型能够赋值 nil,并且和 nil 进行对比判断。只有 slice、map、channel、interface、指针、函数 这 6 种类型,即指针类型

Init函数

  1. init()函数是用于程序执行前做包的初始化的函数,比如初始化包里的变量等;
  2. 一个包可以出现多个init()函数,一个源文件也可以包含多个init()函数;
  3. 同一个包中多个init()函数的执行顺序没有明确的定义,但是不同包的init函数是根据包导入的依赖关系决定的;
  4. init函数在代码中不能被显式调用、不能被引用(赋值给函数变量),否则出现编译失败;
  5. 一个包被引用多次,如A import B,C import B,A import C,B被引用多次,但B包只会初始化一次;
  6. 引入包,不可出现死循环。即A import B,B import A,这种情况下编译失败;

导入包的四种方法

  1. import "fmt"
    
  2. import F "fmt" //以F作为别名
    
  3. import . "fmt" //不需要加fmt前缀即可直接使用
    
  4. import _ "fmt" //仅运行该包里的init方法,而不使用
    

defer的用法

panic和error

https://zhuanlan.zhihu.com/p/87345297

Go 的 error 就相当于原先的 “Exception”,而那些异常严重的 “Exception” 叫 panic

panic:

  1. 内建函数
  2. 假如函数F中书写了panic语句,会终止其后要执行的代码,在panic所在函数F内如果存在要执行的defer函数列表,按照defer的逆序执行
  3. 返回函数F的调用者G,在G中,调用函数F语句之后的代码不会执行,假如函数G中存在要执行的defer函数列表,按照defer的逆序执行
  4. 直到goroutine整个退出,并报告错误

recover:

  1. 内建函数
  2. 用来控制一个goroutine的panicking行为,捕获panic,从而影响应用的行为
  3. 一般的调用建议
    1. 在defer函数中,通过recover来终止一个goroutine的panicking过程,从而恢复正常代码的执行
    2. 可以获取通过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

img

channel

channel各种情况下的操作情况

channel的使用

用于在数据流动方向。

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 死锁

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

http://cdn.tigerb.cn/20201217165551.png

为什么线程不安全?

因为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 。这种声明方式有限制:

  1. 必须使用显式初始化;
  2. 不能提供数据类型,编译器会自动推导;
  3. 只能在函数内部使用简短模式;

错误的位置

//全局变量
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的三色标记法

垃圾回收器有两种思路

垃圾回收器的工作流程大体如下:

  1. 标记出哪些对象是存活的,哪些是可回收的。
    1. 标记 GC ROOT 能关联到的对象。这里会 STW。
    2. 从 GCRoots 的直接关联对象开始遍历整个对象图。这里不会STW。
  2. 进行回收(清除/复制/整理)。如果在回收期间有移动过的对象(复制/整理),还需要更新引用。

垃圾回收算法主要做的就是1.2,三色标记法也不例外,它将从GC Roots 开始遍历的对象标记为以下三种颜色:

问题:多标白色,回收掉不该回收的

A-B C,C没有标记到,然后清除时A又连上了C

问题:漏标白色,少回收了

A-B,然后清除时A和B断开了,B没有被清理

Go解决问题:

  1. 将对象分为堆上的对象和栈上的对象。
  2. GC 开始将栈上的对象全部扫描并标记为黑色,无需 STW。并且之后不再进行第二次重复扫描
  3. 在 GC 期间,任何在栈上创建的新对象,均为黑色。
  4. 在 GC 期间,在堆上被删除或者添加的对象都标记为灰色。后续继续扫描。
© 2019 - 2023 · YuYoung's Blog