阅读 1275

掌握这些Go语言特性,你的水平将提高N个档次(二)

前言: 大家好,我是asong,这是我的第二篇原创文章。上一文介绍了切片、变量声明、defer三个知识点(回顾上文,关注公众号即可进行阅读),这一文将继续介绍其他Go语言特性,废话不多说,直接上干货。

1. 指针和引用

在Go语言中只有一种参数传递的规则,那就是值拷贝,其包含两种含义:

  • 函数参数传递时使用的值拷贝
  • 实例赋值给接口变量,接口对实例的引用是值拷贝

我们在使用过程中会发现有时明明是值拷贝的地方,结果却修改了变量的内容,有以下两种情况:

  • 直接传递的是指针。指针传递同样是值拷贝,但指针和指针副本的值指向的地址是同一个地方,所以能修改实参
  • 参数是复合数据类型,这些符合数据类型内部有指针类型的元素,此时参数的值拷贝并不影响指针的指向。

在Go语言中,复合类型chan、map、slice、interface内部都是通过指针指向具体的数据,这些类型的变量在作为函数参数传递时,实际上相当于指针的副本。我们可以通过查看源码,看一看他们的底层数据结构:

  1. map的底层数据结构:
//src/runtime/map.go1.14
// A header for a Go map.
type hmap struct {
   // Note: the format of the hmap is also encoded in cmd/compile/internal/gc/reflect.go.
   // Make sure this stays in sync with the compiler's definition.
   count     int // # live cells == size of map.  Must be first (used by len() builtin)
   flags     uint8
   B         uint8  // log_2 of # of buckets (can hold up to loadFactor * 2^B items)
   noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for details
   hash0     uint32 // hash seed
   buckets    unsafe.Pointer // array of 2^B Buckets. may be nil if count==0.
   oldbuckets unsafe.Pointer // previous bucket array of half the size, non-nil only when growing
   nevacuate  uintptr        // progress counter for evacuation (buckets less than this have been evacuated)
   extra *mapextra // optional fields
}
复制代码

通过源码我们可以分析,其通过buckets指针来间接引用map中的存储结构。 2. slice的底层数据结构:

//src/reflect/value.go1.14
// sliceHeader is a safe version of SliceHeader used within this package.
type sliceHeader struct {
   Data unsafe.Pointer
   Len  int
   Cap  int
}
复制代码

slice则采用uinptr指针指向底层存放数据的数组。 3. interface的底层数据结构如下:

//src/reflect/value.go1.14
// nonEmptyInterface is the header for an interface value with methods.
type nonEmptyInterface struct {
   // see ../runtime/iface.go:/Itab
   itab *struct {
      ityp *rtype // static interface type
      typ  *rtype // dynamic concrete type
      hash uint32 // copy of typ.hash
      _    [4]byte
      fun  [100000]unsafe.Pointer // method table
   }
   word unsafe.Pointer
}
// emptyInterface is the header for an interface{} value.
type emptyInterface struct {
   typ  *rtype
   word unsafe.Pointer
}
复制代码

我们可以看到接口内部通过一个指针指向实例值或地址的副本。 4. chan的底层数据结构如下:

//src/runtime/chan.go1.14
type hchan struct {
   qcount   uint           // total data in the queue
   dataqsiz uint           // size of the circular queue
   buf      unsafe.Pointer // points to an array of dataqsiz elements
   elemsize uint16
   closed   uint32
   elemtype *_type // element type
   sendx    uint   // send index
   recvx    uint   // receive index
   recvq    waitq  // list of recv waiters
   sendq    waitq  // list of send waiters
   // lock protects all fields in hchan, as well as several
   // fields in sudogs blocked on this channel.
   //
   // Do not change another G's status while holding this lock
   // (in particular, do not ready a G), as this can deadlock
   // with stack shrinking.
   lock mutex
}
复制代码

通过源码我们可以看出,通道元素的存放地址由buf指针确定,chan内部的数据也是间接通过指针访问的。

2. 函数

Go语言支持匿名函数,其函数名和匿名函数字面量的值有3层含义:

  • 类型信息,表明其数据类型是函数类型

  • 函数名代表函数的执行代码的起始位置

  • 可以通过函数名进行函数调用,函数调用格式为 func_name(param_list)。在底层执行层面包含以下4部分内容。

  • 准备好参数

  • 修改PC值,跳转到函数代码起始位置开始执行

  • 复制值到函数的返回值栈区

  • 通过RET返回到函数调用的下一条指令处继续执行。

2). 函数的方法设计 我们在开发时,有时内部会实现两个"同名"的函数或方法,一个首字母大写,用于导出API供外部调用;一个首字母小写,用于实现具体逻辑。一般首字母大写的函数调用首字母小写的函数,同时包装一些功能;首字母小写的函数负责更多的底层细节。 大部分情况下我们不需要两个同名且只是首字母大小写不同的函数,只有在函数逻辑很复杂,而且函数在包的内、外部都被调用的情况下,才考虑拆分为两个函数进行实现。一方面减少单个函数的复杂性,另一方面进行调用隔离。

这种编程方法在database/sql库中体现较明显,有兴趣的可以查看这一部分的源码。 3) 多值返回函数设计 Go语言支持多值返回函数,这里不对多值返回函数基础使用进行介绍,这里只介绍多值返回函数的推荐编程风格方法。 多值返回函数里如果有error或bool类型的返回值,则应该将error或bool作为最后一个返回值。这是一种编程风格,没有对错。Go标准库的写法也遵循这样的规则。当大多数人都使用、遵循这种方法时,如果有人不遵循这种"潜规则",则写出的代码会让别人读起来就会很别扭。所以推荐你们开发时这样进行书写。示例如下:

func testBool() (int ,bool){}
func testError() (int,error){}
复制代码

3. 代码风格

Go作为新世纪开发的一门语言,其作者在代码干净上有了近乎苛刻的要求,有如下几方面的体现: 1) 编译器不能通过未使用的局部变量。 2)"import"未使用的包同样通不过编译。 3)所有的控制结构、函数和方法定义的"i"放到行尾,而不能另起一行。 4)提供go fmt工具格式化代码,使所有的代码风格保持统一。 Go支持使用comma,ok表达式 常见的几个comma,ok 表达式如下。

1. 读取chan值 读取已经关闭的通道,不会阻塞,也不会引起panic,而是一直返回该通道的零值。若判断通道是否已经关闭有两种方法:一种是读取通道的comma,ok 表达式,如果通道已经关闭,则ok的返回值是false,另一种就是通过range循环迭代。看下面的示例:

import "fmt"
func main()  {
   c := make(chan int)

   go func() {
      c <- 1
      c <- 2
      close(c)
   }()
   for{
      v,ok := <-c
      if ok{
         fmt.Println(v)
      }else {
         break
      }
   }

   /*
   for v := range c{
      fmt.Println(v)
   }
   */
}
复制代码
  1. 获取map值 获取map中不存在键的值不会发生异常,而是会返回值类型的零值,如果想确定map中是否存在key,则可以使用获取map值的comma,ok语法。示例如下:
import "fmt"
func main()  {
   m := make(map[string]string)

   v,ok := m["test"]
   //通过ok进行判断
   if !ok{
      fmt.Println("m[test] is nil")
   }else {
      fmt.Println("m[test] =",v)
   }
}
复制代码
  1. 类型断言 类型断言,是Go语言中一个难点。有一点难理解。这一文将不详细介绍用法,后面将会专门写一篇文章进行详细的介绍。 接口断言通常可以使用comma,ok语句来确定接口是否绑定某个实例类型,或者判断接口绑定的实例类型是否实现另一个接口。可以看src/net/http/request.go中部分代码如下:
858 rc, ok := body.(io.ReadCloser)
1191 if _, ok := r.Body.(*maxBytesReader); !ok {
复制代码

好啦,本文到此结束啦,基本对Go语言基于其他语言的不同做了一个介绍,因为我也是一个新手,理解的还不是很到位,也在努力学习中,有错误或者有需要更改的地方,请联系我,非常感谢。同时再一次推荐我的公众号:Golang梦工厂,我会不断发表关于Golang方面的知识,面试、个人理解等多个方面,一定对你受益匪浅的。公众号搜索:Golang梦工厂,或者直接扫描下方二维码即可。

在这里插入图片描述