欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

go几个小技巧

程序员文章站 2024-02-19 11:27:40
...

1.常量分类

go里的调用单位是包,如何把常量分类又放到一个包下,通过映射是一种手段

package testcase

import (
    "fmt"
    "testing"
)

type Currency int

const (
    USD Currency = iota // 美元
    EUR                 // 欧元
    GBP                 // 英镑
    RMB                 // 人民币
)

func TestQuery2(t *testing.T) {
    symbol := [...]string{USD: "$", EUR: "€", GBP: "£", RMB: "¥"}
    fmt.Println(symbol[USD])
    fmt.Println(USD)
}

 

(2)格式化打印技巧,特别是在log模块。想看到一些SDK自带类型的详细结构

1)利用fmt.Println自带个格式编辑(打印内嵌类型,int,slice,map)

%v 值的默认格式。当打印结构体时,“加号”标记(%+v)会添加字段名

%#v  相应值的Go语法表示,会把字段,字段类型都打印出来

2)自定义类型(重写String方法)

type Family struct {
	Father  string
	Mather  string
	Brother string
}

func main() {
	f := Family{"fa", "ma", "bro"}

	fmt.Println(f)
}

func (f Family) String() string {
	return "Father: " + f.Father + "\nMather: " + f.Mather + "\nBrother: " + f.Brother
}

 

(3)字段对齐

go定义结构体struct存在字节对齐的概念(适当调整字段顺序,可以大量节省内存)

1)go各内嵌类型占用字节数(64位的机器)

bool 1字节

byte 1字节

int    8字节

float32 4字节

float64 8字节

string: 16字节

type stringStruct struct {
	str unsafe.Pointer
	len int
}
slice:24字节
type slice struct {
	array unsafe.Pointer
	len   int
	cap   int
}
map:8字节(可以理解为就是个指针)

验证代码:
type MyData struct {
    f0 byte
	f1 int
	f2 float32
	f3 string
	f4 []int
	f5 map[string]string
}

func main() {
	typ := reflect.TypeOf(MyData{})
	fmt.Printf("Struct is %d bytes long\n", typ.Size())
	// We can run through the fields in the structure in order
	n := typ.NumField()
	for i := 0; i < n; i++ {
		field := typ.Field(i)
		fmt.Printf("%s at offset %v, size=%d, align=%d\n",
			field.Name, field.Offset, field.Type.Size(),
			field.Type.Align())
	}
}

输出:
Struct is 72 bytes long
f0 at offset 0, size=1, align=1
f1 at offset 8, size=8, align=8
f2 at offset 16, size=4, align=4
f3 at offset 24, size=16, align=8
f4 at offset 40, size=24, align=8
f5 at offset 64, size=8, align=8
其中有一个很奇怪的现象f0只占用1个字节,但是f1不是从偏移量2开始,而是从8开始,这就涉及到go字节对齐的概念
64位的机器,一次只能获取8字节数据
struct里的字段会分配一块连续的内存,获取struct里某个字段的值,如果该字段的偏移量是8的整数倍或者该类型占用字节的整数倍,不需要计算偏移量,则可以快速获取
因此go对struct里定义的字段编译时会打padding,使得下一个字段的偏移量是8的整数倍或者该类型占用字节的整数倍以达到加速的目地,因此合理定义struct,
在某种程度上可以大量节省内存

比如:

type MyData struct {
	f0 byte
	f1 int
	f2 byte
	f3 int
	f4 byte
	f5 int
}

func main() {
	typ := reflect.TypeOf(MyData{})
	fmt.Printf("Struct is %d bytes long\n", typ.Size())
	// We can run through the fields in the structure in order
	n := typ.NumField()
	for i := 0; i < n; i++ {
		field := typ.Field(i)
		fmt.Printf("%s at offset %v, size=%d, align=%d\n",
			field.Name, field.Offset, field.Type.Size(),
			field.Type.Align())
	}
}

输出:
Struct is 48 bytes long
f0 at offset 0, size=1, align=1
f1 at offset 8, size=8, align=8
f2 at offset 16, size=1, align=1
f3 at offset 24, size=8, align=8
f4 at offset 32, size=1, align=1
f5 at offset 40, size=8, align=8

----------------------------------------------------------------------------------

type MyData struct {
	f0 byte
	f2 byte
	f4 byte
	f1 int
	f3 int
	f5 int
}

func main() {
	typ := reflect.TypeOf(MyData{})
	fmt.Printf("Struct is %d bytes long\n", typ.Size())
	// We can run through the fields in the structure in order
	n := typ.NumField()
	for i := 0; i < n; i++ {
		field := typ.Field(i)
		fmt.Printf("%s at offset %v, size=%d, align=%d\n",
			field.Name, field.Offset, field.Type.Size(),
			field.Type.Align())
	}
}

输出:
Struct is 32 bytes long
f0 at offset 0, size=1, align=1
f2 at offset 1, size=1, align=1
f4 at offset 2, size=1, align=1
f1 at offset 8, size=8, align=8
f3 at offset 16, size=8, align=8
f5 at offset 24, size=8, align=8

只是把结构体字段的顺序换了位置,占用的内存大大减少

 

 

4)边界检查

数组是否越界(不能运行时做边界检查,有时候特定技巧可以在编译级别取消边界检查)

go语言在处理切片数组时,在编译时如果边界检查去掉,如果程序存在大量的数组切片操作,并且对性能要求比较严格,则如何在编译成面消除边界检查

可作为一个优化反向

如:

// example1.go
package main

func f1(s []int) {
	_ = s[0] // 第5行: 需要边界检查
	_ = s[1] // 第6行: 需要边界检查
	_ = s[2] // 第7行: 需要边界检查
}

func f2(s []int) {
	_ = s[2] // 第11行: 需要边界检查
	_ = s[1] // 第12行: 边界检查消除了!
	_ = s[0] // 第13行: 边界检查消除了!
}


 

go1.1以后版本的编辑器已经非常智能,几乎可以消除所有能消除的边界检查,有时候,我们需要给标准编译器一些暗示来帮助标准编译器将这些不必要的边界检查消除掉。

func f6(s []int) {
	for i := 0; i < len(s); i++ {
		_ = s[i]
		_ = s[i:len(s)]
		_ = s[:i+1]
	}
}

func f7(s []int) {
	for i := len(s) - 1; i >= 0; i-- {
		_ = s[i]
		_ = s[i:len(s)]
	}
}

func f8(s []int, index int) {
	if index >= 0 && index < len(s) {
		_ = s[index]
		_ = s[index:len(s)]
	}
}

func f9(s []int) {
	if len(s) > 2 {
	    _, _, _ = s[0], s[1], s[2]
	}
}

上面的大量目前对切片数组的使用方式,编译器都可以识别数组不会越界并取消边界检查

对于将切片数组作为入参导致的编译器无法识别是否会越界,则可优化

func fd(is []int, bs []byte) {
	if len(is) >= 256 {
		for _, n := range bs {
			_ = is[n] // 第7行: 需要边界检查
		}
	}
}

func fd2(is []int, bs []byte) {
	if len(is) >= 256 {
		is = is[:256] // 第14行: 一个暗示
		for _, n := range bs {
			_ = is[n] // 第16行: 边界检查消除了!
		}
	}
}

 

 

 

相关标签: golang