判斷go對象是否能直接賦值進行深拷貝

来源:https://www.cnblogs.com/janbar/archive/2023/01/29/17072751.html
-Advertisement-
Play Games

在golang中可以使用a := b這種方式將b賦值給a,只有當b能進行深拷貝時a與b才不會互相影響,否則就需要進行更為複雜的深拷貝。 下麵就是Go賦值操作的一個說明: Go語言中所有賦值操作都是值傳遞,如果結構中不含指針,則直接賦值就是深度拷貝;如果結構中含有指針(包括自定義指針,以及切片,map ...


golang中可以使用a := b這種方式將b賦值給a,只有當b能進行深拷貝時ab才不會互相影響,否則就需要進行更為複雜的深拷貝。

下麵就是Go賦值操作的一個說明:
Go語言中所有賦值操作都是值傳遞,如果結構中不含指針,則直接賦值就是深度拷貝;如果結構中含有指針(包括自定義指針,以及切片,map等使用了指針的內置類型),則數據源和拷貝之間對應指針會共同指向同一塊記憶體,這時深度拷貝需要特別處理。目前,有三種方法,一是用gob序列化成位元組序列再反序列化生成克隆對象;二是先轉換成json位元組序列,再解析位元組序列生成克隆對象;三是針對具體情況,定製化拷貝。前兩種方法雖然比較通用但是因為使用了reflex反射,性能比定製化拷貝要低出2個數量級,所以在性能要求較高的情況下應該儘量避免使用前兩者。

現在我需要判斷某個對象是否可以直接用賦值進行深拷貝,如果不能直接進行深拷貝時,到底是哪個欄位影響了深拷貝,下麵就是判斷的代碼:

package main

import (
	"bytes"
	"fmt"
	"reflect"
)

type (
	PerA struct {
		A int
		B string
		c []byte
	}
	Per struct {
		PerA
		Name string
		Age  int
	}
	BarA struct {
		A string
		b *int
	}
	Bar struct {
		A int64
		BarA
	}
	CatA struct {
		name string
		age  int
	}
	Cat struct {
		name string
		age  int
		CatA
	}
)

func main() {
	var out bytes.Buffer
	ok := CanDeepCopy(Per{}, &out)
	fmt.Println(ok, out.String())

	out.Reset()
	ok = CanDeepCopy(Bar{}, &out)
	fmt.Println(ok, out.String())

	out.Reset()
	ok = CanDeepCopy(Cat{}, &out)
	fmt.Println(ok, out.String())

	bi := 1
	b0 := Bar{A: 1, BarA: BarA{A: "11", b: &bi}}
	b1 := b0
	b1.A, b1.BarA.A, *b1.BarA.b = 2, "22", 2
	fmt.Printf("%#v,%p,%d\n", b0, &b0, *b0.BarA.b)
	fmt.Printf("%#v,%p,%d\n", b1, &b1, *b1.BarA.b)

	c0 := Cat{name: "1", age: 1, CatA: CatA{name: "1", age: 1}}
	c1 := c0
	c1.name, c1.age, c1.CatA.name, c1.CatA.age = "2", 2, "2", 2
	fmt.Printf("%#v,%p\n", c0, &c0)
	fmt.Printf("%#v,%p\n", c1, &c1)
}

func CanDeepCopy(v any, path *bytes.Buffer) bool {
	t := reflect.TypeOf(v)
	if path.Len() == 0 {
		path.WriteString(t.Name()) // 記錄首次對象名稱
	}
	switch t.Kind() {
	case reflect.Pointer: // 指針可比較,但不能深拷貝
		path.WriteString(" is pointer") // 該欄位為指針
		return false
	case reflect.Struct: // 結構體需要判斷每一個欄位
		path.WriteByte('.')
		for i, pn := 0, path.Len(); i < t.NumField(); i++ {
			tf := t.Field(i)
			path.WriteString(tf.Name) // 記錄子欄位名稱
			// 構造一個該欄位類型的對象,註意將指針換成值
			fv := reflect.New(tf.Type).Elem().Interface()
			if !CanDeepCopy(fv, path) {
				return false // 遞歸判斷每個欄位,包括匿名欄位
			}
			path.Truncate(pn) // 回溯時截斷沒問題的子欄位
		}
	}
	if t.Comparable() {
		return true
	}
	path.WriteString(" incomparable") // 該欄位不可比較
	return false
}

運行結果:

false Per.PerA.c incomparable # 說明 Per.a.c.cc 欄位屬於不可比較欄位導致不能深拷貝
false Bar.BarA.b is pointer   # 說明 Bar.BarA.b 欄位是指針導致不能深拷貝
true Cat.  # 說明 Cat 對象可以直接進行深拷貝

# 由於 Bar 不可以深拷貝
# 可以看到 b1 := b0 之後,兩個對象共用 BarA.b 指針指向對象,因此 *b1.BarA.b = 2 之後也影響了b0
main.Bar{A:1, BarA:main.BarA{A:"11", b:(*int)(0xc0000a6148)}},0xc0000a03e0,2
main.Bar{A:2, BarA:main.BarA{A:"22", b:(*int)(0xc0000a6148)}},0xc0000a0400,2

# 由於 Cat 可以深拷貝,因此 c1 := c0 之後這兩個對象互不影響,這種對象直接賦值,不用其他方案進行深拷貝
main.Cat{name:"1", age:1, CatA:main.CatA{name:"1", age:1}},0xc0000bc5d0
main.Cat{name:"2", age:2, CatA:main.CatA{name:"2", age:2}},0xc0000bc600

通過研究go賦值邏輯,理解了深拷貝和淺拷貝的邏輯。實際上go的賦值操作只存在值拷貝,由於一些引用類型賦值的是地址導致兩個變數共用記憶體數據才導致需要額外進行深拷貝處理。

同理可得函數傳參也是賦值,因此值傳遞時對象不能自動深拷貝也需要特殊處理,看如下示例:

package main

import (
	"fmt"
)

func main() {
	err := test()
	if err != nil {
		panic(err)
	}
}

type TT struct {
	a int
	b *string
}

func test() error {
	as := "123"
	t := TT{a: 123, b: &as}
	fmt.Printf("t1 %#v,%p,%s\n", t, &t, *t.b)
	a(t)
	fmt.Printf("t2 %#v,%p,%s\n", t, &t, *t.b)
	return nil
}

func a(t TT) {
	fmt.Printf("a1 %#v,%p,%s\n", t, &t, *t.b)
	*t.b = "456"
	fmt.Printf("a2 %#v,%p,%s\n", t, &t, *t.b)
}

結果如下,很多人都以為函數參數為值傳遞時被調函數參數無法影響上層函數,看來這是錯的:

t1 main.TT{a:123, b:(*string)(0xc00005a260)},0xc00005a270,123
a1 main.TT{a:123, b:(*string)(0xc00005a260)},0xc00005a2a0,123
a2 main.TT{a:123, b:(*string)(0xc00005a260)},0xc00005a2a0,456
t2 main.TT{a:123, b:(*string)(0xc00005a260)},0xc00005a270,456

如下所示值類型對象方法也是能夠影響引用類型數據的:

package main

import (
	"fmt"
)

func main() {
	bs := "123"
	t := TT{a: 1, b: &bs}
	fmt.Printf("1 %#v,%p,%s\n", t, &t, *t.b)
	t.A()
	fmt.Printf("2 %#v,%p,%s\n", t, &t, *t.b)
	t.B()
	fmt.Printf("3 %#v,%p,%s\n", t, &t, *t.b)
}

type TT struct {
	a int
	b *string
}

func (t TT) A() {
	*t.b = "A"
}

func (t TT) B() {
	*t.b = "B"
}

結果如下:

# 雖然 A() 和 B() 都是值對象函數,但是結構體中指針類型屬於引用類型
1 main.TT{a:1, b:(*string)(0xc00005a260)},0xc00005a270,123
2 main.TT{a:1, b:(*string)(0xc00005a260)},0xc00005a270,A
3 main.TT{a:1, b:(*string)(0xc00005a260)},0xc00005a270,B

關於字元串的參數賦值:

package main

import (
	"fmt"
	"reflect"
	"unsafe"
)

func main() {
	s := "123"

	sh := (*reflect.StringHeader)(unsafe.Pointer(&s))
	fmt.Printf("m1 %#v,%p,%v\n", s, &s, sh.Data)
	a(s)

	b := []byte("456")
	s = *(*string)(unsafe.Pointer(&b))

	sh = (*reflect.StringHeader)(unsafe.Pointer(&s))
	fmt.Printf("m2 %#v,%p,%v\n", s, &s, sh.Data)
	a(s)

	b[0] = '6' // 修改記憶體中的數據
	sh = (*reflect.StringHeader)(unsafe.Pointer(&s))
	fmt.Printf("m3 %#v,%p,%v\n", s, &s, sh.Data)
	a(s)
}

func a(s string) {
	sh := (*reflect.StringHeader)(unsafe.Pointer(&s))
	fmt.Printf("a %#v,%p,%v\n", s, &s, sh.Data)
}

結論是,字元串傳參實際底層數據是共用的,因為字元串不可變邏輯,因此這樣更省記憶體:

m1 "123",0xc00005a260,18648789
a "123",0xc00005a280,18648789
m2 "456",0xc00005a260,824633827584
a "456",0xc00005a2b0,824633827584
m3 "656",0xc00005a260,824633827584
a "656",0xc00005a2e0,824633827584

另外還有一個關於錯誤處理的可比較特性的坑,因此強烈建議自定義error用指針,否則就得確保必須可比較:

package main

import (
	"errors"
	"fmt"
)

func main() {
	err := DoSomething(true)
	ok := errors.Is(err, ErrorA)
	fmt.Println(ok, err)

	err = DoSomething(false)
	ok = errors.Is(err, ErrorB)
	fmt.Println(ok, err)
}

type CustomError struct {
	Metadata map[string]string
	Message  string
}

func (c CustomError) Error() string {
	return c.Message
}

var (
	// ErrorA 包含不可比較欄位,在 errors.Is 中
	ErrorA = CustomError{Message: "A", Metadata: map[string]string{"Reason": "A"}}
	ErrorB = &CustomError{Message: "B", Metadata: map[string]string{"Reason": "B"}}
)

func DoSomething(isA bool) error {
	if isA {
		return ErrorA
	}
	return ErrorB
}

引用
https://www.ssgeek.com/post/golang-jie-gou-ti-lei-xing-de-shen-qian-kao-bei/
https://sorcererxw.com/articles/go-comparable-type
https://blog.csdn.net/pengpengzhou/article/details/105839518
https://www.cnblogs.com/gtea/p/16850496.html

作者:janbar 出處:https://www.cnblogs.com/janbar 本文版權歸作者和博客園所有,歡迎轉載,轉載請標明出處。喜歡我的文章請 [關註我] 吧。 如果您覺得本篇博文對您有所收穫,可點擊 [推薦] [收藏] ,或到右側 [打賞] 里請我喝杯咖啡,非常感謝。
您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 2023-01-29 一、redis事務與樂觀鎖相關命令 1、redis事務 (1)redis事務的含義 redis事務是一個單獨的隔離操作:事務中的所有命令都會序列化、按順序執行。事務在執行過程中,不會被其他客戶端送來的命令請求所打斷。 (2)redis事務的作用 redis事務的主要作用就是串聯 ...
  • 簡介 GraalVM是高性能的JDK,支持Java/Python/JavaScript等語言。它可以讓Java變成二進位文件來執行,讓程式在任何地方運行更快。這或許是Java與Go的一場戰爭? 下載安裝GraalVM 安裝GraalVM 首先到官網下載,我是直接到GitHub Release Pag ...
  • @(Matlab導入多個.mat文件並畫圖的過程詳解) 0. 實驗背景 需要導入多個.mat文件,將多組數據畫在一個圖中,並設置圖例、坐標軸、散點圖、折線圖、子圖等。 由於存在重覆操作,主要使用腳本文件編寫代碼。 figure視窗操作比較簡單,腳本文件不好寫的部分可用figure視窗操作進行輔助。 ...
  • 什麼是時區? 1884年在華盛頓召開的一次國際經度會議(又稱國際子午線會議)上,規定將全球劃分為24個時區(東、西各12個時區)。規定英國(格林尼治天文臺舊址)為中時區(零時區)、東1-12區,西1-12區。每個時區橫跨經度15度,時間正好是1小時。 時區的分類? 理論時區 上述時區定義其實是理論時 ...
  • openfeign開啟熔斷之後MDC為null,這是有前提的,首先,你的熔斷開啟後,使用的是線程池的熔斷模式,即hystrix.command.default.execution.isolation.strategy=THREAD,或者不寫這行,如果值是SEMAPHORE模式,是可以獲取到MDC對象 ...
  • 每當有新員工入職,人事小姐姐都要收集大量的工資卡信息,並且生成Excel文檔,看到小姐姐這麼辛苦,我就忍不住要去幫她了… 於是我用1行代碼就實現了自動識別銀行卡信息並且自動生成Excel文件,小姐姐當場就亮眼汪汪的看著我,搞得我都害羞了~ 第一步:識別一張銀行卡 識別銀行卡的代碼最簡單,只需要1行騰 ...
  • 1. 基礎知識 1.1 基本配置 main # 全局配置 events { # nginx 工作模式配置 } http { # http 設置 .... server { # 伺服器主機配置 .... location { # 路由配置 .... } location path { .... } l ...
  • 本文結合京東監控埋點場景,對解決樣板代碼的技術選型方案進行分析,給出最終解決方案後,結合理論和實踐進一步展開。通過關註文中的技術分析過程和技術場景,讀者可收穫一種樣板代碼思想過程和解決思路,並對Java編譯器底層有初步瞭解。 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...