前言: 大家好,我是asong,这是我的第二篇原创文章。上一文介绍了切片、变量声明、defer三个知识点(回顾上文,关注公众号即可进行阅读),这一文将继续介绍其他Go语言特性,废话不多说,直接上干货。
1. 指针和引用
在Go语言中只有一种参数传递的规则,那就是值拷贝,其包含两种含义:
- 函数参数传递时使用的值拷贝
- 实例赋值给接口变量,接口对实例的引用是值拷贝
我们在使用过程中会发现有时明明是值拷贝的地方,结果却修改了变量的内容,有以下两种情况:
- 直接传递的是指针。指针传递同样是值拷贝,但指针和指针副本的值指向的地址是同一个地方,所以能修改实参
- 参数是复合数据类型,这些符合数据类型内部有指针类型的元素,此时参数的值拷贝并不影响指针的指向。
在Go语言中,复合类型chan、map、slice、interface内部都是通过指针指向具体的数据,这些类型的变量在作为函数参数传递时,实际上相当于指针的副本。我们可以通过查看源码,看一看他们的底层数据结构:
- 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)
}
*/
}
复制代码
- 获取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)
}
}
复制代码
- 类型断言 类型断言,是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梦工厂,或者直接扫描下方二维码即可。
有疑问加站长微信联系(非本文作者)