13.其他特性

? ? 本節(jié)主要來學習Go語言中存在一些獨有特性

13.1 值類型/引用類型

? ? Go語言中針對不同的數據類型可以分為值類型引用類型變量,兩者描述如下所示:

  • 值類型變量是變量直接存儲數據,內存通常在棧中分配,對應的數據類型整型、布爾型、浮點型、字符串、數組等
  • 引用類型變量是變量存儲的是一個內存地址,在內存地址上再存儲數據,內存通常在堆上分配。對應的數據類型指針、切片、Map、通道和接口等。

13.2 深淺拷貝

? ? 在Go語言中,淺拷貝深拷貝是兩種常見的對象復制方式,它們主要的區(qū)別在于是否真正獲取到了被拷貝對象的單獨掌握,從而避免相互影響的問題。

  • 淺拷貝(Shadow Copy)

? ? 淺拷貝也稱為影子拷貝,其只是復制了對象本身,而沒有復制其引用的子對象,使得原對象和新對象共享同一塊內存空間。因此,在修改新對象中的引用類型數據會影響到原對象,反之亦然。

  • 深拷貝(Deep Copy)

? ? 深拷貝是指創(chuàng)建一個新對象,并遞歸復制其引用的子對象,使得原對象和新對象在內存是完全獨立的。修改新對象不會影響原對象,反之亦然。深拷貝保證了對象的完全獨立性和數據的完整性。

? ? 深淺拷貝指在拷貝過程中是否發(fā)生遞歸拷貝,即如果某個值是一個地址,是只復制這個地址,還是復制地址所指向的內容。Go語言中,引用類型實際上拷貝的是標頭值,也是值拷貝,因為并沒有通過標頭值中對底層數據結構中的指針指向的內容進行復制,即淺拷貝。非引用類型的復制就是值拷貝,也即再創(chuàng)建一個副本,也被稱為淺拷貝。因為不能說一個整數值在內存復制出一個副本,就是深拷貝,因為像整數類型的基本類型就一個單獨的值,沒辦法深入拷貝。

復雜的數據結構,往往存在嵌套,有時還會嵌套很多層。如果采用深拷貝,則代價會很高。因此,淺拷貝才是最常用的方案。

  • 淺拷貝的使用場景

? ? 當需要創(chuàng)建一個對象的副本,并且希望修改副本時,同時也影響到原對象時,可以使用淺拷貝。在某些性能敏感的場景,為了減少內存占用和提高性能,也可以使用淺拷貝

  • 深拷貝的使用場景

? ? 當需要創(chuàng)建一個獨立的對象,且不希望修改新對象時,也影響到原對象時,可以使用深拷貝。例如,處理敏感數據時,為了避免數據泄漏,需要復制一份新的數據進行操作。另外,在并發(fā)編程中,多個goroutine需要操作同一份數據的副本時,為了防止競態(tài)條件發(fā)生,應該使用深拷貝。

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)
    // 將變量賦值給另一個變量,發(fā)生淺拷貝,即值拷貝
    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])
    // 執(zhí)行淺拷貝
    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])

    // 執(zhí)行深拷貝
    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區(qū)別

? ? Go語言中newmake是兩個內建的函數,主要用來創(chuàng)建分配類型內存,但兩者在初始化變量卻有一些不同,如下所示:

  • 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創(chuàng)建的變量數據類型是指針變量,并且已經分配了對應的內存地址,可以直接對變量執(zhí)行賦值操作
  • 使用var定義的指針變量,其數據和內存地址均為nil,若要操作指針變量必須設置具體的內存地址,因此需要綁定至某個變量的內存地址
  • 使用new函數時,等于實現了指針的定義和賦值過程,其中指針賦值是指針變量設置具體的內存地址,并不是在內存地址中存放數值。

? ? make也是用于分配內存,但和new不同的,其只適用于切片Map通道等數據類型的創(chuàng)建,且返回的數據類型也只是這三個數據類型本身,而不是它們的指針類型。因為這三種類型本身也就是引用類型,所以就沒有必要返回他們的指針了。切片、Map和通道都是引用類型,所以必須初始化,但不是設置為零值,這和new是不一樣的。

? ? make的源碼定義如下所示:

func make(t Type, size ...IntegerType) Type
  • t: 創(chuàng)建變量的數據類型,僅允許切片、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創(chuàng)建的變量是指針類型,make創(chuàng)建的變量數據類型僅為切片、Map和通道等數據類型
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容