golang的傳值調用和傳引用調用

来源:https://www.cnblogs.com/wang_yb/archive/2019/12/31/12126884.html
-Advertisement-
Play Games

傳值還是傳引用 調用函數時, 傳入的參數的 傳值 還是 傳引用 , 幾乎是每種編程語言都會關註的問題. 最近在使用 golang 的時候, 由於 傳值 和 傳引用 的方式沒有弄清楚, 導致了 BUG. 經過深入的嘗試, 終於弄明白了 golang 的 傳值 的 傳引用 , 嘗試過程記錄如下, 供大家 ...


傳值還是傳引用

調用函數時, 傳入的參數的 傳值 還是 傳引用, 幾乎是每種編程語言都會關註的問題. 最近在使用 golang 的時候, 由於 傳值傳引用 的方式沒有弄清楚, 導致了 BUG.

經過深入的嘗試, 終於弄明白了 golang 的 傳值傳引用, 嘗試過程記錄如下, 供大家參考!

golang 本質上都是傳值方式調用

嚴格來說, golang 中都是傳值調用, 下麵通過例子一一說明

普通類型的參數

這裡的普通類型, 指的是 int, string 等原始的數據類型, 這些類型作為函數參數時, 都是 傳值 調用. 這個基本沒什麼疑問.

func param_ref_test01() {
 var t1 = 0
 var t2 = "000"

 var f1 = func(p int) {
   p += 1
 }

 var f2 = func(p string) {
   p += "-changed"
 }

 fmt.Printf(">>>調用前: t1 = %d  t2 = %s\n", t1, t2)
 f1(t1)
 f2(t2)
 fmt.Printf("<<<調用後: t1 = %d  t2 = %s\n", t1, t2)
}

運行的結果:

>>>調用前: t1 = 0  t2 = 000
<<<調用後: t1 = 0  t2 = 000

struct 指針, map, slice 類型的參數

對於這種類型的參數, 錶面上是 傳引用 調用, 我也被這個錶面現象迷惑過…

func param_ref_test02() {
 type Person struct {
   Name string
   Age  int
 }

 var t3 = &Person{
   Name: "test",
   Age:  10,
 }
 var t4 = []string{"a", "b", "c"}
 var t5 = make(map[string]int)
 t5["hello"] = 1
 t5["world"] = 2

 var f3 = func(p *Person) {
   p.Name = "test-change"
   p.Age = 20
 }

 var f4 = func(p []string) {
   p[0] = "aa"
   p = append(p, "d")
 }

 var f5 = func(p map[string]int) {
   p["hello"] = 11
   p["hello2"] = 22
 }

 fmt.Printf(">>>調用前: t3 = %v  t4 = %v  t5 = %v\n", t3, t4, t5)
 f3(t3)
 f4(t4)
 f5(t5)
 fmt.Printf("<<<調用後: t3 = %v  t4 = %v  t5 = %v\n", t3, t4, t5)
}

運行的結果:

>>>調用前: t3 = &{test 10}  t4 = [a b c]  t5 = map[hello:1 world:2]
<<<調用後: t3 = &{test-change 20}  t4 = [aa b c]  t5 = map[hello:11 hello2:22 world:2]

從運行結果中, 可以看出基本符合 傳引用 調用的特征, 除了 t4 的 append 沒有生效之外

既然都是傳值調用, 為什麼 f3 內修改了 *Person, 會導致外面的 t3 改變

改造下 f3, 將變數的地址列印出來

func param_ref_test03() {
  type Person struct {
    Name string
    Age  int
  }

  var t3 = &Person{
    Name: "test",
    Age:  10,
  }

  var f3 = func(p *Person) {
    p.Name = "test-change"
    p.Age = 20
    fmt.Printf("參數p 指向的記憶體地址 = %p\n", p)
    fmt.Printf("參數p 記憶體地址 = %p\n", &p)
  }
  fmt.Printf("t3 指向的記憶體地址 = %p\n", t3)
  fmt.Printf("t3 的記憶體地址 = %p\n", &t3)
  f3(t3)
}

運行的結果:

t3 指向的記憶體地址 = 0xc00000fe20
t3 的記憶體地址 = 0xc000010570
參數p 指向的記憶體地址 = 0xc00000fe20
參數p 記憶體地址 = 0xc000010578

從結果可以看出, t3 和 p 都是指針類型, 但是它們的記憶體地址是不一樣的, 所以這是一個 傳值 調用. 但是, 它們指向的地址(0xc00000fe20)是一樣的, 所以通過 p 修改了指向的數據(*Person), t3 指向的數據也發生了變化.

只要 p 的指向地址變化, 就不會影響 t3 的變化了

var f3 = func(p *Person) {
 p = &Person{}   // 這行會改變p指向的地址
 p.Name = "test-change"
 p.Age = 20
}
f3(t3)

可以試試看, 只要加上上面代碼中有註釋的那行, 調用 f3 就不會改變 t3 了.

既然都是傳值調用, 為什麼 f4 內修改了 []string, 會導致外面的 t4 改變

golang 中的 slice 也是指針類型, 所以和上面 *Person 的原因一樣

為什麼 f4 內對 []string append 之後, 沒有導致外面的 t4 改變

代碼是最好的解釋, 先觀察 append 之後記憶體地址的變化, 我們再分析

func param_ref_test04() {
  var s = []string{"a", "b", "c"}
  fmt.Printf("s 的記憶體地址 = %p\n", &s)
  fmt.Printf("s 指向的記憶體地址 = %p\n", s)
  s[0] = "aa"
  fmt.Printf("修改s[0] 之後, s 的記憶體地址 = %p\n", &s)
  fmt.Printf("修改s[0] 之後, s 指向的記憶體地址 = %p\n", s)
  s = append(s, "d")
  fmt.Printf("append之後, s 的記憶體地址 = %p\n", &s)
  fmt.Printf("append之後, s 指向的記憶體地址 = %p\n", s)
}

運行的結果:

s 的記憶體地址 = 0xc00008fec0
s 指向的記憶體地址 = 0xc00016d530
修改s[0] 之後, s 的記憶體地址 = 0xc00008fec0
修改s[0] 之後, s 指向的記憶體地址 = 0xc00016d530
append之後, s 的記憶體地址 = 0xc00008fec0
append之後, s 指向的記憶體地址 = 0xc000096f00

首先, 無論是修改 slice 中的元素, 還是添加 slice 的元素, 都不會改變 s 本身的地址(0xc00008fec0) 其次, 修改 slice 中的元素, 不會改變 s 指向的地址(0xc00016d530), 所有在 f4 中修改 slice 的元素, 也會改變函數 f4 外面的變數 最後, append 操作會修改 s 指向的地址, append 之後, s 和 函數 f4 外的變數已經不是指向同一地址了, 所以 append 的元素不會影響函數 f4 外的變數

既然都是傳值調用, 為什麼 f5 內修改了 map, 會導致外面的 t5 改變

map 類型也是指針類型, 所以原因和上面的 *Person 一樣

為什麼 f5 內增加了 map 中元素, 會導致外面的 t5 改變, 沒有像 t4 那樣, 只變修改的部分, 不變新增的部分

同樣, 看代碼

func param_ref_test05() {
  var m = make(map[string]int)
  m["hello"] = 1
  m["world"] = 2
  fmt.Printf("m 的記憶體地址 = %p\n", &m)
  fmt.Printf("m 指向的記憶體地址 = %p\n", m)
  m["hello"] = 11
  fmt.Printf("修改m 之後, m 的記憶體地址 = %p\n", &m)
  fmt.Printf("修改m 之後, m 指向的記憶體地址 = %p\n", m)
  m["hello2"] = 22
  fmt.Printf("追加元素之後, m 的記憶體地址 = %p\n", &m)
  fmt.Printf("追加元素之後, m 指向的記憶體地址 = %p\n", m)
}

運行的結果:

m 的記憶體地址 = 0xc000010598
m 指向的記憶體地址 = 0xc000151590
修改m 之後, m 的記憶體地址 = 0xc000010598
修改m 之後, m 指向的記憶體地址 = 0xc000151590
追加元素之後, m 的記憶體地址 = 0xc000010598
追加元素之後, m 指向的記憶體地址 = 0xc000151590

根據上面的分析經驗, 一目瞭然, 因為無論是修改還是添加 map 中的元素, m 指向的地址(0xc000151590)都沒變, 所以函數 f5 中 map 參數修改元素, 添加元素之後, 都會影響函數 f5 之外的變數.

註意 這裡並不是說 map 類型的參數就是 傳引用 調用, 它仍然是 傳值 調用, 參數 map 的地址和函數 f5 外的變數 t5 的地址是不一樣的 如果在函數 f5 中修改的 map 類型參數的指向地址, 就會像傳值調用那樣, 不影響函數 f5 外 t5 的值

func param_ref_test06() {
 var t5 = make(map[string]int)
 t5["hello"] = 1
 t5["world"] = 2

 var f5 = func(p map[string]int) {
   fmt.Printf("修改前 參數p 指向的記憶體地址 = %p\n", p)
   fmt.Printf("修改前 參數p 記憶體地址 = %p\n", &p)
   p = make(map[string]int)  // 這行改變了 p 的指向, 使得 p 和 t5 不再指向同一個地方
   p["hello"] = 11
   p["hello2"] = 22
   fmt.Printf("修改後 參數p 指向的記憶體地址 = %p\n", p)
   fmt.Printf("修改後 參數p 記憶體地址 = %p\n", &p)
 }

 fmt.Printf("t5 指向的記憶體地址 = %p\n", t5)
 fmt.Printf("t5記憶體地址 = %p\n", &t5)
 fmt.Printf(">>>調用前: t5 = %v\n", t5)
 f5(t5)
 fmt.Printf("<<<調用後: t5 = %v\n", t5)
}

運行的結果:

t5 指向的記憶體地址 = 0xc000151590
t5記憶體地址 = 0xc000010598
>>>調用前: t5 = map[hello:1 world:2]
修改前 參數p 指向的記憶體地址 = 0xc000151590
修改前 參數p 記憶體地址 = 0xc0000105a0
修改後 參數p 指向的記憶體地址 = 0xc000151650
修改後 參數p 記憶體地址 = 0xc0000105a0
<<<調用後: t5 = map[hello:1 world:2]

雖然是 map 類型參數, 但是調用前後, t5 的值沒有改變.

總結

上面的嘗試不敢說有多全, 但基本可以弄清 golang 函數傳參的本質.

  1. 對於普通類型(int, string 等等), 就是 傳值 調用, 函數內對參數的修改, 不影響外面的變數
  2. 對於 struct 指針, slice 和 map 類型, 函數內對參數的修改之所以能影響外面, 是因為參數和外面的變數指向了同一塊數據的地址
  3. 對於 struct 指針, slice 和 map 類型, 函數的參數和外面的變數的地址是不一樣的, 所以本質上還是 傳值 調用
  4. slice 的 append 操作會改變 slice 指針的地址, 這個非常重要!!! 我曾經寫了一個基於 slice 的排序演算法在這個上面吃了大虧, 調研很久才發現原因…

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 一、Spring Data JPA 1、簡介 (1)官網地址: https://spring.io/projects/spring-data-jpa參考文檔: https://docs.spring.io/spring-data/jpa/docs/2.2.3.RELEASE/reference/ht ...
  • 本篇文章主要介紹PHP+swoole實現聊天群發功能,感興趣的朋友參考下,希望對大家有所幫助。 php代碼: $serv = new swoole_websocket_server("127.0.0.1",3999); //服務的基本設置 $serv->set(array( 'worker_num' ...
  • 這篇文章主要介紹了關於PHP實現微信網頁登陸授權開發,有著一定的參考價值,現在分享給大家,有需要的朋友可以參考一下 更多PHP相關知識請關註我的專欄PHP​zhuanlan.zhihu.com 微信開放平臺和公眾平臺的區別 1.公眾平臺面向的時普通的用戶,比如自媒體和媒體,企業官方微信公眾賬號運營人 ...
  • 程式入口 SpringApplication.run(BeautyApplication.class, args); 執行此方法來載入整個SpringBoot的環境。 1. 從哪兒開始? SpringApplication.java /** * Run the Spring application, ...
  • 責編 | 劉靜 天氣降溫,感情卻升溫了? 上午剛到公司,就收到小Q發來的靈魂拷問: ​ ​ “Q仔!要不然下午請個假!我帶你去精神科看看!?”我實在忍不了,脫口而出。 話音未落,前排的運營小花回頭看向小Q,莞爾一笑,百媚橫生。 ​ 這個悶騷小伙子什麼時候勾搭上運營一枝花了?我正要追問,小Q看穿了我的 ...
  • Talk is cheap, show me the code! 以上這段mybatis的入門案例代碼,相信每一個瞭解mybatis的朋友都能看得懂,知碼醬同學今天也細細品了品! 1. 項目的路徑問題 : 在實際的項目中,並不推薦眾所周知的相對路徑和絕對路徑。 相對路徑: web應用是需要部署到服務 ...
  • 一、生成表格1.創建模型類(在 models.py文件中創建一個person類並且繼承models.Models類) 2.生成表格(在項目目錄下)(1)生成遷移文件:在pycharm下方的命令行Terminal中寫入python manage.py makemigrations,回車鍵後顯示遷移文件 ...
  • Mapper代理 "上一節" 中直接利用session+id來執行sql的方式存在一些問題 session執行sql時都需要提供要執行sql的id,而這個id是字元串類型,意味著id是否正確在編譯期間是無法獲知的,必須等到運行時才能發現錯誤, sql需要的參數和返回值類都不明確,這也增加了出錯的概率 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...