判斷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
  • 在本篇教程中,我們學習瞭如何使用 Taurus.MVC WebMVC 框架創建一個簡單的頁面。 我們創建了一個控制器並編寫了一個用於呈現頁面的方法,然後創建了對應的視圖,並最終成功運行了應用程式。 在下一篇教程中,我們將繼續探索 Taurus.MVC WebMVC 框架的更多功能和用法。 ...
  • 一:背景 1. 講故事 很多.NET開發者在學習高級調試的時候,使用sos的命令輸出會發現這裡也看不懂那裡也看不懂,比如截圖中的這位朋友。 .NET高級調試屬於一個偏冷門的領域,國內可觀測的資料比較少,所以很多東西需要你自己去探究源代碼,然後用各種調試工具去驗證,相關源代碼如下: coreclr: ...
  • 我一直都以為c中除以2的n次方可以使用右移n位代替,然而在實際調試中發現並不都是這樣的。是在計算餘數是發現了異常 被除數:114325068 右移15計算結果:3488 除法取整計算結果:3489 右移操作計算餘數:33772 除法取整計算餘數:1005 顯然:這是不一樣的。 移位操作是一條cpu指 ...
  • 在上一篇文章中,我們介紹了ReentrantLock類的一些基本用法,今天我們重點來介紹一下ReentrantLock其它的常用方法,以便對ReentrantLock類的使用有更深入的理解。 ...
  • Excelize 是 Go 語言編寫的用於操作電子錶格辦公文檔的開源基礎庫,2024年2月26日,社區正式發佈了 2.8.1 版本,該版本包含了多項新增功能、錯誤修複和相容性提升優化。 ...
  • 雲採用框架(Cloud Adoption Framework,簡稱CAF)為企業上雲提供策略和技術的指導原則和最佳實踐,幫助企業上好雲、用好雲、管好雲,併成功實現業務目標。本雲採用框架是基於服務大量企業客戶的經驗總結,將企業雲採用分為四個階段,並詳細探討企業應在每個階段採取的業務和技術策略;同時,還 ...
  • 與TXT文本文件,PDF文件更加專業也更適合傳輸,常用於正式報告、簡歷、合同等場合。項目中如果有使用Java將TXT文本文件轉為PDF文件的需求,可以查看本文中介紹的免費實現方法。 免費Java PDF庫 本文介紹的方法需要用到Free Spire.PDF for Java,該免費庫支持多種操作、轉 ...
  • 指針和引用 當我們需要在程式中傳遞變數的地址時,可以使用指針或引用。它們都可以用來間接訪問變數,但它們之間有一些重要的區別。 指針是一個變數,它存儲另一個變數的地址。通過指針,我們可以訪問存儲在該地址中的變數。指針可以被重新分配,可以指向不同的變數,也可以為NULL。指針使用*運算符來訪問存儲在地址 ...
  • 即使再小再簡單的需求,作為研發開發完畢之後,我們可以直接上線麽?其實很多時候事故往往就是由於“不以為意”發生的。事故的發生往往也遵循“墨菲定律”,這就要求我們更要敬畏線上,再小的需求點都需要經過嚴格的測試驗證才能上線。 ...
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 一、是什麼 許可權是對特定資源的訪問許可,所謂許可權控制,也就是確保用戶只能訪問到被分配的資源 而前端許可權歸根結底是請求的發起權,請求的發起可能有下麵兩種形式觸發 頁面載入觸發 頁面上的按鈕點擊觸發 總的來說,所有的請求發起都觸發自前端路由或 ...