14 内存分配:new 还是 make?什么情况下该用谁?

程序的运行都需要内存,比如像变量的创建、函数的调用、数据的计算等。所以在需要内存的时候就要申请内存,进行内存分配。在 C/C++ 这类语言中,内存是由开发者自己管理的,需要主动申请和释放,而在 Go 语言中则是由该语言自己管理的,开发者不用做太多干涉,只需要声明变量,Go 语言就会根据变量的类型自动分配相应的内存。

Go 语言程序所管理的虚拟内存空间会被分为两部分:堆内存和栈内存。栈内存主要由 Go 语言来管理,开发者无法干涉太多,堆内存才是我们开发者发挥能力的舞台,因为程序的数据大部分分配在堆内存上,一个程序的大部分内存占用也是在堆内存上。

小提示:我们常说的 Go 语言的内存垃圾回收是针对堆内存的垃圾回收。

变量的声明、初始化就涉及内存的分配,比如声明变量会用到 var 关键字,如果要对变量初始化,就会用到 = 赋值运算符。除此之外还可以使用内置函数 new 和 make,这两个函数你在前面的课程中已经见过,它们的功能非常相似,但你可能还是比较迷惑,所以这节课我会基于内存分配,进而引出内置函数 new 和 make,为你讲解他们的不同,以及使用场景。

变量

一个数据类型,在声明初始化后都会赋值给一个变量,变量存储了程序运行所需的数据。

变量的声明

和前面课程讲的一样,如果要单纯声明一个变量,可以通过 var 关键字,如下所示:

var s string

该示例只是声明了一个变量 s,类型为 string,并没有对它进行初始化,所以它的值为 string 的零值,也就是 ""(空字符串)。

上节课你已经知道 string 其实是个值类型,现在我们来声明一个指针类型的变量试试,如下所示:

var sp *string

发现也是可以的,但是它同样没有被初始化,所以它的值是 *string 类型的零值,也就是 nil。

变量的赋值

变量可以通过 = 运算符赋值,也就是修改变量的值。如果在声明一个变量的时候就给这个变量赋值,这种操作就称为变量的初始化。如果要对一个变量初始化,可以有三种办法。

  1. 声明时直接初始化,比如 var s string = "飞雪无情"。
  2. 声明后再进行初始化,比如 s="飞雪无情"(假设已经声明变量 s)。
  3. 使用 := 简单声明,比如 s:="飞雪无情"。

小提示:变量的初始化也是一种赋值,只不过它发生在变量声明的时候,时机最靠前。也就是说,当你获得这个变量时,它就已经被赋值了。

现在我们就对上面示例中的变量 s 进行赋值,示例代码如下:

ch14/main.go

func main() {
   var s string
   s = "张三"
   fmt.Println(s)
}

运行以上代码,可以正常打印出张三,说明值类型的变量没有初始化时,直接赋值是没有问题的。那么对于指针类型的变量呢?

在下面的示例代码中,我声明了一个指针类型的变量 sp,然后把该变量的值修改为“飞雪无情”。

ch14/main.go

func main() {
   var sp *string
   *sp = "飞雪无情"
   fmt.Println(*sp)
}

运行这些代码,你会看到如下错误信息:

panic: runtime error: invalid memory address or nil pointer dereference

panic: runtime error: invalid memory address or nil pointer dereference

这是因为指针类型的变量如果没有分配内存,就默认是零值 nil,它没有指向的内存,所以无法使用,强行使用就会得到以上 nil 指针错误。

而对于值类型来说,即使只声明一个变量,没有对其初始化,该变量也会有分配好的内存。

在下面的示例中,我声明了一个变量 s,并没有对其初始化,但是可以通过 &s 获取它的内存地址。这其实是 Go 语言帮我们做的,可以直接使用。

func main() {
   var s string
   fmt.Printf("%p\n",&s)
}

还记得我们在讲并发的时候,使用 var wg sync.WaitGroup 声明的变量 wg 吗?现在你应该知道为什么不进行初始化也可以直接使用了吧?因为 sync.WaitGroup 是一个 struct 结构体,是一个值类型,Go 语言自动分配了内存,所以可以直接使用,不会报 nil 异常。

于是可以得到结论:如果要对一个变量赋值,这个变量必须有对应的分配好的内存,这样才可以对这块内存操作,完成赋值的目的

小提示:其实不止赋值操作,对于指针变量,如果没有分配内存,取值操作一样会报 nil 异常,因为没有可以操作的内存。

所以一个变量必须要经过声明、内存分配才能赋值,才可以在声明的时候进行初始化。指针类型在声明的时候,Go 语言并没有自动分配内存,所以不能对其进行赋值操作,这和值类型不一样。

小提示:map 和 chan 也一样,因为它们本质上也是指针类型。

new 函数

既然我们已经知道了声明的指针变量默认是没有分配内存的,那么给它分配一块就可以了。于是就需要今天的主角之一 new 函数出场了,对于上面的例子,可以使用 new 函数进行如下改造:

ch14/main.go

func main() {
   var sp *string
   sp = new(string)//关键点
   *sp = "飞雪无情"
   fmt.Println(*sp)
}

以上代码的关键点在于通过内置的 new 函数生成了一个 *string,并赋值给了变量 sp。现在再运行程序就正常了。

内置函数 new 的作用是什么呢?可以通过它的源代码定义分析,如下所示:

// The new built-in function allocates memory. The first argument is a type,
// not a value, and the value returned is a pointer to a newly
// allocated zero value of that type.
func new(Type) *Type

它的作用就是根据传入的类型申请一块内存,然后返回指向这块内存的指针,指针指向的数据就是该类型的零值。

比如传入的类型是 string,那么返回的就是 string 指针,这个 string 指针指向的数据就是空字符串,如下所示:

sp1 = new(string)
   fmt.Println(*sp1)//打印空字符串,也就是string的零值。

通过 new 函数分配内存并返回指向该内存的指针后,就可以通过该指针对这块内存进行赋值、取值等操作。

变量初始化

当声明了一些类型的变量时,这些变量的零值并不能满足我们的要求,这时就需要在变量声明的同时进行赋值(修改变量的值),这个过程称为变量的初始化。

下面的示例就是 string 类型的变量初始化,因为它的零值(空字符串)不能满足需要,所以需要在声明的时候就初始化为“飞雪无情”。

var s string = "飞雪无情"
s1:="飞雪无情"

不止基础类型可以通过以上这种字面量的方式进行初始化,复合类型也可以,比如之前课程示例中的 person 结构体,如下所示:

type person struct {
   name string
   age int
}
func main() {
   //字面量初始化
   p:=person{name: "张三",age: 18}
}

该示例代码就是在声明这个 p 变量的时候,把它的 name 初始化为张三,age 初始化为 18。

指针变量初始化

在上个小节中,你已经知道了 new 函数可以申请内存并返回一个指向该内存的指针,但是这块内存中数据的值默认是该类型的零值,在一些情况下并不满足业务需求。比如我想得到一个 *person 类型的指针,并且它的 name 是飞雪无情、age 是 20,但是 new 函数只有一个类型参数,并没有初始化值的参数,此时该怎么办呢?

要达到这个目的,你可以自定义一个函数,对指针变量进行初始化,如下所示:

ch14/main.go

func NewPerson() *person{
   p:=new(person)
   p.name = "飞雪无情"
   p.age = 20
   return p
}

还记得前面课程讲的工厂函数吗?这个代码示例中的 NewPerson 函数就是工厂函数,除了使用 new 函数创建一个 person 指针外,还对它进行了赋值,也就是初始化。这样 NewPerson 函数的使用者就会得到一个 name 为飞雪无情、age 为 20 的 *person 类型的指针,通过 NewPerson 函数做一层包装,把内存分配(new 函数)和初始化(赋值)都完成了。

下面的代码就是使用 NewPerson 函数的示例,它通过打印 *pp 指向的数据值,来验证 name 是否是飞雪无情,age 是否是 20。

pp:=NewPerson()
fmt.Println("name为",pp.name,",age为",pp.age)

为了让自定义的工厂函数 NewPerson 更加通用,我把它改造一下,让它可以接受 name 和 age 参数,如下所示:

ch14/main.go

pp:=NewPerson("飞雪无情",20)
func NewPerson(name string,age int) *person{
   p:=new(person)
   p.name = name
   p.age = age
   return p
}

这些代码的效果和刚刚的示例一样,但是 NewPerson 函数更通用,因为你可以传递不同的参数,构建出不同的 *person 变量。

make 函数

铺垫了这么多,终于讲到今天的第二个主角 make 函数了。在上节课中你已经知道,在使用 make 函数创建 map 的时候,其实调用的是 makemap 函数,如下所示:

src/runtime/map.go

// makemap implements Go map creation for make(map[k]v, hint).
func makemap(t *maptype, hint int, h *hmap) *hmap{
  //省略无关代码
}

makemap 函数返回的是 *hmap 类型,而 hmap 是一个结构体,它的定义如下面的代码所示:

src/runtime/map.go

// 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
}

可以看到,我们平时使用的 map 关键字其实非常复杂,它包含 map 的大小 count、存储桶 buckets 等。要想使用这样的 hmap,不是简单地通过 new 函数返回一个 *hmap 就可以,还需要对其进行初始化,这就是 make 函数要做的事情,如下所示:

m:=make(map[string]int,10)

是不是发现 make 函数和上一小节中自定义的 NewPerson 函数很像?其实 make 函数就是 map 类型的工厂函数,它可以根据传递它的 K-V 键值对类型,创建不同类型的 map,同时可以初始化 map 的大小。

小提示:make 函数不只是 map 类型的工厂函数,还是 chan、slice 的工厂函数。它同时可以用于 slice、chan 和 map 这三种类型的初始化。

总结

通过这节课的讲解,相信你已经理解了函数 new 和 make 的区别,现在我再来总结一下。

new 函数只用于分配内存,并且把内存清零,也就是返回一个指向对应类型零值的指针。new 函数一般用于需要显式地返回指针的情况,不是太常用。

make 函数只用于 slice、chan 和 map 这三种内置类型的创建和初始化,因为这三种类型的结构比较复杂,比如 slice 要提前初始化好内部元素的类型,slice 的长度和容量等,这样才可以更好地使用它们。

在这节课的最后,给你留一个练习题:使用 make 函数创建 slice,并且使用不同的长度和容量作为参数,看看它们的效果。

下节课我将介绍“运行时反射:字符串和结构体之间如何转换?”记得来听课!


15 运行时反射:字符串和结构体之间如何转换?

我们在开发中会接触很多字符串和结构体之间的转换,尤其是在调用 API 的时候,你需要把 API 返回的 JSON 字符串转换为 struct 结构体,便于操作。那么一个 JSON 字符串是如何转换为 struct 结构体的呢?这就需要用到反射的知识,这节课我会基于字符串和结构体之间的转换,一步步地为你揭开 Go 语言运行时反射的面纱。

反射是什么?

和 Java 语言一样,Go 语言也有运行时反射,这为我们提供了一种可以在运行时操作任意类型对象的能力。比如查看一个接口变量的具体类型、看看一个结构体有多少字段、修改某个字段的值等。

Go 语言是静态编译类语言,比如在定义一个变量的时候,已经知道了它是什么类型,那么为什么还需要反射呢?这是因为有些事情只有在运行时才知道。比如你定义了一个函数,它有一个**interface{}**类型的参数,这也就意味着调用者可以传递任何类型的参数给这个函数。在这种情况下,如果你想知道调用者传递的是什么类型的参数,就需要用到反射。如果你想知道一个结构体有哪些字段和方法,也需要反射。

还是以我常用的函数 fmt.Println 为例,如下所示:

src/fmt/print.go

func Println(a ...interface{}) (n int, err error) {
   return Fprintln(os.Stdout, a...)
}

例子中 fmt.Println 的源代码有一个可变参数,类型为 interface{},这意味着你可以传递零个或者多个任意类型参数给它,都能被正确打印。

reflect.Value 和 reflect.Type

在 Go 语言的反射定义中,任何接口都由两部分组成:接口的具体类型,以及具体类型对应的值。比如 var i int = 3,因为 interface{} 可以表示任何类型,所以变量 i 可以转为 interface{}。你可以把变量 i 当成一个接口,那么这个变量在 Go 反射中的表示就是 <Value,Type>。其中 Value 为变量的值,即 3,而 Type 为变量的类型,即 int。

小提示:interface{} 是空接口,可以表示任何类型,也就是说你可以把任何类型转换为空接口,它通常用于反射、类型断言,以减少重复代码,简化编程。

在 Go 反射中,标准库为我们提供了两种类型 reflect.Value 和 reflect.Type 来分别表示变量的值和类型,并且提供了两个函数 reflect.ValueOf 和 reflect.TypeOf 分别获取任意对象的 reflect.Value 和 reflect.Type。

我用下面的代码进行演示:

ch15/main.go

func main() {
   i:=3
   iv:=reflect.ValueOf(i)
   it:=reflect.TypeOf(i)
   fmt.Println(iv,it)//3 int
}

代码定义了一个 int 类型的变量 i,它的值为 3,然后通过 reflect.ValueOf 和 reflect.TypeOf 函数就可以获得变量 i 对应的 reflect.Value 和 reflect.Type。通过 fmt.Println 函数打印后,可以看到结果是 3 int,这也可以证明 reflect.Value 表示的是变量的值,reflect.Type 表示的是变量的类型。

reflect.Value

reflect.Value 可以通过函数 reflect.ValueOf 获得,下面我将为你介绍它的结构和用法。

结构体定义

在 Go 语言中,reflect.Value 被定义为一个 struct 结构体,它的定义如下面的代码所示:

type Value struct {
   typ *rtype
   ptr unsafe.Pointer
   flag
}

我们发现 reflect.Value 结构体的字段都是私有的,也就是说,我们只能使用 reflect.Value 的方法。现在看看它有哪些常用方法,如下所示:

//针对具体类型的系列方法
//以下是用于获取对应的值
Bool
Bytes
Complex
Float
Int
String
Uint
CanSet //是否可以修改对应的值
以下是用于修改对应的值
Set
SetBool
SetBytes
SetComplex
SetFloat
SetInt
SetString
Elem //获取指针指向的值,一般用于修改对应的值
//以下Field系列方法用于获取struct类型中的字段
Field
FieldByIndex
FieldByName
FieldByNameFunc
Interface //获取对应的原始类型
IsNil //值是否为nil
IsZero //值是否是零值
Kind //获取对应的类型类别,比如Array、Slice、Map等
//获取对应的方法
Method
MethodByName
NumField //获取struct类型中字段的数量
NumMethod//类型上方法集的数量
Type//获取对应的reflect.Type

//针对具体类型的系列方法
//以下是用于获取对应的值
Bool
Bytes
Complex
Float
Int
String
Uint
CanSet //是否可以修改对应的值
以下是用于修改对应的值
Set
SetBool
SetBytes
SetComplex
SetFloat
SetInt
SetString
Elem //获取指针指向的值,一般用于修改对应的值
//以下Field系列方法用于获取struct类型中的字段
Field
FieldByIndex
FieldByName
FieldByNameFunc
Interface //获取对应的原始类型
IsNil //值是否为nil
IsZero //值是否是零值
Kind //获取对应的类型类别,比如Array、Slice、Map等
//获取对应的方法
Method
MethodByName
NumField //获取struct类型中字段的数量
NumMethod//类型上方法集的数量
Type//获取对应的reflect.Type

看着比较多,其实就三类:一类用于获取和修改对应的值;一类和 struct 类型的字段有关,用于获取对应的字段;一类和类型上的方法集有关,用于获取对应的方法。

下面我通过几个例子讲解如何使用它们。

获取原始类型

在上面的例子中,我通过 reflect.ValueOf 函数把任意类型的对象转为一个 reflect.Value,而如果想逆向转回来也可以,reflect.Value 为我们提供了 Inteface 方法,如下面的代码所示:

ch15/main.go

func main() {
   i:=3
   //int to reflect.Value
   iv:=reflect.ValueOf(i)
   //reflect.Value to int
   i1:=iv.Interface().(int)
   fmt.Println(i1)
}

这是 reflect.Value 和 int 类型互转,换成其他类型也可以。

修改对应的值

已经定义的变量可以通过反射在运行时修改,比如上面的示例 i=3,修改为 4,如下所示:

ch15/main.go

func main() {
   i:=3
   ipv:=reflect.ValueOf(&i)
   ipv.Elem().SetInt(4)
   fmt.Println(i)
}

这样就通过反射修改了一个变量。因为 reflect.ValueOf 函数返回的是一份值的拷贝,所以我们要传入变量的指针才可以。 因为传递的是一个指针,所以需要调用 Elem 方法找到这个指针指向的值,这样才能修改。 最后我们就可以使用 SetInt 方法修改值了。

要修改一个变量的值,有几个关键点:传递指针(可寻址),通过 Elem 方法获取指向的值,才可以保证值可以被修改,reflect.Value 为我们提供了 CanSet 方法判断是否可以修改该变量。

那么如何修改 struct 结构体字段的值呢?参考变量的修改方式,可总结出以下步骤:

  1. 传递一个 struct 结构体的指针,获取对应的 reflect.Value;
  2. 通过 Elem 方法获取指针指向的值;
  3. 通过 Field 方法获取要修改的字段;
  4. 通过 Set 系列方法修改成对应的值。

运行下面的代码,你会发现变量 p 中的 Name 字段已经被修改为张三了。

ch15/main.go

func main() {
   p:=person{Name: "飞雪无情",Age: 20}
   ppv:=reflect.ValueOf(&p)
   ppv.Elem().Field(0).SetString("张三")
   fmt.Println(p)
}
type person struct {
   Name string
   Age int
}

最后再来总结一下通过反射修改一个值的规则。

  1. 可被寻址,通俗地讲就是要向 reflect.ValueOf 函数传递一个指针作为参数。
  2. 如果要修改 struct 结构体字段值的话,该字段需要是可导出的,而不是私有的,也就是该字段的首字母为大写。
  3. 记得使用 Elem 方法获得指针指向的值,这样才能调用 Set 系列方法进行修改。

记住以上规则,你就可以在程序运行时通过反射修改一个变量或字段的值。

获取对应的底层类型

底层类型是什么意思呢?其实对应的主要是基础类型,比如接口、结构体、指针......因为我们可以通过 type 关键字声明很多新的类型。比如在上面的例子中,变量 p 的实际类型是 person,但是 person 对应的底层类型是 struct 这个结构体类型,而 &p 对应的则是指针类型。我们来通过下面的代码进行验证:

ch15/main.go

func main() {
   p:=person{Name: "飞雪无情",Age: 20}
   ppv:=reflect.ValueOf(&p)
   fmt.Println(ppv.Kind())
   pv:=reflect.ValueOf(p)
   fmt.Println(pv.Kind())
}

运行以上代码,可以看到如下打印输出:

ptr
struct

ptr
struct

Kind 方法返回一个 Kind 类型的值,它是一个常量,有以下可供使用的值:

type Kind uint
const (
   Invalid Kind = iota
   Bool
   Int
   Int8
   Int16
   Int32
   Int64
   Uint
   Uint8
   Uint16
   Uint32
   Uint64
   Uintptr
   Float32
   Float64
   Complex64
   Complex128
   Array
   Chan
   Func
   Interface
   Map
   Ptr
   Slice
   String
   Struct
   UnsafePointer
)

从以上源代码定义的 Kind 常量列表可以看到,已经包含了 Go 语言的所有底层类型。

reflect.Type

reflect.Value 可以用于与值有关的操作中,而如果是和变量类型本身有关的操作,则最好使用 reflect.Type,比如要获取结构体对应的字段名称或方法。

要反射获取一个变量的 reflect.Type,可以通过函数 reflect.TypeOf。

接口定义

和 reflect.Value 不同,reflect.Type 是一个接口,而不是一个结构体,所以也只能使用它的方法。

以下是我列出来的 reflect.Type 接口常用的方法。从这个列表来看,大部分都和 reflect.Value 的方法功能相同。

type Type interface {

Implements(u Type) bool
AssignableTo(u Type) bool
ConvertibleTo(u Type) bool
Comparable() bool

//以下这些方法和Value结构体的功能相同
Kind() Kind

Method(int) Method
MethodByName(string) (Method, bool)
NumMethod() int
Elem() Type
Field(i int) StructField
FieldByIndex(index []int) StructField
FieldByName(name string) (StructField, bool)
FieldByNameFunc(match func(string) bool) (StructField, bool)
NumField() int
}

其中几个特有的方法如下:

  1. Implements 方法用于判断是否实现了接口 u;
  2. AssignableTo 方法用于判断是否可以赋值给类型 u,其实就是是否可以使用 =,即赋值运算符;
  3. ConvertibleTo 方法用于判断是否可以转换成类型 u,其实就是是否可以进行类型转换;
  4. Comparable 方法用于判断该类型是否是可比较的,其实就是是否可以使用关系运算符进行比较。

我同样会通过一些示例来讲解 reflect.Type 的使用。

遍历结构体的字段和方法

我还是采用上面示例中的 person 结构体进行演示,不过需要修改一下,为它增加一个方法 String,如下所示:

func (p person) String() string{
   return fmt.Sprintf("Name is %s,Age is %d",p.Name,p.Age)
}

新增一个 String 方法,返回对应的字符串信息,这样 person 这个 struct 结构体也实现了 fmt.Stringer 接口。

你可以通过 NumField 方法获取结构体字段的数量,然后使用 for 循环,通过 Field 方法就可以遍历结构体的字段,并打印出字段名称。同理,遍历结构体的方法也是同样的思路,代码也类似,如下所示:

ch15/main.go

func main() {
   p:=person{Name: "飞雪无情",Age: 20}
   pt:=reflect.TypeOf(p)
   //遍历person的字段
   for i:=0;i<pt.NumField();i++{
      fmt.Println("字段:",pt.Field(i).Name)
   }
   //遍历person的方法
   for i:=0;i<pt.NumMethod();i++{
      fmt.Println("方法:",pt.Method(i).Name)
   }
}

运行这个代码,可以看到如下结果:

字段: Name
字段: Age
方法: String

字段: Name
字段: Age
方法: String

这正好和我在结构体 person 中定义的一致,说明遍历成功。

小技巧:你可以通过 FieldByName 方法获取指定的字段,也可以通过 MethodByName 方法获取指定的方法,这在需要获取某个特定的字段或者方法时非常高效,而不是使用遍历。

是否实现某接口

通过 reflect.Type 还可以判断是否实现了某接口。我还是以 person 结构体为例,判断它是否实现了接口 fmt.Stringer 和 io.Writer,如下面的代码所示:

func main() {
   p:=person{Name: "飞雪无情",Age: 20}
   pt:=reflect.TypeOf(p)
   stringerType:=reflect.TypeOf((*fmt.Stringer)(nil)).Elem()
   writerType:=reflect.TypeOf((*io.Writer)(nil)).Elem()
   fmt.Println("是否实现了fmt.Stringer:",pt.Implements(stringerType))
   fmt.Println("是否实现了io.Writer:",pt.Implements(writerType))
}

小提示:尽可能通过类型断言的方式判断是否实现了某接口,而不是通过反射。

这个示例通过 Implements 方法来判断是否实现了 fmt.Stringer 和 io.Writer 接口,运行它,你可以看到如下结果:

是否实现了fmt.Stringer: true
是否实现了io.Writer: false

是否实现了fmt.Stringer: true
是否实现了io.Writer: false

因为结构体 person 只实现了 fmt.Stringer 接口,没有实现 io.Writer 接口,所以和验证的结果一致。

字符串和结构体互转

在字符串和结构体互转的场景中,使用最多的就是 JSON 和 struct 互转。在这个小节中,我会用 JSON 和 struct 讲解 struct tag 这一功能的使用。

JSON 和 Struct 互转

Go 语言的标准库有一个 json 包,通过它可以把 JSON 字符串转为一个 struct 结构体,也可以把一个 struct 结构体转为一个 json 字符串。下面我还是以 person 这个结构体为例,讲解 JSON 和 struct 的相互转换。如下面的代码所示:

func main() {
   p:=person{Name: "飞雪无情",Age: 20}
   //struct to json
   jsonB,err:=json.Marshal(p)
   if err==nil {
      fmt.Println(string(jsonB))
   }
   //json to struct
   respJSON:="{\"Name\":\"李四\",\"Age\":40}"
   json.Unmarshal([]byte(respJSON),&p)
   fmt.Println(p)
}

这个示例是我使用 Go 语言提供的 json 标准包做的演示。通过 json.Marshal 函数,你可以把一个 struct 转为 JSON 字符串。通过 json.Unmarshal 函数,你可以把一个 JSON 字符串转为 struct。

运行以上代码,你会看到如下结果输出:

{"Name":"飞雪无情","Age":20}
Name is 李四,Age is 40

{"Name":"飞雪无情","Age":20}
Name is 李四,Age is 40

仔细观察以上打印出的 JSON 字符串,你会发现 JSON 字符串的 Key 和 struct 结构体的字段名称一样,比如示例中的 Name 和 Age。那么是否可以改变它们呢?比如改成小写的 name 和 age,并且字段的名称还是大写的 Name 和 Age。当然可以,要达到这个目的就需要用到 struct tag 的功能了。

Struct Tag

顾名思义,struct tag 是一个添加在 struct 字段上的标记,使用它进行辅助,可以完成一些额外的操作,比如 json 和 struct 互转。在上面的示例中,如果想把输出的 json 字符串的 Key 改为小写的 name 和 age,可以通过为 struct 字段添加 tag 的方式,示例代码如下:

type person struct {
   Name string `json:"name"`
   Age int `json:"age"`
}

为 struct 字段添加 tag 的方法很简单,只需要在字段后面通过反引号把一个键值对包住即可,比如以上示例中的 json:"name"。其中冒号前的 json 是一个 Key,可以通过这个 Key 获取冒号后对应的 name。

小提示:json 作为 Key,是 Go 语言自带的 json 包解析 JSON 的一种约定,它会通过 json 这个 Key 找到对应的值,用于 JSON 的 Key 值。

我们已经通过 struct tag 指定了可以使用 name 和 age 作为 json 的 Key,代码就可以修改成如下所示:

respJSON:="{\"name\":\"李四\",\"age\":40}"

没错,JSON 字符串也可以使用小写的 name 和 age 了。现在再运行这段代码,你会看到如下结果:

{"name":"张三","age":20}
Name is 李四,Age is 40

{"name":"张三","age":20}
Name is 李四,Age is 40

输出的 JSON 字符串的 Key 是小写的 name 和 age,并且小写的 name 和 age JSON 字符串也可以转为 person 结构体。

相信你已经发现,struct tag 是整个 JSON 和 struct 互转的关键,这个 tag 就像是我们为 struct 字段起的别名,那么 json 包是如何获得这个 tag 的呢?这就需要反射了。我们来看下面的代码:

//遍历person字段中key为json的tag
for i:=0;i<pt.NumField();i++{
   sf:=pt.Field(i)
   fmt.Printf("字段%s上,json tag为%s\n",sf.Name,sf.Tag.Get("json"))
}

要想获得字段上的 tag,就要先反射获得对应的字段,我们可以通过 Field 方法做到。该方法返回一个 StructField 结构体,它有一个字段是 Tag,存有字段的所有 tag。示例中要获得 Key 为 json 的 tag,所以只需要调用 sf.Tag.Get("json") 即可。

结构体的字段可以有多个 tag,用于不同的场景,比如 json 转换、bson 转换、orm 解析等。如果有多个 tag,要使用空格分隔。采用不同的 Key 可以获得不同的 tag,如下面的代码所示:

//遍历person字段中key为json、bson的tag
for i:=0;i<pt.NumField();i++{
   sf:=pt.Field(i)
   fmt.Printf("字段%s上,json tag为%s\n",sf.Name,sf.Tag.Get("json"))
   fmt.Printf("字段%s上,bson tag为%s\n",sf.Name,sf.Tag.Get("bson"))
}
type person struct {
   Name string `json:"name" bson:"b_name"`
   Age int `json:"age" bson:"b_name"`
}

运行代码,你可以看到如下结果:

字段Name上,key为json的tag为name
字段Name上,key为bson的tag为b_name
字段Age上,key为json的tag为age
字段Age上,key为bson的tag为b_name

字段Name上,key为json的tag为name
字段Name上,key为bson的tag为b_name
字段Age上,key为json的tag为age
字段Age上,key为bson的tag为b_name

可以看到,通过不同的 Key,使用 Get 方法就可以获得自定义的不同的 tag。

实现 Struct 转 JSON

相信你已经理解了什么是 struct tag,下面我再通过一个 struct 转 json 的例子演示它的使用:

func main() {
   p:=person{Name: "飞雪无情",Age: 20}
   pv:=reflect.ValueOf(p)
   pt:=reflect.TypeOf(p)
   //自己实现的struct to json
   jsonBuilder:=strings.Builder{}
   jsonBuilder.WriteString("{")
   num:=pt.NumField()
   for i:=0;i<num;i++{
      jsonTag:=pt.Field(i).Tag.Get("json") //获取json tag
      jsonBuilder.WriteString("\""+jsonTag+"\"")
      jsonBuilder.WriteString(":")
      //获取字段的值
      jsonBuilder.WriteString(fmt.Sprintf("\"%v\"",pv.Field(i)))
      if i<num-1{
         jsonBuilder.WriteString(",")
      }
   }
   jsonBuilder.WriteString("}")
   fmt.Println(jsonBuilder.String())//打印json字符串
}

这是一个比较简单的 struct 转 json 示例,但是已经可以很好地演示 struct 的使用。在上述示例中,自定义的 jsonBuilder 负责 json 字符串的拼接,通过 for 循环把每一个字段拼接成 json 字符串。运行以上代码,你可以看到如下打印结果:

{"name":"飞雪无情","age":"20"}

{"name":"飞雪无情","age":"20"}

json 字符串的转换只是 struct tag 的一个应用场景,你完全可以把 struct tag 当成结构体中字段的元数据配置,使用它来做想做的任何事情,比如 orm 映射、xml 转换、生成 swagger 文档等。

反射定律

反射是计算机语言中程序检视其自身结构的一种方法,它属于元编程的一种形式。反射灵活、强大,但也存在不安全。它可以绕过编译器的很多静态检查,如果过多使用便会造成混乱。为了帮助开发者更好地理解反射,Go 语言的作者在博客上总结了反射的三大定律

  1. 任何接口值 interface{} 都可以反射出反射对象,也就是 reflect.Value 和 reflect.Type,通过函数 reflect.ValueOf 和 reflect.TypeOf 获得。
  2. 反射对象也可以还原为 interface{} 变量,也就是第 1 条定律的可逆性,通过 reflect.Value 结构体的 Interface 方法获得。
  3. 要修改反射的对象,该值必须可设置,也就是可寻址,参考上节课修改变量的值那一节的内容理解。

小提示:任何类型的变量都可以转换为空接口 intferface{},所以第 1 条定律中函数 reflect.ValueOf 和 reflect.TypeOf 的参数就是 interface{},表示可以把任何类型的变量转换为反射对象。在第 2 条定律中,reflect.Value 结构体的 Interface 方法返回的值也是 interface{},表示可以把反射对象还原为对应的类型变量。

一旦你理解了这三大定律,就可以更好地理解和使用 Go 语言反射。

总结

在反射中,reflect.Value 对应的是变量的值,如果你需要进行和变量的值有关的操作,应该优先使用 reflect.Value,比如获取变量的值、修改变量的值等。reflect.Type 对应的是变量的类型,如果你需要进行和变量的类型本身有关的操作,应该优先使用 reflect.Type,比如获取结构体内的字段、类型拥有的方法集等。

此外我要再次强调:反射虽然很强大,可以简化编程、减少重复代码,但是过度使用会让你的代码变得复杂混乱。所以除非非常必要,否则尽可能少地使用它们。

go语言清空slice go语言内存申请和释放_初始化

这节课的作业是:自己写代码运行通过反射调用结构体的方法。

下节课我将介绍“非类型安全:让你既爱又恨的 unsafe”,记得来听课!


16 非类型安全:让你既爱又恨的 unafe

上节课我留了一个小作业,让你练习一下如何使用反射调用一个方法,下面我来进行讲解。

还是以 person 这个结构体类型为例。我为它增加一个方法 Print,功能是打印一段文本,示例代码如下:

func (p person) Print(prefix string){
   fmt.Printf("%s:Name is %s,Age is %d\n",prefix,p.Name,p.Age)
}

然后就可以通过反射调用 Print 方法了,示例代码如下:

func main() {
   p:=person{Name: "飞雪无情",Age: 20}
   pv:=reflect.ValueOf(p)
   //反射调用person的Print方法
   mPrint:=pv.MethodByName("Print")
   args:=[]reflect.Value{reflect.ValueOf("登录")}
   mPrint.Call(args)
}

从示例中可以看到,要想通过反射调用一个方法,首先要通过 MethodByName 方法找到相应的方法。因为 Print 方法需要参数,所以需要声明参数,它的类型是 []reflect.Value,也就是示例中的 args 变量,最后就可以通过 Call 方法反射调用 Print 方法了。其中记得要把 args 作为参数传递给 Call 方法。

运行以上代码,可以看到如下结果:

登录:Name is 飞雪无情,Age is 20

登录:Name is 飞雪无情,Age is 20

从打印的结果可以看到,和我们直接调用 Print 方法是一样的结果,这也证明了通过反射调用 Print 方法是可行的。

下面我们继续深入 Go 的世界,这节课会介绍 Go 语言自带的 unsafe 包的高级用法。

顾名思义,unsafe 是不安全的。Go 将其定义为这个包名,也是为了让我们尽可能地不使用它。不过虽然不安全,它也有优势,那就是可以绕过 Go 的内存安全机制,直接对内存进行读写。所以有时候出于性能需要,还是会冒险使用它来对内存进行操作。

指针类型转换

Go 的设计者为了编写方便、提高效率且降低复杂度,将其设计成一门强类型的静态语言。强类型意味着一旦定义了,类型就不能改变;静态意味着类型检查在运行前就做了。同时出于安全考虑,Go 语言是不允许两个指针类型进行转换的。

我们一般使用 *T 作为一个指针类型,表示一个指向类型 T 变量的指针。为了安全的考虑,两个不同的指针类型不能相互转换,比如 *int 不能转为 *float64。

我们来看下面的代码:

func main() {
   i:= 10
   ip:=&i
   var fp *float64 = (*float64)(ip)
   fmt.Println(fp)
}

这个代码在编译的时候,会提示 cannot convert ip (type * int) to type * float64,也就是不能进行强制转型。那如果还是需要转换呢?这就需要使用 unsafe 包里的 Pointer 了。下面我先为你介绍 unsafe.Pointer 是什么,然后再介绍如何转换。

unsafe.Pointer

unsafe.Pointer 是一种特殊意义的指针,可以表示任意类型的地址,类似 C 语言里的 void* 指针,是全能型的。

正常情况下,*int 无法转换为 *float64,但是通过 unsafe.Pointer 做中转就可以了。在下面的示例中,我通过 unsafe.Pointer 把 *int 转换为 *float64,并且对新的 *float64 进行 3 倍的乘法操作,你会发现原来变量 i 的值也被改变了,变为 30。

ch16/main.go

func main() {
   i:= 10
   ip:=&i
   var fp *float64 = (*float64)(unsafe.Pointer(ip))
   *fp = *fp * 3
   fmt.Println(i)
}

这个例子没有任何实际意义,但是说明了通过 unsafe.Pointer 这个万能的指针,我们可以在 *T 之间做任何转换。那么 unsafe.Pointer 到底是什么?为什么其他类型的指针可以转换为 unsafe.Pointer 呢?这就要看 unsafe.Pointer 的源代码定义了,如下所示:

// ArbitraryType is here for the purposes of documentation
// only and is not actually part of the unsafe package. 
// It represents the type of an arbitrary Go expression.
type ArbitraryType int
type Pointer *ArbitraryType

按 Go 语言官方的注释,ArbitraryType 可以表示任何类型(这里的 ArbitraryType 仅仅是文档需要,不用太关注它本身,只要记住可以表示任何类型即可)。 而 unsafe.Pointer 又是 *ArbitraryType,也就是说 unsafe.Pointer 是任何类型的指针,也就是一个通用型的指针,足以表示任何内存地址。

uintptr 指针类型

uintptr 也是一种指针类型,它足够大,可以表示任何指针。它的类型定义如下所示:

// uintptr is an integer type that is large enough 
// to hold the bit pattern of any pointer.
type uintptr uintptr

既然已经有了 unsafe.Pointer,为什么还要设计 uintptr 类型呢?这是因为 unsafe.Pointer 不能进行运算,比如不支持 +(加号)运算符操作,但是 uintptr 可以。通过它,可以对指针偏移进行计算,这样就可以访问特定的内存,达到对特定内存读写的目的,这是真正内存级别的操作。

在下面的代码中,我以通过指针偏移修改 struct 结构体内的字段为例,演示 uintptr 的用法。

func main() {
   p :=new(person)
   //Name是person的第一个字段不用偏移,即可通过指针修改
   pName:=(*string)(unsafe.Pointer(p))
   *pName="飞雪无情"
   //Age并不是person的第一个字段,所以需要进行偏移,这样才能正确定位到Age字段这块内存,才可以正确的修改
   pAge:=(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(p))+unsafe.Offsetof(p.Age)))
   *pAge = 20
   fmt.Println(*p)
}
type person struct {
   Name string
   Age int
}

这个示例不是通过直接访问相应字段的方式对 person 结构体字段赋值,而是通过指针偏移找到相应的内存,然后对内存操作进行赋值。

下面我详细介绍操作步骤。

  1. 先使用 new 函数声明一个 *person 类型的指针变量 p。
  2. 然后把 *person 类型的指针变量 p 通过 unsafe.Pointer,转换为 *string 类型的指针变量 pName。
  3. 因为 person 这个结构体的第一个字段就是 string 类型的 Name,所以 pName 这个指针就指向 Name 字段(偏移为 0),对 pName 进行修改其实就是修改字段 Name 的值。
  4. 因为 Age 字段不是 person 的第一个字段,要修改它必须要进行指针偏移运算。所以需要先把指针变量 p 通过 unsafe.Pointer 转换为 uintptr,这样才能进行地址运算。既然要进行指针偏移,那么要偏移多少呢?这个偏移量可以通过函数 unsafe.Offsetof 计算出来,该函数返回的是一个 uintptr 类型的偏移量,有了这个偏移量就可以通过 + 号运算符获得正确的 Age 字段的内存地址了,也就是通过 unsafe.Pointer 转换后的 *int 类型的指针变量 pAge。
  5. 然后需要注意的是,如果要进行指针运算,要先通过 unsafe.Pointer 转换为 uintptr 类型的指针。指针运算完毕后,还要通过 unsafe.Pointer 转换为真实的指针类型(比如示例中的 *int 类型),这样可以对这块内存进行赋值或取值操作。
  6. 有了指向字段 Age 的指针变量 pAge,就可以对其进行赋值操作,修改字段 Age 的值了。

运行以上示例,你可以看到如下结果:

{飞雪无情 20}

{飞雪无情 20}

这个示例主要是为了讲解 uintptr 指针运算,所以一个结构体字段的赋值才会写得这么复杂,如果按照正常的编码,以上示例代码会和下面的代码结果一样。

func main() {
   p :=new(person)
   p.Name = "飞雪无情"
   p.Age = 20
   fmt.Println(*p)
}

指针运算的核心在于它操作的是一个个内存地址,通过内存地址的增减,就可以指向一块块不同的内存并对其进行操作,而且不必知道这块内存被起了什么名字(变量名)。

指针转换规则

你已经知道 Go 语言中存在三种类型的指针,它们分别是:常用的 *T、unsafe.Pointer 及 uintptr。通过以上示例讲解,可以总结出这三者的转换规则:

  1. 任何类型的 *T 都可以转换为 unsafe.Pointer;
  2. unsafe.Pointer 也可以转换为任何类型的 *T;
  3. unsafe.Pointer 可以转换为 uintptr;
  4. uintptr 也可以转换为 unsafe.Pointer。


(指针转换示意图)


可以发现,unsafe.Pointer 主要用于指针类型的转换,而且是各个指针类型转换的桥梁。uintptr 主要用于指针运算,尤其是通过偏移量定位不同的内存。

unsafe.Sizeof

Sizeof 函数可以返回一个类型所占用的内存大小,这个大小只与类型有关,和类型对应的变量存储的内容大小无关,比如 bool 型占用一个字节、int8 也占用一个字节。

通过 Sizeof 函数你可以查看任何类型(比如字符串、切片、整型)占用的内存大小,示例代码如下:

fmt.Println(unsafe.Sizeof(true))
fmt.Println(unsafe.Sizeof(int8(0)))
fmt.Println(unsafe.Sizeof(int16(10)))
fmt.Println(unsafe.Sizeof(int32(10000000)))
fmt.Println(unsafe.Sizeof(int64(10000000000000)))
fmt.Println(unsafe.Sizeof(int(10000000000000000)))
fmt.Println(unsafe.Sizeof(string("飞雪无情")))
fmt.Println(unsafe.Sizeof([]string{"飞雪u无情","张三"}))

对于整型来说,占用的字节数意味着这个类型存储数字范围的大小,比如 int8 占用一个字节,也就是 8bit,所以它可以存储的大小范围是 -128~~127,也就是 −2^(n-1) 到 2^(n-1)−1。其中 n 表示 bit,int8 表示 8bit,int16 表示 16bit,以此类推。

对于和平台有关的 int 类型,要看平台是 32 位还是 64 位,会取最大的。比如我自己测试以上输出,会发现 int 和 int64 的大小是一样的,因为我用的是 64 位平台的电脑。

小提示:一个 struct 结构体的内存占用大小,等于它包含的字段类型内存占用大小之和。

总结

unsafe 包里最常用的就是 Pointer 指针,通过它可以让你在 *T、uintptr 及 Pointer 三者间转换,从而实现自己的需求,比如零内存拷贝或通过 uintptr 进行指针运算,这些都可以提高程序效率。

unsafe 包里的功能虽然不安全,但的确很香,比如指针运算、类型转换等,都可以帮助我们提高性能。不过我还是建议尽可能地不使用,因为它可以绕开 Go 语言编译器的检查,可能会因为你的操作失误而出现问题。当然如果是需要提高性能的必要操作,还是可以使用,比如 []byte 转 string,就可以通过 unsafe.Pointer 实现零内存拷贝,下节课我会详细讲解。