13.其他特性

    本节主要来学习Go语言中存在一些独有特性

13.1 值类型/引用类型

    Go语言中针对不同的数据类型可以分为值类型引用类型变量,两者描述如下所示:

  • 值类型变量是变量直接存储数据,内存通常在栈中分配,对应的数据类型整型、布尔型、浮点型、字符串、数组等
  • 引用类型变量是变量存储的是一个内存地址,在内存地址上再存储数据,内存通常在堆上分配。对应的数据类型指针、切片、Map、通道和接口等。

13.2 深浅拷贝

    在Go语言中,浅拷贝深拷贝是两种常见的对象复制方式,它们主要的区别在于是否真正获取到了被拷贝对象的单独掌握,从而避免相互影响的问题

  • 浅拷贝(Shadow Copy)

    浅拷贝也称为影子拷贝,其只是复制了对象本身,而没有复制其引用的子对象,使得原对象和新对象共享同一块内存空间。因此,在修改新对象中的引用类型数据会影响到原对象,反之亦然。

  • 深拷贝(Deep Copy)

    深拷贝是指创建一个新对象,并递归复制其引用的子对象,使得原对象和新对象在内存是完全独立的。修改新对象不会影响原对象,反之亦然。深拷贝保证了对象的完全独立性和数据的完整性

    深浅拷贝指在拷贝过程中是否发生递归拷贝,即如果某个值是一个地址,是只复制这个地址,还是复制地址所指向的内容。Go语言中,引用类型实际上拷贝的是标头值,也是值拷贝,因为并没有通过标头值中对底层数据结构中的指针指向的内容进行复制,即浅拷贝。非引用类型的复制就是值拷贝,也即再创建一个副本,也被称为浅拷贝。因为不能说一个整数值在内存复制出一个副本,就是深拷贝,因为像整数类型的基本类型就一个单独的值,没办法深入拷贝。

复杂的数据结构,往往存在嵌套,有时还会嵌套很多层。如果采用深拷贝,则代价会很高。因此,浅拷贝才是最常用的方案。

  • 浅拷贝的使用场景

    当需要创建一个对象的副本,并且希望修改副本时,同时也影响到原对象时,可以使用浅拷贝。在某些性能敏感的场景,为了减少内存占用和提高性能,也可以使用浅拷贝

  • 深拷贝的使用场景

    当需要创建一个独立的对象,且不希望修改新对象时,也影响到原对象时,可以使用深拷贝。例如,处理敏感数据时,为了避免数据泄漏,需要复制一份新的数据进行操作。另外,在并发编程中,多个goroutine需要操作同一份数据的副本时,为了防止竞态条件发生,应该使用深拷贝。

package main

import (
	"fmt"

	"github.com/mohae/deepcopy"
)

type Person struct {
	Name string
	Age  int
}

func main() {
	// 值类型变量
	age := 28
	fmt.Printf("变量age的值为:%+v,内存地址为:%p\n", age, &age)
	// 将变量赋值给另一个变量,发生浅拷贝,即值拷贝
	myAge := age
	fmt.Printf("变量MyAge的值为:%+v,内存地址为:%p\n", myAge, &myAge)

	// 引用类型
	s := make([]int, 5, 10)
	s[0] = 100
	fmt.Printf("变量s的值为:%#v,内存地址为:%p,s[0]地址:%p\n", s, &s, &s[0])
	// 执行浅拷贝
	ss1 := s
	fmt.Printf("变量ss1的值为:%#v,内存地址为:%p,ss1[0]地址:%p\n", ss1, &ss1, &ss1[0])

	// 修改变量的值
	ss1[0] = 200
	fmt.Printf("变量s的值为:%#v,内存地址为:%p,s[0]地址:%p\n", s, &s, &s[0])
	fmt.Printf("变量ss1的值为:%#v,内存地址为:%p,ss1[0]地址:%p\n", ss1, &ss1, &ss1[0])

	// 执行深拷贝
	cpy := deepcopy.Copy(s)
	ss2 := cpy.([]int)
	ss2[0] = 300
	fmt.Printf("变量s的值为:%#v,内存地址为:%p,s[0]地址:%p\n", s, &s, &s[0])
	fmt.Printf("变量ss2的值为:%#v,内存地址为:%p,ss2[0]地址:%p\n", ss2, &ss2, &ss2)
}

    运行结果如下所示:

变量age的值为:28,内存地址为:0xc000190068
变量MyAge的值为:28,内存地址为:0xc000190090
变量s的值为:[]int{100, 0, 0, 0, 0},内存地址为:0xc00018e048,s[0]地址:0xc0001b40f0
变量ss1的值为:[]int{100, 0, 0, 0, 0},内存地址为:0xc00018e078,ss1[0]地址:0xc0001b40f0
变量s的值为:[]int{200, 0, 0, 0, 0},内存地址为:0xc00018e048,s[0]地址:0xc0001b40f0
变量ss1的值为:[]int{200, 0, 0, 0, 0},内存地址为:0xc00018e078,ss1[0]地址:0xc0001b40f0
变量s的值为:[]int{200, 0, 0, 0, 0},内存地址为:0xc00018e048,s[0]地址:0xc0001b40f0
变量ss2的值为:[]int{300, 0, 0, 0, 0},内存地址为:0xc00018e138,ss2[0]地址:0xc00018e138

13.3 类型别名/自定义类型

    类型别名和自定义类型都是使用关键字type定实现的,但代表的功能却不尽相同,如下所示:

  • 类型别名

    类型别名是对已有数据类型重新定义一个新的名字。主要用于解决代码升级、迁移过程存在的数据兼容性问题

  • 自定义类型

    是用户根据自身需求定义一个新的类型,但自定义的数据类型必须在已有的数据类型上进行定义。最常见的自定义类型就是结构体和接口或者为现有类型添加方法。

    类型别名和自定义类型在代码结构非常相似,两者的语法如下所示:

// 类型别名
type name = Type
// 自定义类型
type name Type

    示例代码如下所示:

package main

import "fmt"

// 类型别名
type MyInt = int

// 自定义类型
type MyString string

// 为自定义类型添加方法

func (ms *MyString) MyMethod() {
	fmt.Println("为自定义类型MyString添加方法")
}

func main() {
	var age MyInt
	fmt.Printf("age的数据类型为:%T\n", age)

	var ms MyString
	fmt.Printf("ms的数据类型为:%T\n", ms)
	ms.MyMethod()
}

    运行结果如下所示:

age的数据类型为:int
ms的数据类型为:main.MyString
为自定义类型MyString添加方法
  • 将MyInt设置为int的一个别名,等同于int类型
  • 通过type定义,添加一个自定义数据类型MyString,但它依然具备字符串string的特性。

13.4 new和make区别

    Go语言中newmake是两个内建的函数,主要用来创建分配类型内存,但两者在初始化变量却有一些不同,如下所示:

  • make: 初始化内置的数据结构,例如切片、Map和Channel等
  • new: 根据传入的类型分配一块内存空间并返回指向这块内存空间的指针

    我们先来看看以下代码:

var i int
var s string

    变量的声明可以通过var来声明,然后就可以在程序中使用。如果没有指定默认值,则相应的默认变量值为其零值。例如int的默认值为0,string类型的默认值为空,引用类型的默认值为nil。那如果将数据类型换成引用类型呢?

var p *int
*p = 100
fmt.Printf("p类型:%T,值:%[1]v,内存地址:%[1]p\n", p)

    以上代码会直接出现panic,如下所示:

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

    从panic提示中可以看出,对于引用类型的变量,不但要声明它,还要为其分配内存空间。内置函数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

    new函数只接受一个参数,而该参数就是一个类型,分配好内存后,返回一个指向该类型内存地址的指针。同时它也会把分配的内存置为零,也就类型的零值。

    示例代码如下所示:

package main

import "fmt"

func main() {
	myInt := new(int)
	fmt.Printf("myInt类型:%T,值:%v,内存地址:%v\n", *myInt, *myInt, myInt)
	*myInt = 100
	fmt.Printf("myInt类型:%T,值:%v,内存地址:%v\n", *myInt, *myInt, myInt)

	myString := new(string)
	fmt.Printf("myString类型:%T,值:%v,内存地址:%v\n", *myString, *myString, myString)
	*myString = "Surpass"
	fmt.Printf("myString类型:%T,值:%v,内存地址:%v\n", *myString, *myString, myString)

	myMap := new(map[string]string)
	fmt.Printf("myMap类型:%T,值:%v,内存地址:%v\n", *myMap, *myMap, myMap)
	*myMap = map[string]string{"name": "Surpass"}
	fmt.Printf("myMap类型:%T,值:%v,内存地址:%v\n", *myMap, *myMap, myMap)

	mySlice := new([]int)
	fmt.Printf("mySlice类型:%T,值:%v,内存地址:%v\n", *mySlice, *mySlice, mySlice)
	*mySlice = []int{1, 2, 3}
	fmt.Printf("mySlice类型:%T,值:%v,内存地址:%v\n", *mySlice, *mySlice, mySlice)

	// 引用类型
	// 方式一:
	var p *int = new(int)
	*p = 100
	fmt.Printf("指针类型:%T,值:%v,内存地址:%v\n", p, *p, p)

	// 方式二:
	var p2 *int
	fmt.Printf("指针类型:%T,值:%[1]v,内存地址:%[1]v\n", p2)
	var i int = 200
	p2 = &i
	fmt.Printf("指针类型:%T,值:%v,内存地址:%v\n", p2, *p2, p2)
}

    运行结果如下所示:

myInt类型:int,值:0,内存地址:0xc00000a0b8
myInt类型:int,值:100,内存地址:0xc00000a0b8
myString类型:string,值:,内存地址:0xc00002a280
myString类型:string,值:Surpass,内存地址:0xc00002a280
myMap类型:map[string]string,值:map[],内存地址:&map[]
myMap类型:map[string]string,值:map[name:Surpass],内存地址:&map[name:Surpass]
mySlice类型:[]int,值:[],内存地址:&[]
mySlice类型:[]int,值:[1 2 3],内存地址:&[1 2 3]
指针类型:*int,值:100,内存地址:0xc00000a120
指针类型:*int,值:<nil>,内存地址:<nil>
指针类型:*int,值:200,内存地址:0xc00000a128

    从以上运行结果,可总结如下所示:

  • new创建的变量数据类型是指针变量,并且已经分配了对应的内存地址,可以直接对变量执行赋值操作
  • 使用var定义的指针变量,其数据和内存地址均为nil,若要操作指针变量必须设置具体的内存地址,因此需要绑定至某个变量的内存地址
  • 使用new函数时,等于实现了指针的定义和赋值过程,其中指针赋值是指针变量设置具体的内存地址,并不是在内存地址中存放数值。

    make也是用于分配内存,但和new不同的,其只适用于切片Map通道等数据类型的创建,且返回的数据类型也只是这三个数据类型本身,而不是它们的指针类型。因为这三种类型本身也就是引用类型,所以就没有必要返回他们的指针了。切片、Map和通道都是引用类型,所以必须初始化,但不是设置为零值,这和new是不一样的。

    make的源码定义如下所示:

func make(t Type, size ...IntegerType) Type
  • t: 创建变量的数据类型,仅允许切片、Map和通道等
  • size:是可选参数,用于设置切片、Map和通道的长度或容量

从函数的定义中可以看到,返回的类型还是该类型

    示例代码如下所示:

package main

import "fmt"

func main() {
	s := make([]int, 5, 10)
	fmt.Printf("切片的值:%+v,长度:%d,容量:%d\n", s, len(s), cap(s))
	s[0] = 100
	fmt.Printf("切片的值:%+v,长度:%d,容量:%d\n", s, len(s), cap(s))

	m := make(map[string]string)
	fmt.Printf("Map的值:%+v,元素数量为:%d\n", m, len(m))
	m["name"] = "Surpass"
	m["location"] = "Shanghai"
	fmt.Printf("Map的值:%+v,元素数量为:%d\n", m, len(m))

	c := make(chan string, 10)
	fmt.Printf("通道值:%+v,长度为:%d,容量为:%d\n", c, len(c), cap(c))
	c <- "Surpass"
	c <- "Shanghai"
	fmt.Printf("通道值:%+v,已使用:%d,容量为:%d\n", c, len(c), cap(c))
}

    运行结果如下所示:

切片的值:[0 0 0 0 0],长度:5,容量:10
切片的值:[100 0 0 0 0],长度:5,容量:10
Map的值:map[],元素数量为:0
Map的值:map[location:Shanghai name:Surpass],元素数量为:2
通道值:0xc000116000,长度为:0,容量为:10
通道值:0xc000116000,已使用:2,容量为:10

    根据以上代码运行结果,总结如下:

  • new适用于所有数据类型,make仅适用于切片、Map和通道等数据类型
  • new创建的变量是指针类型,make创建的变量数据类型仅为切片、Map和通道等数据类型
posted @ 2025-08-30 18:11  Surpassme  阅读(1)  评论(0)    收藏  举报