16

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

 4 years ago
source link: https://studygolang.com/articles/28514
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

前言: 大家好,我是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梦工厂,或者直接扫描下方二维码即可。

VZruE33.jpg!web

欢迎关注我们的微信公众号,每天学习Go知识

FveQFjN.jpg!web

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK