判斷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
  • 下麵是一個標準的IDistributedCache用例: public class SomeService(IDistributedCache cache) { public async Task<SomeInformation> GetSomeInformationAsync (string na ...
  • 這個庫提供了在啟動期間實例化已註冊的單例,而不是在首次使用它時實例化。 單例通常在首次使用時創建,這可能會導致響應傳入請求的延遲高於平時。在註冊時創建實例有助於防止第一次Request請求的SLA 以往我們要在註冊的時候實例單例可能會這樣寫: //註冊: services.AddSingleton< ...
  • 最近公司的很多項目都要改單點登錄了,不過大部分都還沒敲定,目前立刻要做的就只有一個比較老的項目 先改一個試試手,主要目標就是最短最快實現功能 首先因為要保留原登錄方式,所以頁面上的改動就是在原來登錄頁面下加一個SSO登錄入口 用超鏈接寫的入口,頁面改造後如下圖: 其中超鏈接的 href="Staff ...
  • Like運算符很好用,特別是它所提供的其中*、?這兩種通配符,在Windows文件系統和各類項目中運用非常廣泛。 但Like運算符僅在VB中支持,在C#中,如何實現呢? 以下是關於LikeString的四種實現方式,其中第四種為Regex正則表達式實現,且在.NET Standard 2.0及以上平... ...
  • 一:背景 1. 講故事 前些天有位朋友找到我,說他們的程式記憶體會偶發性暴漲,自己分析了下是非托管記憶體問題,讓我幫忙看下怎麼回事?哈哈,看到這個dump我還是非常有興趣的,居然還有這種游戲幣自助機類型的程式,下次去大玩家看看他們出幣的機器後端是不是C#寫的?由於dump是linux上的程式,剛好win ...
  • 前言 大家好,我是老馬。很高興遇到你。 我們為 java 開發者實現了 java 版本的 nginx https://github.com/houbb/nginx4j 如果你想知道 servlet 如何處理的,可以參考我的另一個項目: 手寫從零實現簡易版 tomcat minicat 手寫 ngin ...
  • 上一次的介紹,主要圍繞如何統一去捕獲異常,以及為每一種異常添加自己的Mapper實現,並且我們知道,當在ExceptionMapper中返回非200的Response,不支持application/json的響應類型,而是寫死的text/plain類型。 Filter為二方包異常手動捕獲 參考:ht ...
  • 大家好,我是R哥。 今天分享一個爽飛了的面試輔導 case: 這個杭州兄弟空窗期 1 個月+,面試了 6 家公司 0 Offer,不知道問題出在哪,難道是杭州的 IT 崩盤了麽? 報名面試輔導後,經過一個多月的輔導打磨,現在成功入職某上市公司,漲薪 30%+,955 工作制,不咋加班,還不捲。 其他 ...
  • 引入依賴 <!--Freemarker wls--> <dependency> <groupId>org.freemarker</groupId> <artifactId>freemarker</artifactId> <version>2.3.30</version> </dependency> ...
  • 你應如何運行程式 互動式命令模式 開始一個互動式會話 一般是在操作系統命令行下輸入python,且不帶任何參數 系統路徑 如果沒有設置系統的PATH環境變數來包括Python的安裝路徑,可能需要機器上Python可執行文件的完整路徑來代替python 運行的位置:代碼位置 不要輸入的內容:提示符和註 ...