[Golang梦工厂]掌握这些Go语言特性,你的水平将提高N个档次(一)

sunsong1997 · · 1003 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

前言:
这一栏是我个人公众号文章,在这里推荐一下我的公众号,专注于Golang相关技术;Golang面试、Beego、Gin、Mysql、Linux、网络、操作系统等。有需要的小伙伴可以关注一下 ,每天观看优质文章。没有使用这门语音,也可以关注一下,提前了解一下,相信你会爱上这门语言。
添加方式:公众号搜索 Golang梦工厂 或 扫描下方二维码

image

正文:

大家好,我是asong,这是我的第一篇原创文章。今天这篇文章主要来介绍Go语言一些特性。Go语言虽然语法简单,整个设计近乎完美,但是它有一些特性会让初次使用者感到困惑、迷茫。所以本文将介绍Go语言的几个特性,解除困惑,避免犯错。

1. 切片

切片这个概念是Go语言中所特有的,它的提出是为了解决数组的定长性和值拷贝限制了Go使用的场景。我们有过其他语言学的话,会知道C++语言既有引用传递,又有值传递,但是Go语言的参数都是值传递,因此引入切片。Go提供的这种数据类型slice(中文为切片),这是一种变长数组,它在数据结构中有指向数组的指针,所以是一种引用类型。查看底层源码如下:

//src/runtime/slice.go1.14
type slice struct {
  array unsafe.Pointer
  len   int
  cap   int
}

根据源码我们可以分析,Go为切片维护了三个元素----指向底层数组的指针、切片的元素数量和底层数组的容量。当len增长超过cap时,会申请一个更大容量的底层数组,并将数据从老数组复制到新申请的数组中。结构如下图所示:

image
  • nil切片和空切片

我们使用make([]int,0)与var a []int 创建的切片是有区别的。使用前者的切片指针有分配,后者的内部指针为0。来看示例:

import "fmt"
func main(){
  var a []int
  b := make([]int,0)
  if  a == nil{
    fmt.Println("a is nil")
  }else {
    fmt.Println("a is not nil")
  }
 
  if b == nil{
    fmt.Println("b is nil")
  }else {
    fmt.Println("b is not nil")
  }
}
//运行结果如下
a is nil
b is not nil

因此我们可以看出make([]int,0)创建的是一个空切片(底层数组指针非空,但底层数组是空的)。为什么会这样呢,我们查看一下makeslice的源码如下:

func makeslice(et *_type, len, cap int) unsafe.Pointer {
  mem, overflow := math.MulUintptr(et.size, uintptr(cap))
  if overflow || mem > maxAlloc || len < 0 || len > cap {
// NOTE: Produce a 'len out of range' error instead of a
// 'cap out of range' error when someone does make([]T, bignumber).
// 'cap out of range' is true too, but since the cap is only being
// supplied implicitly, saying len is clearer.
// See golang.org/issue/4085.
    mem, overflow := math.MulUintptr(et.size, uintptr(len))
    if overflow || mem > maxAlloc || len < 0 {
      panicmakeslicelen()
    }
    panicmakeslicecap()
  }
  return mallocgc(mem, et, true)
}

调用了mallocgc分配空间,所以make创建的切片是有指针分配的。

  • 多个切片引用同一数组

因为切片可以由底层数组创建,一个底层数组可以创建多个切片,这些切片共享底层数组,使用append扩展切片的过程中可能修改底层数组的元素,间接的影响其他切片的值,也可能发生数组复制重建,共用底层数组的切片,因为可能会造成问题,所以不推荐使用。示例如下:

func main(){
  array := []int{0,1,2,3,4,5,6}
  sl := array[0:4]
  as := (*reflect.SliceHeader)(unsafe.Pointer(&array))
  bs := (*reflect.SliceHeader)(unsafe.Pointer(&sl))
  fmt.Printf("array=%v,len=%d,cap=%d,type=%d\n",array,len(array),cap(array),as.Data)
  fmt.Printf("sl=%v,len=%d,cap=%d,type=%d\n",sl,len(sl),cap(sl),bs.Data)
  sl = append(sl,10,11,12)
  fmt.Printf("array=%v,len=%d,cap=%d\n",array,len(array),cap(array))
  fmt.Printf("sl=%v,len=%d,cap=%d\n",sl,len(sl),cap(sl))
  sl = append(sl,13,14)
  as = (*reflect.SliceHeader)(unsafe.Pointer(&array))
  bs = (*reflect.SliceHeader)(unsafe.Pointer(&sl))
  fmt.Printf("array=%v,len=%d,cap=%d,type=%d\n",array,len(array),cap(array),as.Data)
  fmt.Printf("sl=%v,len=%d,cap=%d,type=%d\n",sl,len(sl),cap(sl),bs.Data)
}
//运行结果
array=[0 1 2 3 4 5 6],len=7,cap=7,type=824633787392
sl=[0 1 2 3],len=4,cap=7,type=824633787392
array=[0 1 2 3 10 11 12],len=7,cap=7
sl=[0 1 2 3 10 11 12],len=7,cap=7
array=[0 1 2 3 10 11 12],len=7,cap=7,type=824633787392
sl=[0 1 2 3 10 11 12 13 14],len=9,cap=14,type=824633811280

从上面这个例子,我们就可以看出指针已经发生变化了。所以我们可以得出以下结论:

append追加的元素没有超过底层数组的容量,此种append操作会直接操作共享的底层数组,如果其他切片有引用数组被覆盖的元素,则会导致其他切片的值也隐式地发生变化。如果使用append追加的元素如果超出底层数组的容量,则此种append操作会重新申请数组,并将原来数组复制到新数组。

所以我们在开发中要注意这种用法,如果需要复制可以使用copy进行显式复制。切片在我们开发中使用比较多,需要我们好好注意一下。

2. 赋值与变量声明

Go语言支持多值赋值,并且在函数或方法内部支持短变量声明并赋值,同时Go语言依据类型字面量的值能够自动进行类型推断。使用好这些特性,将大大提高我们的开发效率。

  • 多值赋值
    多值赋值就是可以一次性声明多个变量,并且在声明时赋值,可省略类型。示例如下:
var x,y int
var x,y int = 1,2
var x,y = 1,2
var x,y = 1,"test"
var (
  x int 
  y string
)

多值赋值有两种格式:

第一种:右边是一个返回值的表达式,可以是返回多值的函数调用,也可以是range对map、slice等函数的操作,还可以是类型断言。示例如下:

x,y = function()
for k,v:=range map{}
v,ok := i.(x)

赋值的左边操作数和右边的单一返回值的表达式的个数一样,逐个从左向右依次对左边的操作数赋值,例如:

x,y,z = a,b,c
  • 短变量的声明和赋值
    这是Go语言所特有的。短变量的声明和赋值是指在Go函数或类型方法内部使用":="声明并初始化变量,支持多值赋值。短变量的声明和赋值的语言要符合如下要求:

  • 使用":="操作符,变量的定义和初始化同时完成

  • 变量名后不能跟任何类型名,Go编译器完全靠右边的值进行推导。

  • 支持多值短变量声明赋值

  • 只能在函数和类型方法的内部

短变量的声明和赋值中最容易产生歧义的地方就是多值短变量的声明和赋值,这个问题的根源是Go语言的语法允许多值短变量声明和赋值的多个变量中,只要有一个是新变量就可以使用":="进行赋值。也就说,在多值短变量的声明和赋值时,至少有一个变量是新创建的局部变量,其他的变量可以复用以前的变量,不是新创建的变量执行的仅仅是赋值。示例如下:

package main
var num int
func res() (int,error){
    return 1, nil
}
func readGloabl(){
    fmt.Println(num)
}
func main() {
    num,_:=res()
    readGloabl()
    fmt.Println(n)
}

根据示例我们可以分析复用已存变量的规则,分析a,b:=va,vb

  1. 如果想编译通过,则a和b中至少要有一个是新的定义的局部变量。所以不能同时预先声明a,b两个局部变量。
  2. 在该赋值语句a,b:=va,vb中已经存在一个局部变量a,则赋值语句a,b:=va,vb不会创建新变量a,而是直接使用va赋值给已经声明的局部变量a,但是会创建新变量b,并将vb赋值给b。
  3. 如果在赋值语句a,b:=va,vb中没有局部变量a和b,但在全局命名空间有变量a和b,则该语句会创建新的局部变量a和b并使用va,vb初始化他们。此时赋值语句所在的局部作用域类内,全局的a和b被屏蔽。
    我们对操作符"=“和”:="做一个区别对比:
  • "="不会声明并创建新变量,而是在当前赋值语句所在的作用域由内向外逐层去搜寻变量,如果没有搜索到相同变量名,则报编译错误。
  • ”:=“必须出现在函数或类型方法内部。
  • ":="至少要创建一个局部变量并初始化。

3. defer

defer是Go语言中提供的关键字,可以注册多个延迟调用,,这些调用可以先进后出的顺序在函数返回前被执行。使用defer可以方便我们的开发,但同时要注意它的副作用,下面对几个要注意的点进行介绍讲解。

  • defer和函数返回值

defer中如果引用了函数的返回值,则会因引用形式不同导致不同的结果,这些结果会造成很大问题困扰,下面我们先看一下示例:

package main
import "fmt"
func func1()  (r int){
  defer func(){
    r++
  }()
  return 0
}
func func2()  (r int){
  t := 5
  defer func() {
    t = t+5
  }()
  return t
}
func func3() (r int){
  defer func(r int) {
    r = r+5
  }(r)
  return 1
}
func main(){
  fmt.Println("func1=",func1())
  fmt.Println("func2=",func2())
  fmt.Println("func3=",func3())
}
//运行结果
1
5
1

根据结果我们进行分析,func1、func2、func3这三个函数的共同点就是带命名返回值的函数,返回值都是变量r。我们知道函数调用方负责开辟栈空间,包括形参和返回值的空间。有名的函数返回值相当于函数的局部变量,被初始化为类型的零值。所以我们可以分析func1函数如下:

  • r是函数的有名返回值,分配在栈上,其地址又被称为返回值所在栈区。首先r被初始化为0.

  • "return 0"会复制0到返回值栈区,返回值r被赋值为0。
    执行defer语句,由于匿名函数对返回值r是闭包引用,所以r++执行后,函数返回值被修改为1。

  • defer语句执行完后RET返回,此时函数的返回值仍然为1。
    画图分析如下:

image

同样道理分析func2的逻辑:

  • 返回值r被初始化为0。

  • 引入局部变量t,并初始化为5。

  • 复制t的值5到返回值r所在的栈区

  • defer语句后面的匿名函数是对局部变量t的闭包引用,t的值被设置为10.

  • 函数返回,此时函数返回值栈区上的值仍然是5.

画图分析如下:

image

最后我们分析func3的逻辑:

  • 返回值r被初始化为0.

  • 复制1到函数返回值r所在的栈区

  • 执行defer,defer后匿名函数使用的是传参数调用,在注册defer函数是将函数返回值r作为实参传进去,由于函数调用的是值拷贝,所以defer函数执行后只是形参值变为5,对实参没有任何影响。

  • 函数返回,此时函数返回值栈区上的值是1。

画图分析如下:

image

通过对以上三个函数的分析,我们可以对带defer的函数返回整体进行总结,有如下三个步骤:

  • 执行return的值拷贝,将return语句返回的值复制到函数返回值栈区(如果只有一个return,不带任何变量或值,则此步骤不做任何动作)

  • 执行defer语句,多个defer按照FILO顺序执行。

  • 执行调整RET指令。

上面的例子是在defer中修改函数返回值不是一种明智的编程方法,所以在实际编程中应尽可能避免此种情况。还有一种方法可以解决这个问题,在定义函数时使用不带返回值名的格式。通过这种方式,defer就不能引用返回值的栈区,也就避免了返回值被修改的问题。

今天暂时分享这三大特性,后续将继续分享Go语言其他陷阱,想获取更多学习内容,请关注公众号,更多精彩内容将持续奉上。

image

有疑问加站长微信联系(非本文作者)

本文来自:简书

感谢作者:sunsong1997

查看原文:[Golang梦工厂]掌握这些Go语言特性,你的水平将提高N个档次(一)

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

1003 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传