golang 编程规范查漏补缺

这篇具有很好参考价值的文章主要介绍了golang 编程规范查漏补缺。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

背景

公司最近出了 golang 语言规范,大部分参考 uber 的 go 语言规范(原版和翻译),以及官方的 Effective Go。这里分享一下自己之前没注意的点,查漏补缺

方法和函数

defer 和返回值赋值的执行顺序

对应知识点为方法返回值是有名还是无名的时候,defer 的顺序的差异

package main

func deferWithAnonymous() int {
	ret := 1
	defer func() {
		ret++
	}()
	return ret
}

func deferWithNamed() (ret int) {
	ret = 1
	defer func() {
		ret++
	}()
	return
}

func main() {
	println(deferWithAnonymous()) // 1
	println(deferWithNamed()) // 2
}

defer 和返回值之间的关系: 设置函数返回值 -> 执行 defer -> 最终返回给调用方
关键在第一步,匿名返回值函数中,设置的返回值就是具体的值,而在有名返回值函数,设置的是返回值的引用(即 ret 的引用)
所以有名返回值函数的 defer 会影响最后的返回值

对 defer 的字节码解析可以参考这篇文章

sync.Mutex 作为传参的时候,需要传指针,否则可能导致死锁

因为 Mutex 的加锁和释放锁逻辑是通过内部的state和sema两个整数对象控制的,直接拷贝 Mutex 只是复制了锁的状态,但和原来的锁并不是同一个,所以释放复制后的 Mutex 并不能解锁原来的 Mutex

一个复现这个问题的示例,是通过 pointer receiver 占锁,通过 value receiver 释放锁,由于 value receiver 会拷贝调用者对象,所以释放的锁对象和外面的不同,导致死锁

参考-Detect locks passed by value in Go

package main

import "sync"

type T struct {
    lock sync.Mutex
}
func (t *T) Lock() {
    t.lock.Lock()
}
func (t T) Unlock() {
   t.lock.Unlock()
}
func main() {
    t := T{lock: sync.Mutex{}}
    t.Lock()
    t.Unlock()
    t.Lock() // 死锁
}

基本类型

interface 的判空

Go 面试题:Go interface 的一个 “坑” 及原理分析

interface 表示 golang 的接口类型,它和其他语言的“基类”(如 Java 的 interface)相比,在空对象上的表现不太一样

示例代码: 思考以下代码会输出什么

type MyError struct {
	msg string
}

func (err *MyError) Error() string {
	return err.msg
}

func workWithBalance() bool {
	return true
}

func workTooHard() bool {
	return false
}

func getError(f func() bool) error {
	var err *MyError
	if !f() {
		err = &MyError{
			msg: "need relax",
		}
	}
	return err
}

func main() {
	if err := getError(workTooHard); err != nil {
		println("work too hard caused " + err.Error())
	}
	if getError(workWithBalance) == nil {
		println("work with balance")
	}
}

以上代码对自定义错误 MyError 进行了判空,预期是通过 getError(workWithBalance) 获取到的 error 为空,结果却不为空(work with balance 不会打印)

那么为什么 var err *MyError 声明,但没有赋值的 err 判空得到的是 false 呢?我们可以从 interface 的内部结构 iface、eface 可以了解到端倪

// runtime/runtime2.go

type eface struct {
    _type *_type
    data  unsafe.Pointer
}

type iface struct {
    tab  *itab
    data unsafe.Pointer
}

其中,iface 包含了接口的类型、方法和数据,iface 的 tab 描述了接口的类型和方法,data 则指向实际的接口数据

itab 的结构如下:

type itab struct {
	inter *interfacetype // abi.InterfaceType(abi: application binary interface 二进制接口),包含接口类型,pkg path(import 的路径)和接口方法(Imethod)
	_type *_type // abi.Type,实体类型
	hash  uint32 // _type.hash 拷贝而来
	_     [4]byte // 占位,留给以后可能用到的对象
	fun   [1]uintptr // 接口方法对应的地址,多个方法则在这个数组后面继续添加,fun[0] == 0 表示未实现接口的方法
}

而 eface 的数据结构就简单很多了,只包含实体类型 _type 和数据指针 data,不包含方法信息
不包含方法的 eface 对应 var i interface{} 这种对象声明,主要用于 传参、序列化和泛型场景

那么 go 是如何判断一个 interface 类型对象是否为空呢?需要两个条件:data 对应的值为空,且 _type 类型也为空
通过 getError(workWithBalance) 获取的 error,虽然没有被初始化,但它有具体实现类型(MyError)而不是纯接口类型(error),所以 err == nil 为 false

想要判断 interface 背后的对象的值确实为空,有两种办法:先强转成具体的类型指针再判断,或者是通过反射方法 reflact.ValueOf 获取到内部的值来判断

e := getError(workWithBalance)
v := reflect.ValueOf(e)
if e.(*MyError) == nil {
	println("err is nil")
}
// 注意: IsNil 对一些无法判断空值的类型,或者未初始化的 interface 会直接 panic,所以需要先判断 value 的 kind
if v.Kind() == reflect.Pointer {
	if v.IsNil() {
		println("err is nil")
	}
}

扩展: 空接口对象,是否可以调用接口方法呢?

type MyError struct {
	msg string
}

func (err *MyError) Error() string {
	if err == nil {
		return "empty error"
	}
	return err.msg
}

func main() {
	var emptyErr *MyError
	println(emptyErr.Error()) // 不会 panic
}

结论是可以调用,这一点和其他语言很不同。一个指针是否可以调用方法,取决于它的类型而不是实际值是否为空,空接口对象调用 pointer receiver 不会报空指针,但注意只是能调用,如果 pointer receiver 内部有获取对象属性的操作,还是会报空指针错误

参考-nil receiver in GoLang

参考-Calling a method on a nil struct pointer doesn’t panic. Why not?

nil channel 的使用场景

在公司规范中,说明“禁止对 nil 或已关闭的 channel 进行读写关闭操作”,这一句算是规范中为数不多需要指正的一点:nil channel 在特定场景是有用的

先了解一下各种特殊情况下使用 channel 会出现什么情况

closed channel: 读不阻塞(会读完剩下的数据,之后返回零值)、写 panic、再次 close panic
nil channel: 读阻塞、写阻塞、close panic

对于 nil channel 读写都会阻塞的特性,有一个使用场景是 合并多个 channel 数据的时候,对于已经取完数据的 channel 可以置为空,这样在继续使用 select 的同时也不影响其他还有数据的 channel 的读取,参考

func merge(a, b <-chan int) <-chan int {
	c := make(chan int)
	go func() {
		defer close(c)
		for a != nil || b != nil {
			select {
			case v, ok := <-a:
				if !ok {
					fmt.Println("a is done")
					a = nil
					continue
				}
				c <- v
			case v, ok := <-b:
				if !ok {
					fmt.Println("b is done")
					b = nil
					continue
				}
				c <- v
			}
		}
	}()
	return c
}

高性能场景

使用 sync.Pool 获取需要频繁申请的对象

比较典型的场景是在高并发的数据流读取和写入场景中,通过 pool 缓存 buffer,避免每次都申请新的 buffer 造成频繁内存资源申请

在框架层代码中会比较容易看到 pool 的使用,如 gin 用来缓存处理请求的 Context 对象,gorm 用来缓存序列化对象(SerializerInterface)等

性能测试结果:

func BenchmarkByteBufferWithoutPool(b *testing.B) {
	for i := 0; i < b.N; i++ {
		buf := bytes.Buffer{}
		buf.WriteString(longStr)
		io.Copy(io.Discard, &buf)
	}
}

func BenchmarkByteBufferWithPool(b *testing.B) {
	pool := sync.Pool{
		New: func() any {
			return new(bytes.Buffer)
		},
	}

	for i := 0; i < b.N; i++ {
		buf := pool.Get().(*bytes.Buffer)
		buf.WriteString(longStr)
		io.Copy(io.Discard, buf)
		buf.Reset()
		pool.Put(buf)
	}
}

// 测试结果
// BenchmarkByteBufferWithoutPool-8           55544210               211.1 ns/op          1072 B/op          2 allocs/op
// BenchmarkByteBufferWithPool-8           355192696               33.25 ns/op            0 B/op          0 allocs/op

从执行次数和内存开销来看,pool 在多协程下达到的对象复用的效果,都能带来很大的提升

bytes 和 string 的 0 内存申请方法

直接看无内存开销的转换方式:

func ByteSliceToString(bytes []byte) string {
	var s string
	sliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&bytes))
	stringHeader := (*reflect.StringHeader)(unsafe.Pointer(&s))
	stringHeader.Data = sliceHeader.Data
	stringHeader.Len = sliceHeader.Len
	return s
}

func StringToByteSlice(s string) (bytes []byte) {
	bh := (*reflect.SliceHeader)(unsafe.Pointer(&bytes))
	sh := *(*reflect.StringHeader)(unsafe.Pointer(&s))
	bh.Data = sh.Data
	bh.Len = sh.Len
	bh.Cap = sh.Len
	return
}

参考

两者的相互转换都用到了反射包中表示底层结构的对象,如 slice 的 SliceHeader,string 和 StringHeader。因为 string 和 byte 数组两者的底层数据结构非常相似,只相差 slice 的 cap,所以转换逻辑并不复杂

string 和 slice 的底层结构在go源码中如下:

// runtime/string.go
type stringStruct struct {
	str unsafe.Pointer
	len int
}

// runtime/slice.go
type slice struct {
	array unsafe.Pointer
	len   int
	cap   int
}

// reflect/value.go
type StringHeader struct {
	Data uintptr
	Len  int
}

type SliceHeader struct {
	Data uintptr
	Len  int
	Cap  int
}

go 1.20 之后,StringHeader 和 SliceHeader 被标注为 Deprecated,改为推荐使用 StringData 和 SliceData,写法上更简单了

参考-The conversion of byte slice and string has changed again in Go 1.20

func byteSliceToString(bytes []byte) string {
	return unsafe.String(unsafe.SliceData(bytes), len(bytes))
}

func stringToByteSlice(s string) (bytes []byte) {
	return unsafe.Slice(unsafe.StringData(s), len(s))
}

实测: 直接强转和通过反射转换的benchmark测试结果对比

bytes 转 string

BenchmarkForceConvertBytesToString-8    66501550               178.7 ns/op          1024 B/op          1 allocs/op

BenchmarkConvertBytesToString-8         1000000000               0.3236 ns/op          0 B/op          0 allocs/op

可以看到,强转的方式执行速度(平均每次 178ns)远小于通过反射方式执行的,并且强转每次需要申请 1kb 内存,刚好和转换的字符串大小对应

string 转 bytes

BenchmarkForceConvertStringToBytes-8    67139846               200.6 ns/op          1024 B/op          1 allocs/op

BenchmarkConvertStringToBytes-8         1000000000               0.3230 ns/op          0 B/op          0 allocs/op

结果和 bytes 转 string 类似,不再赘述

高并发的任务(如接口)创建协程池去消费和执行

协程确实很”轻“,相比操作系统线程默认大小为1M 来说,它的初始大小只有 2k,确实很小(但随着栈空间扩大可能会扩缩容),不过在高并发场景下还是需要对开启协程进行控制的

协程池的选型有很多,常见的开源项目有 tunny 和 ants,两者实现方式略有区别,tunny 提交任务时是同步提交,可以拿到执行后的返回值,ants 是异步提交,不支持获取返回值,要拿到返回值的话得自己实现。示例如下:

import (
	"github.com/Jeffail/tunny"
	"github.com/panjf2000/ants/v2"
)

func TestTunnyPool(t *testing.T) {
	wg := sync.WaitGroup{}
	wg.Add(100)
	pool := tunny.NewFunc(10, func(payload interface{}) interface{} {
		time.Sleep(3 * time.Second)
		wg.Done()
		return payload
	})
	defer pool.Close()

	for i := 0; i < 100; i++ {
		// tunny.pool.Process 是同步方法,所以需要开启协程才能并发
		go func(i int) {
			pool.Process(i)
		}(i)
	}

	wg.Wait()
}

func TestAntsPool(t *testing.T) {
	wg := sync.WaitGroup{}
	wg.Add(100)
	pool, _ := ants.NewPoolWithFunc(10, func(i interface{}) {
		fmt.Printf("%d execute\n", i)
		time.Sleep(3 * time.Second)
		fmt.Printf("%d finish\n", i)
		wg.Done()
	})
	defer pool.Release()

	for i := 0; i < 100; i++ {
		pool.Invoke(i)
	}

	wg.Wait()
}

当然,对于 web 框架来说,这种控制并发的功能官方都有。如 gin 通过 limit 插件,本质也是通过 channel 控制并发协程数文章来源地址https://www.toymoban.com/news/detail-727234.html

到了这里,关于golang 编程规范查漏补缺的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 数据结构:阶段测试(查漏补缺)

    目录 选择题: 题一: 题二: 题三: 题四: 编程题: 题一:左叶子之和 思路一: 题二:约瑟夫问题(用单链表实现) 思路一: 本人实力有限可能对一些地方解释和理解的不够清晰,可以自己尝试读代码,或者评论区指出错误,望海涵! 感谢大佬们的一键三连! 感谢大

    2024年02月08日
    浏览(22)
  • C++新经典 | C++ 查漏补缺(智能指针)

    目录 一、动态分配 1.初始化的三种方式 2. 释放内存 (1)悬空指针  3.创建新工程与观察内存泄漏 二、深入了解new/delete 1.new和delete 2.operator new()和operator delete() 3.申请和释放一个数组 三、智能指针  1.shared_ptr (1)常规初始化 (2)make_shared (3)引用计数 (3.1)引用计

    2024年02月07日
    浏览(25)
  • Android SystemUI 信号栏后添加信号图标,查漏补缺

    android:layout_width=“wrap_content” android:layout_marginStart=“1dp” android:visibility=“gone” android:tag=“mobile_slot_indicator_4” /      ImageView android:id=“@+id/custom_signal_4g” android:layout_height=“wrap_content” android:layout_width=“wrap_content” android:visibility=“gone” android:background=“@drawable/stat_sys

    2024年03月26日
    浏览(37)
  • 5种常用Web安全扫描工具,快来查漏补缺吧!

    漏洞扫描是一种安全检测行为,更是一类重要的网络安全技术,它能够有效提高网络的安全性,而且漏洞扫描属于主动的防范措施,可以很好地避免黑客攻击行为,做到防患于未然。那么好用的漏洞扫描工具有哪些? 答案就在本文! 1、AWVS Acunetix Web Vulnerability Scanner(简称

    2024年02月08日
    浏览(29)
  • Android-高级-UI-进阶之路-(二)-深入理解-Android-8-0-View-触摸事件分发机制,查漏补缺

    我们看到内部又调用了父类 dispatchTouchEvent 方法, 所以最终是交给 ViewGroup 顶级 View 来处理分发了。 顶级 View 对点击事件的分发过程 在上一小节中我们知道了一个事件的传递流程,这里我们就大致在回顾一下。首先点击事件到达顶级 ViewGroup 之后,会调用自身的 dispatchTouchE

    2024年04月14日
    浏览(58)
  • C温故补缺(十八):网络编程

    参考:TCP三次握手详解. 简单分层: 其中,链路层还可以分出物理层和数据链路层。应用层可以分出会话层,表示层和应用层。 七层模型: 链路层:只是物理的比特流和简单封装的数据帧 网络层:主要任务是,通过路由选择算法,为报文通过通信子网选择最适当的路径。也就

    2024年02月07日
    浏览(27)
  • golang推荐的命名规范

    很少见人总结一些命名规范,也可能是笔者孤陋寡闻, 作为一个两年的golang 开发者, 我根据很多知名的项目,如 moby, kubernetess 等总结了一些常见的命名规范。 命名规范可以使得代码更容易与阅读, 更少的出现错误。 文件命名规范 由于文件跟包无任何关系, 而又避免win

    2024年02月02日
    浏览(64)
  • Java编程规范(代码规范)--精选

    说明 本文介绍精选的Java编程规范(代码规范)。遵守这些规范,代码的bug数将会大幅减少,代码可维护性、可读性、扩展性会大幅上升。(本文持续更新) 为什么要有编程规范? 编程规范有如下作用: 提高代码可读性、维护性、扩展性 提高开发速度、减少bug 有助于留住人

    2024年02月05日
    浏览(24)
  • 【编程】C++语言编程规范-2

    结合C++ Effective系列参考树、尤其是工程经验教训的总结。 并发 除非必要,尽量少用线程。 多线程编程要守护好内存,使用atomic、mutex、condition variable、future、semaphore、latch、barrier等同步机制避免数据竞争。 尽量缩小临界区,临界区指独占的资源,禁止其他线程访问变量的代

    2024年02月21日
    浏览(37)
  • python 编程规范有哪些?

    Python 编程规范主要包括代码布局、命名规范、注释规范、函数编写规范等多个方面,下面给出一些常见的编程规范及其示例代码。 代码布局规范主要是指代码的缩进、行宽、空行、换行等方面,下面是一些常见的代码布局规范: 使用四个空格作为一个缩进级别,不要使用制

    2024年02月01日
    浏览(24)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包