1. 「每周譯Go」了解 Go 中的指針

        共 12921字,需瀏覽 26分鐘

         ·

        2023-02-08 01:13

        目錄

        1. 在 Go 中導(dǎo)入包
        2. 理解 Go 中包的可見性
        3. 如何在 Go 中編寫條件語句
        4. 如何在 Go 中編寫 Switch 語句
        5. 如何在 Go 中構(gòu)造 for 循環(huán)
        6. 在循環(huán)中使用 Break 和 Continue
        7. 如何在 Go 中定義并調(diào)用函數(shù)
        8. 如何在 Go 中使用可變參數(shù)函數(shù)
        9. 了解 Go 中的 defer
        10. 了解 Go 中的 init
        11. 用構(gòu)建標(biāo)簽定制 Go 二進(jìn)制文件
        12. 了解 Go 中的指針
        13. 在 Go 中定義結(jié)構(gòu)體
        14. 在 Go 中定義方法
        15. 如何構(gòu)建和安裝 Go 程序
        16. 如何在 Go 中使用結(jié)構(gòu)體標(biāo)簽
        17. 如何在 Go 使用 interface
        18. 在不同的操作系統(tǒng)和架構(gòu)編譯 Go 應(yīng)用
        19. 用 ldflags 設(shè)置 Go 應(yīng)用程序的版本信息
        20. 在 Go 里面如何使用 Flag 包

        了解 Go 中的指針

        簡介

        當(dāng)你用 Go 編寫軟件時,你會編寫函數(shù)和方法。你將數(shù)據(jù)作為 參數(shù) 傳遞給這些函數(shù)。

        有時,函數(shù)會需要一個數(shù)據(jù)的本地拷貝,你希望原始數(shù)據(jù)保持不變。

        例如,如果你是一家銀行,你有一個函數(shù)可以根據(jù)用戶選擇的儲蓄計(jì)劃來顯示他們的余額變化,你不想在客戶選擇計(jì)劃之前改變他們的實(shí)際余額,而只想用它來做計(jì)算。

        這被稱為 按值傳遞,因?yàn)槟闶窃谙蚝瘮?shù)發(fā)送變量的值,而不是變量本身。

        其他時候,你可能希望函數(shù)能夠改變原始變量中的數(shù)據(jù)。

        例如,當(dāng)銀行客戶向其賬戶存款時,你希望存款函數(shù)能夠訪問實(shí)際的余額,而不是一個副本。在這種情況下,你不需要向函數(shù)發(fā)送實(shí)際數(shù)據(jù), 而只需要告訴函數(shù)數(shù)據(jù)在內(nèi)存中的位置。

        一個叫做 指針 的數(shù)據(jù)類型持有數(shù)據(jù)的內(nèi)存地址,但不是數(shù)據(jù)本身。內(nèi)存地址告訴函數(shù)在哪里可以找到數(shù)據(jù),而不是數(shù)據(jù)的值。你可以把指針傳給函數(shù)而不是實(shí)際的數(shù)據(jù),然后函數(shù)就可以在原地改變原始變量的值。

        這被稱為 通過引用傳遞,因?yàn)樽兞康闹挡]有傳遞給函數(shù),而是傳遞了它指向的位置。

        在這篇文章中,你將創(chuàng)建并使用指針來分享對一個變量的內(nèi)存空間的訪問。


        定義和使用指針

        當(dāng)你使用一個指向變量的指針時,有幾個不同的語法元素你需要了解。

        第一個是與號(&)的使用。如果你在一個變量名稱前面加一個與號,你就說明你想獲得 地址,或者說是該變量的一個指針。

        第二個語法元素是使用星號(*)或 引用 操作符。當(dāng)你聲明一個指針變量時,你在變量名后面加上指針指向的變量類型,前面加一個*,像這樣:

              
              var?myPointer?*int32?=?&someint

        這將創(chuàng)建 myPointer 作為一個指向 int32 變量的指針,并以 someint 的地址初始化該指針。指針實(shí)際上并不包含一個 int32,而只是一個地址。

        讓我們來看看一個指向 string 的指針。下面的代碼既聲明了一個字符串的值,又聲明了一個指向字符串的指針:

        main.go
              
              package?main

        import?"fmt"

        func?main()?{
        ?var?creature?string?=?"shark"
        ?var?pointer?*string?=?&creature

        ?fmt.Println("creature?=",?creature)
        ?fmt.Println("pointer?=",?pointer)
        }

        用以下命令運(yùn)行該程序:

              
              go?run?main.go

        當(dāng)你運(yùn)行程序時,它將打印出變量的值,以及該變量的存儲地址(指針地址)。

        內(nèi)存地址是一個十六進(jìn)制的數(shù)字,并不是為了讓人看懂。

        在實(shí)踐中,你可能永遠(yuǎn)不會輸出內(nèi)存地址來查看它。

        我們給你看是為了說明問題。因?yàn)槊總€程序運(yùn)行時都是在自己的內(nèi)存空間中創(chuàng)建的,所以每次運(yùn)行時指針的值都會不同,也會與下面顯示的輸出不同:

              
              creature?=?shark
        pointer?=?0xc0000721e0

        我們定義的第一個變量名為 creature,并將其設(shè)置為一個 string,其值為 shark 。

        然后我們創(chuàng)建了另一個名為 pointer 的變量。這一次,我們將 pointer 變量的值設(shè)置為 creature 變量的地址。我們通過使用與號(&)符號將一個值的地址存儲在一個變量中。

        這意味著 pointer 變量存儲的是 creature 變量的 地址 ,而不是實(shí)際值。這就是為什么當(dāng)我們打印出 pointer 的值時,我們收到的值是 0xc0000721e0 ,這是 creature 變量目前在計(jì)算機(jī)內(nèi)存中的地址。

        如果你想打印出 pointer 變量所指向的變量的值,你需要 解引用 該變量。

        下面的代碼使用 * 操作符來解除對 pointer 變量的引用并檢索其值。

        main.go
              
              package?main

        import?"fmt"

        func?main()?{
        ?var?creature?string?=?"shark"
        ?var?pointer?*string?=?&creature

        ?fmt.Println("creature?=",?creature)
        ?fmt.Println("pointer?=",?pointer)

        ?fmt.Println("*pointer?=",?*pointer)
        }

        如果你運(yùn)行這段代碼,你會看到以下輸出:

              
              creature?=?shark
        pointer?=?0xc000010200
        *pointer?=?shark

        我們添加的最后一行現(xiàn)在解除了對 pointer 變量的引用,并打印出了存儲在該地址的值。?

        如果你想修改存儲在 pointer 變量位置的值,你也可以使用解除引用操作:

        main.go
              
              package?main

        import?"fmt"

        func?main()?{
        ?var?creature?string?=?"shark"
        ?var?pointer?*string?=?&creature

        ?fmt.Println("creature?=",?creature)
        ?fmt.Println("pointer?=",?pointer)

        ?fmt.Println("*pointer?=",?*pointer)

        ?*pointer?=?"jellyfish"
        ?fmt.Println("*pointer?=",?*pointer)
        }

        運(yùn)行這段代碼可以看到輸出:

              
              creature?=?shark
        pointer?=?0xc000094040
        *pointer?=?shark
        *pointer?=?jellyfish

        我們通過在變量名稱前使用星號(*)來設(shè)置 pointer 變量所指的值,然后提供一個 jellyfish 的新值。

        正如你所看到的,當(dāng)我們打印解引用的值時,它現(xiàn)在被設(shè)置為 jellyfish 。?

        你可能沒有意識到,但實(shí)際上我們也改變了 creature 變量的值。

        這是因?yàn)?pointer 變量實(shí)際上是指向 creature 變量的地址。這意味著如果我們改變了 pointer 變量所指向的值,同時我們也會改變 creature 變量的值。

        main.go
              
              package?main

        import?"fmt"

        func?main()?{
        ?var?creature?string?=?"shark"
        ?var?pointer?*string?=?&creature

        ?fmt.Println("creature?=",?creature)
        ?fmt.Println("pointer?=",?pointer)

        ?fmt.Println("*pointer?=",?*pointer)

        ?*pointer?=?"jellyfish"
        ?fmt.Println("*pointer?=",?*pointer)

        ?fmt.Println("creature?=",?creature)
        }

        輸出看起來像這樣:

              
              creature?=?shark
        pointer?=?0xc000010200
        *pointer?=?shark
        *pointer?=?jellyfish
        creature?=?jellyfish

        雖然這段代碼說明了指針的工作原理,但這并不是你在 Go 中使用指針的典型方式。

        更常見的是在定義函數(shù)參數(shù)和返回值時使用它們,或者在定義自定義類型的方法時使用它們。

        讓我們看看如何在函數(shù)中使用指針來共享對一個變量的訪問。同樣,請記住,我們正在打印 pointer 的值,是為了說明它是一個指針。

        在實(shí)踐中,你不會使用指針的值,除了引用底層的值來檢索或更新該值之外。


        函數(shù)指針接收器

        當(dāng)你寫一個函數(shù)時,你可以定義參數(shù),以 引用 的方式傳遞。

        通過 傳遞意味著該值的副本被發(fā)送到函數(shù)中,并且在該函數(shù)中對該參數(shù)的任何改變 在該函數(shù)中影響該變量,而不是從哪里傳遞。

        然而,如果你通過 引用 傳遞,意味著你傳遞了一個指向該參數(shù)的指針,你可以在函數(shù)中改變該值,也可以改變傳遞進(jìn)來的原始變量的值。

        你可以在我們的《如何在 Go 中定義和調(diào)用函數(shù)》(點(diǎn)擊跳轉(zhuǎn)查看哦)中閱讀更多關(guān)于如何定義函數(shù)的信息。

        什么時候傳遞一個指針,什么時候發(fā)送一個值,都取決于你是否希望這個值發(fā)生變化。

        如果你不希望數(shù)值改變,就把它作為一個值來發(fā)送。如果你希望你傳遞給你的變量的函數(shù)能夠改變它,那么你就把它作為一個指針傳遞。

        為了看到區(qū)別,讓我們先看看一個通過 傳遞參數(shù)的函數(shù):

        main.go
              
              package?main

        import?"fmt"

        type?Creature?struct?{
        ?Species?string
        }

        func?main()?{
        ?var?creature?Creature?=?Creature{Species:?"shark"}

        ?fmt.Printf("1)?%+v\n",?creature)
        ?changeCreature(creature)
        ?fmt.Printf("3)?%+v\n",?creature)
        }

        func?changeCreature(creature?Creature)?{
        ?creature.Species?=?"jellyfish"
        ?fmt.Printf("2)?%+v\n",?creature)
        }

        輸出看起來像這樣:

              
              1)?{Species:shark}
        2)?{Species:jellyfish}
        3)?{Species:shark}

        首先我們創(chuàng)建了一個名為 Creature 的自定義類型。它有一個名為 Species 的字段,它是一個字符串。在 main 函數(shù)中,我們創(chuàng)建了一個名為Creature 的新類型實(shí)例,并將Species 字段設(shè)置為shark 。

        然后我們打印出變量,以顯示存儲在 creature 變量中的當(dāng)前值。

        接下來,我們調(diào)用 changeCreature,并傳入 creature 變量的副本。

        changeCreature 被定義為接受一個名為 creature 的參數(shù),并且它是我們之前定義的 Creature 類型的函數(shù)。

        然后我們將Species 字段的值改為 jellyfish 并打印出來。

        注意在 changeCreature 函數(shù)中,Species 的值現(xiàn)在是 jellyfish,并且打印出 2) {Species:jellyfish}。這是因?yàn)槲覀儽辉试S在我們的函數(shù)范圍內(nèi)改變這個值。

        然而,當(dāng) main 函數(shù)的最后一行打印出 creature 的值時,Species 的值仍然是 shark 。

        值沒有變化的原因是我們通過 傳遞變量。這意味著在內(nèi)存中創(chuàng)建了一個值的副本,并傳遞給 changeCreature 函數(shù)。這允許我們有一個函數(shù),可以根據(jù)需要對傳入的任何參數(shù)進(jìn)行修改,但不會影響函數(shù)之外的任何變量。

        接下來,讓我們改變 changeCreature 函數(shù),使其通過 引用 接受一個參數(shù)。

        我們可以通過使用星號(*)操作符將類型從 Creature 改為指針來做到這一點(diǎn)。我們現(xiàn)在傳遞的不是一個 Creature,而是一個指向 Creature 的指針,或者是一個 *Creature。

        在前面的例子中,creature 是一個 struct,它的 Species 值為 shark。*creature 是一個指針,不是一個結(jié)構(gòu)體,所以它的值是一個內(nèi)存位置,這就是我們傳遞給 changeCreature() 真正的東西。

        main.go
              
              package?main

        import?"fmt"

        type?Creature?struct?{
        ?Species?string
        }

        func?main()?{
        ?var?creature?Creature?=?Creature{Species:?"shark"}

        ?fmt.Printf("1)?%+v\n",?creature)
        ?changeCreature(&creature)
        ?fmt.Printf("3)?%+v\n",?creature)
        }

        func?changeCreature(creature?*Creature)?{
        ?creature.Species?=?"jellyfish"
        ?fmt.Printf("2)?%+v\n",?creature)
        }

        運(yùn)行這段代碼可以看到以下輸出:

              
              1)?{Species:shark}
        2)?&{Species:jellyfish}
        3)?{Species:jellyfish}

        注意,現(xiàn)在當(dāng)我們在 changeCreature 函數(shù)中把 Species 的值改為 jellyfish 時,它也改變了 main 函數(shù)中定義的原始值。

        這是因?yàn)槲覀兺ㄟ^ 引用 傳遞了 creature 變量,它允許訪問內(nèi)存里的原始值并可以根據(jù)需要改變它。?

        因此,如果你想讓一個函數(shù)能夠改變一個值,你需要通過引用來傳遞它。要通過引用傳遞,你就需要傳遞變量的指針,而不是變量本身。

        然而,有時你可能沒有為一個指針定義一個實(shí)際的值。在這些情況下,有可能在程序中出現(xiàn) 恐慌(點(diǎn)擊跳轉(zhuǎn)查看哦)。

        讓我們來看看這種情況是如何發(fā)生的,以及如何對這種潛在的問題進(jìn)行規(guī)劃。


        空指針

        Go 中的所有變量都有一個零值(點(diǎn)擊跳轉(zhuǎn)查看哦)。

        即使對指針來說也是如此。如果你聲明了一個類型的指針,但是沒有賦值,那么零值將是 nil。nil 是一種表示變量 "沒有被初始化" 的方式。

        在下面的程序中,我們定義了一個指向 Creature 類型的指針,但是我們從來沒有實(shí)例化過 Creature 的實(shí)際實(shí)例,也沒有將它的地址分配給 creature 指針變量。該值將是 nil,因此我們不能引用任何定義在 Creature 類型上的字段或方法:

        main.go
              
              package?main

        import?"fmt"

        type?Creature?struct?{
        ?Species?string
        }

        func?main()?{
        ?var?creature?*Creature

        ?fmt.Printf("1)?%+v\n",?creature)
        ?changeCreature(creature)
        ?fmt.Printf("3)?%+v\n",?creature)
        }

        func?changeCreature(creature?*Creature)?{
        ?creature.Species?=?"jellyfish"
        ?fmt.Printf("2)?%+v\n",?creature)
        }

        輸出看起來像這樣:

              
              1)?<nil>
        panic:?runtime?error:?invalid?memory?address?or?nil?pointer?dereference
        [signal?SIGSEGV:?segmentation?violation?code=0x1?addr=0x8?pc=0x109ac86]

        goroutine?1?[running]:
        main.changeCreature(0x0)
        ????????/Users/corylanou/projects/learn/src/github.com/gopherguides/learn/_training/digital-ocean/pointers/src/nil.go:18?+0x26
        ?main.main()
        ?????????/Users/corylanou/projects/learn/src/github.com/gopherguides/learn/_training/digital-ocean/pointers/src/nil.go:13?+0x98
        ??exit?status?2

        當(dāng)我們運(yùn)行程序時,它打印出了 creature 變量的值,該值是 <nil>

        然后我們調(diào)用 changeCreature 函數(shù),當(dāng)該函數(shù)試圖設(shè)置 Species 字段的值時,它 panics?(恐慌) 了。

        這是因?yàn)閷?shí)際上沒有創(chuàng)建 creature 變量的實(shí)例。正因?yàn)槿绱?,程序沒有地方可以實(shí)際存儲這個值,所以程序就恐慌了。

        在 Go 中很常見的是,如果你以指針的形式接收一個參數(shù),在對它進(jìn)行任何操作之前,你要檢查它是否為 nil,以防止程序恐慌。

        這是檢查 nil 的一種常見方法:

              
              if?someVariable?==?nil?{
        ?//?print?an?error?or?return?from?the?method?or?fuction
        }

        實(shí)際上,你想確保你沒有一個 nil 指針被傳入你的函數(shù)或方法。

        如果有的話,你可能只想返回,或者返回一個錯誤,以表明一個無效的參數(shù)被傳遞到函數(shù)或方法中。

        下面的代碼演示了對 nil 的檢查:

        main.go
              
              package?main

        import?"fmt"

        type?Creature?struct?{
        ?Species?string
        }

        func?main()?{
        ?var?creature?*Creature

        ?fmt.Printf("1)?%+v\n",?creature)
        ?changeCreature(creature)
        ?fmt.Printf("3)?%+v\n",?creature)
        }

        func?changeCreature(creature?*Creature)?{
        ?if?creature?==?nil?{
        ??fmt.Println("creature?is?nil")
        ??return
        ?}

        ?creature.Species?=?"jellyfish"
        ?fmt.Printf("2)?%+v\n",?creature)
        }

        我們在 changeCreature 中添加了一個檢查,看 creature 參數(shù)的值是否為 nil。如果是,我們打印出 "creature is nil",并返回函數(shù)。否則,我們繼續(xù)并改變 Species 字段的值。

        如果我們運(yùn)行該程序,我們現(xiàn)在將得到以下輸出:

              
              1)?<nil>
        creature?is?nil
        3)?<nil>

        請注意,雖然我們?nèi)匀粸?creature 變量設(shè)置了 nil 值,但我們不再恐慌,因?yàn)槲覀冋跈z查這種情況。

        最后,如果我們創(chuàng)建一個 Creature 類型的實(shí)例,并將其賦值給 creature 變量,程序現(xiàn)在將按照預(yù)期改變值:

        main.go
              
              package?main

        import?"fmt"

        type?Creature?struct?{
        ?Species?string
        }

        func?main()?{
        ?var?creature?*Creature
        ?creature?=?&Creature{Species:?"shark"}

        ?fmt.Printf("1)?%+v\n",?creature)
        ?changeCreature(creature)
        ?fmt.Printf("3)?%+v\n",?creature)
        }

        func?changeCreature(creature?*Creature)?{
        ?if?creature?==?nil?{
        ??fmt.Println("creature?is?nil")
        ??return
        ?}

        ?creature.Species?=?"jellyfish"
        ?fmt.Printf("2)?%+v\n",?creature)
        }

        現(xiàn)在我們有了一個 Creature 類型的實(shí)例,程序?qū)⑦\(yùn)行,我們將得到以下預(yù)期輸出:

              
              1)?&{Species:shark}
        2)?&{Species:jellyfish}
        3)?&{Species:jellyfish}

        當(dāng)你在使用指針時,程序有可能會出現(xiàn)恐慌。為了避免恐慌,你應(yīng)該在試圖訪問任何字段或定義在其上的方法之前,檢查一個指針值是否為 nil。

        接下來,讓我們看看使用指針和值是如何影響在一個類型上定義方法的。


        方法指針接收器

        Go 中的 接收器 是指在方法聲明中定義的參數(shù)。看一下下面的代碼:

              
              type?Creature?struct?{
        ?Species?string
        }

        func?(c?Creature)?String()?string?{
        ?return?c.Species
        }

        這個方法的接收器是 c Creature。它說明 c 的實(shí)例屬于 Creature 類型,你將通過該實(shí)例變量引用該類型。

        方法跟函數(shù)一樣,也是根據(jù)你送入的參數(shù)是指針還是值而有不同的行為。

        最大的區(qū)別是,如果你用一個值接收器定義一個方法,你就不能對該方法所定義的那個類型的實(shí)例進(jìn)行修改。

        有的時候,你希望你的方法能夠更新你所使用的變量的實(shí)例。為了實(shí)現(xiàn)這一點(diǎn),你會想讓接收器成為一個指針。

        讓我們給我們的 Creature 類型添加一個 Reset 方法,將 Species字段設(shè)置為一個空字符串:

        main.go
              
              package?main

        import?"fmt"

        type?Creature?struct?{
        ?Species?string
        }

        func?(c?Creature)?Reset()?{
        ?c.Species?=?""
        }

        func?main()?{
        ?var?creature?Creature?=?Creature{Species:?"shark"}

        ?fmt.Printf("1)?%+v\n",?creature)
        ?creature.Reset()
        ?fmt.Printf("2)?%+v\n",?creature)
        }

        如果我們運(yùn)行該程序,我們將得到以下輸出:

              
              1)?{Species:shark}
        2)?{Species:shark}

        注意到即使在 Reset 方法中我們將 Species 的值設(shè)置為空字符串,當(dāng)我們在 main 函數(shù)中打印出 creature 變量的值時,該值仍然被設(shè)置為 shark 。

        這是因?yàn)槲覀兌x的 Reset 方法有一個 接收器。這意味著該方法只能訪問 creature 變量的 副本。

        如果我們想在方法中修改 creature 變量的實(shí)例,我們需要將它們定義為有一個 指針 接收器:

        main.go
              
              package?main

        import?"fmt"

        type?Creature?struct?{
        ?Species?string
        }

        func?(c?*Creature)?Reset()?{
        ?c.Species?=?""
        }

        func?main()?{
        ?var?creature?Creature?=?Creature{Species:?"shark"}

        ?fmt.Printf("1)?%+v\n",?creature)
        ?creature.Reset()
        ?fmt.Printf("2)?%+v\n",?creature)
        }

        注意,我們現(xiàn)在在定義 Reset 方法時,在 Creature 類型前面添加了一個星號(*)。這意味著傳遞給 Reset 方法的 Creature 實(shí)例現(xiàn)在是一個指針,因此當(dāng)我們進(jìn)行修改時,將影響到該變量的原始實(shí)例。

              
              1)?{Species:shark}
        2)?{Species:}

        現(xiàn)在 Reset 方法已經(jīng)改變了 Species 字段的值。


        總結(jié)

        將一個函數(shù)或方法定義為通過 或通過 引用,將影響你的程序的哪些部分能夠?qū)ζ渌糠诌M(jìn)行修改??刂圃撟兞亢螘r能被改變,將使你能寫出更健壯和可預(yù)測的軟件。

        現(xiàn)在你已經(jīng)了解了指針,你也可以看到它們是如何在接口中使用的了。


        往期推薦



        354bc91d24f5720b88056a93d6132a49.webp

        Go 1.20正式發(fā)布,又變得巨快無比了

        52847cbd3b33366e24087ca360c0d1dd.webp

        「每周譯Go」用構(gòu)建標(biāo)簽定制Go二進(jìn)制文件


        f02175a4532b382e7b877aef09a4748f.webp

        Go 1.20新變化!第一部分:語言特性

        想要了解Go更多內(nèi)容,歡迎掃描下方??關(guān)注公眾號, 回復(fù)關(guān)鍵詞 [實(shí)戰(zhàn)群]? ?,就有機(jī)會進(jìn)群和我們進(jìn)行交流

        分享、在看與點(diǎn)贊Go? 0e12da5e68f103cf5f35cbf3f755de0d.webp
        瀏覽 45
        點(diǎn)贊
        評論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點(diǎn)贊
        評論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報
          
          

            1. 贪婪洞窟h5双开 国产乱╳╳ | 99成人乱码一区二区三区在线豆花视频 | 思思热免费在线视频 | 国产精品苏妲己野外勾搭 | 男人的天堂毛片 |