百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 编程字典 > 正文

哦,原来是这么回事:Golang 中的一些常识

toyiye 2024-07-03 02:00 15 浏览 0 评论

前言

回想起来使用Go已三年有余,有很多踩过的坑。Go是门活力四射的语言,语法简单但表述能力强大且足够高效,但是也有很多细微的点,这些点就是一些基本细节实现,如果能注意这些细节,我相信我们能够对Go的理解能更深一些,写的bug会少一些。

作为一名初学者我们时常写对一些固定的写法,不知道为什么要这么写;我们时常写了一些bug,不知道为什么bug;我们时常知道可以这么写,但是不知道那样写是否可以;有时候我们很懒,懒得去测试是否可以,有时候我们很勤快,测试了并且知道答案,但是不求甚解;

再深一点?很多时候我们不求甚解,这天杀的产品经理又在催好像是个不错的借口。慢慢地又觉得自己理解不够深刻,所以总是闲暇的时候思考这些问题。我相信在二进制的世界里,nothing is magic, 一定是有Why的,因为这是我们所创造的世界 ( AI算法除外 ) 。

希望这篇文章能够帮到你,哪怕只是一点点。

Common Sense in Go

1. interface{} 后面是有{}的

现象

go中其他的类型都是没有{}的, 只有interface{}有。

理解

go中其他的类型都是没有{} 的 比如 map[int]int, 但是interface{}都是带{}的,据说是为了让你瞅瞅里边什么也没有。

2. 函数参数是值传递的(Passed by value)

现象

函数的参数是值传递,且在调用的时立即执行值拷贝的。

理解

首先,函数调用是值传递的。

所以无论传递什么参数都会被copy到函数的参数变量的内存地址中,堆或者栈上,具体是堆还是栈上涉及到逃逸问题,这里不做过多分析。但是毫无疑问的是,在调用时立即对变量进行了Copy,以下例子中通过打印变量地址佐证。

func main() {
    var i int
    fmt.Printf("main: %p\n", &i)
    foo(i)
}

func foo(i int)  {
    fmt.Printf("foo : %p\n", &i)
}
// 输出的变量地址不一样
main: 0xc0000a0008
foo : 0xc0000a0018

所以对于复杂结构我们应该尽量的传递指针减少copy时的开销。对于这里有看到不同的观点,主要是考虑到空指针问题,但是我仍然觉得应该使用指针。理由主要有以下几点

  • 值传递会Copy对象,对于小内容对象,性能相差不大,但是在大结构下存在明显的性能损耗
  • return 的时候可以直接 return nil, err,代码精简更加优雅
  • nil pointer panic 应该通过error handling来解决,不然即使没有发生panic,也会执行错误的逻辑,引入更多的问题。

但是指针传递的同时也带来变量逃逸,和GC压力,也是一把双刃剑,好在大部分情况下不需要特别的对GC进行调优。所以,在make it simple的理念下,在需要时再针对性调优是个不错的选择。

所以什么时候我们应该传递值,什么时候应该传递指针,这主要取决于copy开销和是否需要在函数内部对变量值进行更改。我们可以用一个简单的例子测试下两者的性能差距:

func passedByValue(foo Value) {
    foo.C = "1"
}

func passedByPointer(bar *Value) {
    bar.C = "1"
}

// 值传递
func Benchmark_PassedByValue(b *testing.B) {
    var val Value
    str := bytes.Buffer{}
    // 这里为了构建一个大值进行传递,小值因为copy代价太小性能差距不明显。
    for i:=0; i < 10000000; i ++ {
        str.Write([]byte("====="))
    }
    val.C = str.String()

    for i := 0; i < b.N; i++ {
        passedByValue(val)
    }
}
// 指针传递
func Benchmark_PassedByPointer(b *testing.B) {
    var val = new(Value)
    str := bytes.Buffer{}
    for i:=0; i < 10000000; i ++ {
        str.Write([]byte("====="))
    }
    val.C = str.String()
    for i := 0; i < b.N; i++ {
        passedByPointer(val)
    }
}

// Benchmark结果差距也很明显,但是一般值的copy代价都比较小,差距不明显。
goos: darwin
goarch: amd64
pkg: demo/go
Benchmark_PassedByValue-4       1000000000               0.676 ns/op
Benchmark_PassedByPointer-4     1000000000               0.383 ns/op
PASS

一般来说,基本类型我们都应该传值,自定义类型中一般内容不可控,所以养成良好的习惯很关键。特别注意的是slice、map、ctx是引用值类型,所以copy时并没有copy其中数据,所以一般也进行值传递,除非你要对其中更改其中的元素。但如果你需要更改其中的内容,还是建议更改完尽量返回回来一个新的,像内置的append函数一样,通过返回新的地址来实现。这样会更加清晰一些,写代码时自己尽量不要和自己过不去。

举个栗子,以下代码可能是一个bug:

func main() {
    var ids []int
    appendSlice(ids)
    fmt.Println("main", len(ids))
}

func appendSlice(ids []int) {
    for i := 0; i < 4; i++ {
        ids = append(ids, i)
    }
    fmt.Println("appendSlice", len(ids))
}

// 输出, 因为appendSlice中的ids并不是main中的ids.
appendSlice 4
main 0

其次,Copy发生在函数调用的时候。比如利用这个原理就可以使用以下代码打印函数耗时。

func do(){

    // 因为 defer 语句执行的时候已经将函数参数转储,只是函数体执行时机有所调整
    defer func(t time.Time) {
       fmt.Println("do Cost: "time.Slice(t).Second())
    }(time.Now())
    
    // balabalabala
}

3. for _, i := range ss, ss 中的元素是 copy 到 变量i 的

现象

for range 的时候 slice 中的元素是copy给 变量i的,并且下次for循环变量i会被直接覆盖。并不是把 n号元素的地址给了ii 是第n 号元素的 copy。

理解

Copy会产生两个变量,i 是个临时变量,下一次for循环就会被覆写,而且因为是临时值,所以以下代码因为更改也不生效,也是非常常见的bug。

type User struct {
    Uid int
}

func main() {
    users := []User{
        {Uid: 1}, {Uid: 2},
    }
    for idx, i := range users {
        i.Uid = 2
        fmt.Printf("i=%p, user_%d=%p\n", &i, idx, &users[idx])
    }
    fmt.Println(users[0].Uid)
}

// 输出
// i 的地址不变,并且不是元素的地址
i=0xc00008c008, user_0=0xc00008c010
i=0xc00008c008, user_1=0xc00008c018
1 // 原数组中的user id并没有发生改变

要更改生效也很简单,主要有两种方案,一种是使用切片指针 []*User,这样对于i的修改会被自动寻址到数字元素上。另一种是使用下标 主动寻址如 users[idx].Uid = 2 。至于[]T还是[]*T 的问题我们接下来再讨论。

这个问题看似简单,如果将其使用go关键字并发将会发生巨大威力,造成血淋淋的事故。

其实用go的公司经常听到这样的事故:

  • 某公司发运营push全部发给了同一个uid
  • 某研发发运营消息发短信发给了同一个uid (如果通道商不限制,我相信用户哭了,哄不好的那种)
  • 批量发优惠券,给同一个uid发了几百张
  • ....

闭包问题一点都不新鲜,就是由于在go func里边使用for了循环的变量i了,然后因为函数体并没在go的时候立即执行需要申请资源挂载然后由M进行运行需要一些时间,所以一般for循环执行一段时间之后go func才会执行,这时候 内部函数取到的值就得听天命了。

经典bug复现

func main() {
    for _, i := range []int{1, 2, 3} {
        go func() {
            println(i)
        }()
    }
    time.Sleep(1* time.Millisecond)
}
// 只会打印 3, 因为等到func执行的时候 i已经变成3了
// 所以把 i 当做 匿名函数的参数传进去或者在for中重新定义一个变量是个不错的做法
3
3
3

所以,使用匿名函数的时候go func的时候要时刻注意循环变量的Scope, 该传参传参,该重新定义重新定义。好在 Goland 最新版本已经会提示i存在Scope问题了。但是好像没几个人会注意IDE警告,所以,习惯很重要,不要写出IDE警告的代码也是一个不错的编程理念。

4. []T 还是 []*T

现象

一般来说[]T 会比较高效一些,但是如果T比较大,在For循环时存在Copy开销,个人觉得[]*T也是可以的。

5. []interface{}并不能接收[]T类型

现象

很多时候我们都以为interface可以传递任意类型,凡事总有例外,他就不能接收 []T 类型, 如果你需要进行赋值,那你要将T转成interface{}

理解

因为一个[]interface{}的空间是一定的,但是 []T 不是,因为占用空间不一致,编译器觉得有些代价,并没有进行转换.

6. Send on closed chan 会Panic,但是 Receive from closed chan 不会

现象

往已经关闭的channel 再send数据会触发runtime panic,但是receive从已经关闭的channel中消费不会触发.

理解

很多人有误区,认为chan关闭了就不能再操作了,但是send进chan的数据总归要消费完的,不然就丢了,你品。

7. Goroutine 之间不能 Recover painc

现象

goroutine没有父子关系(创建应该不算父子吧),不能在一个go中 recover 另一个 go 的 panic

理解

GPM模型在go的调度时没有上下级关系, 也没有跨goroutine的异常捕获机制。

8. error 是一个实现了Error()string 方法的任意类型.

现象

error 被定义为 interface{ Error()string },只要实现该方法的类型,其值都可以认为是error

9. 是否实现某个interface的的判断是区别对待 *T 和 T 的

现象

一个接口实现必须实现接口定义的全部方法,使用 指针类型的receiver 和 值类型的 receiver 是两个不同的实现。

解释

*张三不吃香菜,不等于张三不吃香菜。

type User interface {
    Eat(food interface{}) (bool, error)
}

type ZhangSan struct {
    Name string
}

// *ZhangSan 实现了 User 接口
// 但是 ZhangSan 没有实现
func (*ZhangSan) Eat(food interface{}) (bool, error) {
    if food == "香菜" {
        return false, nil
    }
    return true, nil
}

func userEat(u User,food string) (bool, error){
    return u.Eat(food)
}


func main() {
    someone := ZhangSan{Name: "张三"}
    
    // 这里 someone 是不能传递给 userEat 的
    // 因为 ZhangSan 这个结构没有实现 User 接口, 只能用 &ZhangSan进行传递。
    // userEat(someone, "花生")
    userEat(&someone, "花生")
}

所以,实现接口时receiver类型要统一。

10. Reveiver 在函数调用时其实是作为函数第一参数传递给函数的

现象

receiver 是可以为 nil 的

解释

如果你细心看过panic的日志就会发现,打印日志的时候 receiver其实是作为函数第一参数传递的。所以,你可以在method中对receiver进行空值判断,来防止panic的发生。

func main() {
    var someone *ZhangSan
    _, _ = someone.Eat("花生")
}
// 如果在Eat 中没有对 receiver进行空值判断也可能引发 空指针异常
goroutine 1 [running]:
main.(*ZhangSan).Eat(0x0, 0x10aafc0, 0x10e9680, 0x0, 0x10a9ec0, 0xc0000200b8)
        /Users/haoliu/demo/go/main.go:16 +0x26
main.main()
        /Users/haoliu/demo/go/main.go:30 +0x42

总结

以上就Go在日常使用过程中的基本点进行了一下总结,是golang日常使用过程中经常碰到的点。由于水平有限,如果存在某些表述不清楚的地方,可以一起讨论下。

作者:保护我方李元芳,授权发布

链接:https://juejin.im/post/6881267557346344974

来源:掘金 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。


相关推荐

为何越来越多的编程语言使用JSON(为什么编程)

JSON是JavascriptObjectNotation的缩写,意思是Javascript对象表示法,是一种易于人类阅读和对编程友好的文本数据传递方法,是JavaScript语言规范定义的一个子...

何时在数据库中使用 JSON(数据库用json格式存储)

在本文中,您将了解何时应考虑将JSON数据类型添加到表中以及何时应避免使用它们。每天?分享?最新?软件?开发?,Devops,敏捷?,测试?以及?项目?管理?最新?,最热门?的?文章?,每天?花?...

MySQL 从零开始:05 数据类型(mysql数据类型有哪些,并举例)

前面的讲解中已经接触到了表的创建,表的创建是对字段的声明,比如:上述语句声明了字段的名称、类型、所占空间、默认值和是否可以为空等信息。其中的int、varchar、char和decimal都...

JSON对象花样进阶(json格式对象)

一、引言在现代Web开发中,JSON(JavaScriptObjectNotation)已经成为数据交换的标准格式。无论是从前端向后端发送数据,还是从后端接收数据,JSON都是不可或缺的一部分。...

深入理解 JSON 和 Form-data(json和formdata提交区别)

在讨论现代网络开发与API设计的语境下,理解客户端和服务器间如何有效且可靠地交换数据变得尤为关键。这里,特别值得关注的是两种主流数据格式:...

JSON 语法(json 语法 priority)

JSON语法是JavaScript语法的子集。JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔花括号保存对象方括号保存数组JS...

JSON语法详解(json的语法规则)

JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔大括号保存对象中括号保存数组注意:json的key是字符串,且必须是双引号,不能是单引号...

MySQL JSON数据类型操作(mysql的json)

概述mysql自5.7.8版本开始,就支持了json结构的数据存储和查询,这表明了mysql也在不断的学习和增加nosql数据库的有点。但mysql毕竟是关系型数据库,在处理json这种非结构化的数据...

JSON的数据模式(json数据格式示例)

像XML模式一样,JSON数据格式也有Schema,这是一个基于JSON格式的规范。JSON模式也以JSON格式编写。它用于验证JSON数据。JSON模式示例以下代码显示了基本的JSON模式。{"...

前端学习——JSON格式详解(后端json格式)

JSON(JavaScriptObjectNotation)是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。它基于JavaScriptProgrammingLa...

什么是 JSON:详解 JSON 及其优势(什么叫json)

现在程序员还有谁不知道JSON吗?无论对于前端还是后端,JSON都是一种常见的数据格式。那么JSON到底是什么呢?JSON的定义...

PostgreSQL JSON 类型:处理结构化数据

PostgreSQL提供JSON类型,以存储结构化数据。JSON是一种开放的数据格式,可用于存储各种类型的值。什么是JSON类型?JSON类型表示JSON(JavaScriptO...

JavaScript:JSON、三种包装类(javascript 包)

JOSN:我们希望可以将一个对象在不同的语言中进行传递,以达到通信的目的,最佳方式就是将一个对象转换为字符串的形式JSON(JavaScriptObjectNotation)-JS的对象表示法...

Python数据分析 只要1分钟 教你玩转JSON 全程干货

Json简介:Json,全名JavaScriptObjectNotation,JSON(JavaScriptObjectNotation(记号、标记))是一种轻量级的数据交换格式。它基于J...

比较一下JSON与XML两种数据格式?(json和xml哪个好)

JSON(JavaScriptObjectNotation)和XML(eXtensibleMarkupLanguage)是在日常开发中比较常用的两种数据格式,它们主要的作用就是用来进行数据的传...

取消回复欢迎 发表评论:

请填写验证码