從一道面試題來談談Golang中的 ==

来源:https://www.cnblogs.com/huageyiyangdewo/archive/2023/08/05/17607844.html
-Advertisement-
Play Games

寫這篇文章的時候,已經離我找工作有一段時間了,但是覺得這道題不管是面試還是日常的工作中,都會經常遇到,所以還是特意寫一篇文章,記錄下自己對Golang中`==`的理解。如文章中出現不對的地方,請不吝賜教,謝謝。 > 註意,以下文章內容是基於 go1.16.4 進行演示的,如果和你驗證時,結果不一致, ...


寫這篇文章的時候,已經離我找工作有一段時間了,但是覺得這道題不管是面試還是日常的工作中,都會經常遇到,所以還是特意寫一篇文章,記錄下自己對Golang中==的理解。如文章中出現不對的地方,請不吝賜教,謝謝。

註意,以下文章內容是基於 go1.16.4 進行演示的,如果和你驗證時,結果不一致,可能 Go 的判斷規則有所改變。

1、面試題

大家可以先不看結果,想想答案,再看後面的結果以及相關的分析。

type T interface {

}

func main()  {

	var (
		t T
		p *T

		i1 interface{} = t
		i2 interface{} = p
	)

	fmt.Println(i1 ==t, i1 == nil)
	fmt.Println(i2 ==p, i2 == nil)
	fmt.Println(t == nil)
	fmt.Println(p == nil)

}

執行結果:

true true
true false
true
true

分析:

1、interface 值由動態類型動態值組成。只有在類型都相同時才相等。介面變數i1是介面類型的零值,也就是它的類型和值部分都是nil,介面變數i2的動態值雖然是零值,但是動態類型為 *T
2、變數 t、p 都沒有初始化,未分配記憶體,所以 變數t、p 都等於 nil。

對於上面的描述不太清楚的同學,不用著急,我們一起來學習 Golang 中的==,有較為詳細的介紹。

2、Golang中的數據類型

Golang中的數據類型分為4大類,他們分別是:

  1. 基本類型 (Primary types): 整型(int/uint/int8/uint8/int16/uint16/int32/uint32/int64/uint64/byte/rune等)、浮點數(float32/float64)、複數類型(complex64/complex128)、字元串(string)、布爾(true/false)。這些是Go語言內置的基本數據類型,它們是Go語言的原始數據類型,不能再細分。
  2. 複合類型 (Composite types):又叫聚合類型。包括數組、結構體。複合類型允許將多個值組合成一個新的數據結構。
  3. 引用類型 (Reference types):這些類型在記憶體中存儲的是數據的地址,包括 指針、切片(slice)、映射(map)、通道(channel)、函數類型(func)。引用類型允許在函數間共用和修改數據。
  4. 介面類型 (Interface types):介面類型是一種抽象類型,它定義了對象的行為,而不關心對象的具體類型。通過實現介面,可以實現多態性和代碼復用。比如 error

其實介面類型可以看作是引用類型,在 Go 中,介面類型是一種特殊的引用類型,它包含一個指向實際數據的指針以及類型信息。當你將一個具體類型的值賦給介面變數時,介面會存儲一個指向實際數據的指針或實際數據的拷貝。因此,介面可以看作是對其他類型的引用,而不是直接包含實際數據。

在Go語言中,自定義類型屬於基本類型的概念中。

自定義類型屬於基本類型的一種,它通過使用 type 關鍵字來創建新的類型,底層使用基本數據類型。通過自定義類型,我們可以為基本類型賦予更多語義,並且可以為它們定義自己的方法。自定義類型和其他基本類型具有相同的操作和運算規則,但在類型系統中它們是不同的類型。

例如使用 type number int64 時,我們自定義了一種數據類型,叫做number。雖然它底層使用了int64,但在類型系統中,numberfloat64是不同的類型。

在Go語言中,還有一種類型別名的叫法,是 Go1.9 引用的新功能。

類型別名規定:TypeAlias只是Type的別名,本質上TypeAlias與Type是同一個類型。例如:

type byte = uint8
type rune = int32

==操作最重要的一個前提是:兩個操作數類型必須相同!!!

golang 的類型系統非常嚴格,沒有C/C++/python中的隱式類型轉換。這個需要註意。

3、四大類型如何使用 ==

3.1、基本類型

基本類型的比較,就比較簡單直觀,直接使用==判斷就好了,註意的是Go中並沒有隱式轉換,而且類型一致才可以

package main

import "fmt"

func main() {
	var a int64
	var b int64
	var c int32

	fmt.Println(a == b)
	fmt.Println(c)

	// Invalid operation: a == c (mismatched types int64 and int32)
	//fmt.Println(a == c)
}

接下來我們看看浮點數的比較:

package main

import "fmt"

func main() {
	var a float64 = 0.1
	var b float64 = 0.2
	var c float64 = 0.3

	fmt.Println(a + b)  // 0.30000000000000004
	fmt.Println(a+b == c)  // false

}

是不是有點小驚訝,這個是因為Go 中的 浮點數遵循 IEEE 754 標準,所以會有有些浮點數不能精確表示,浮點運算結果會有誤差。

想大概瞭解電腦是如何表示浮點數的可以看看下麵的文章,有一個基礎的瞭解。

數字編碼

註意:

浮點數做 判等 操作一般是使用 計算兩個浮點數的差的絕對值,如果小於一定的值就認為它們相等,比如1e-9

package main

import (
	"fmt"
	"math"
)

func main() {
	var a = 0.1
	var b = 0.2
	var c = 0.3

	fmt.Println(a + b)  // 0.30000000000000004
	fmt.Println(math.Abs((a+b)-c) < 1e-9) // true
	fmt.Printf("%T", a) // float64
}

3.2、複合類型

合類型也叫做聚合類型。golang 中的複合類型只有兩種:數組和結構體。它們是逐元素/欄位比較的。

註意:數組的長度視為類型的一部分,長度不同的兩個數組是不同的類型,不能直接比較

  • 對於數組來說,依次比較各個元素的值。根據元素類型的不同,再依據是基本類型、複合類型、引用類型或介面類型,按照特定類型的規則進行比較。所有元素全都相等,數組才是相等的。
  • 對於結構體來說,依次比較各個欄位的值。根據欄位類型的不同,再依據是 4 中類型中的哪一種,按照特定類型的規則進行比較。所有欄位全都相等,結構體才是相等的。

註意:如包含了不支持直接使用 == 符號的類型,在編譯階段會報錯。

例如:

package main

import "fmt"

type Student struct {
	Name string
	Age  int
	Sex  bool
}

type S1 struct {
	Name   string
	Scores []int8  // 註意這裡定義的是 slice 類型
}

type ITest interface{}

func main() {
	arrayA := [...]int64{2, 3, 4}
	arrayB := [...]int64{2, 3, 4}
	arrayC := [...]int64{1, 3, 4}
	fmt.Println(arrayA == arrayB) // true
	fmt.Println(arrayB == arrayC) // false
	fmt.Println("-------")

	s1 := Student{"xiaoming", 18, false}
	s2 := Student{"xiaoming", 18, false}
	s3 := Student{"xiaowang", 18, false}
	fmt.Println(s1 == s2) // true
	fmt.Println(s1 == s3) // false

	fmt.Println("-------")
	a1 := [...]Student{s1, s2}
	// 註意這兩個元素!
	a2 := [2]Student{s2, s2}
	a3 := [2]Student{s2, s3}
	fmt.Println(a1 == a2) // true
	fmt.Println(a3 == a2) // false
	fmt.Println("-------")

	var i1 ITest = 23
	var i2 ITest = 23
	var i3 ITest = "tt"
	var i4 ITest = 23

	fmt.Println(i1 == i2) // true
	fmt.Println(i3 == i4) // false
	is1 := [...]ITest{i1, i2}
	is2 := [...]ITest{i1, i4}
	is3 := [...]ITest{i1, i3}

	fmt.Println(is1 == is2) // true
	fmt.Println(is1 == is3) // false

	fmt.Println("-------")

	t1 := S1{"xw", []int8{66, 88}}
	t2 := S1{"xw", []int8{66, 88}}
	t3 := S1{"xw", []int8{66, 99}}
	
  // 為什麼這裡會報錯呢,因為我們定義的結構體中的 Score 欄位是 slice, slice 是不支持使用 == 符號的
	// Invalid operation: t1 == t2 (the operator == is not defined on S1)
	//fmt.Println(t1 == t2)
	// Invalid operation: t1 == t2 (the operator == is not defined on S1)
	//fmt.Println(t1 == t3)

	// go 中 slice 使用 reflect.DeepEqual 判斷是否相等
	fmt.Println(reflect.DeepEqual(t1, t2)) // true
	fmt.Println(reflect.DeepEqual(t1, t3)) // false
}

3.3、引用類型

引用類型是指那些底層數據結構的值是引用地址(指針)的類型。它們在記憶體中存儲的是指向實際數據的指針,而不是實際數據本身。切片、映射、通道和函數都是引用類型,因為它們在底層都使用了指針來引用實際的數據。

引用類型的比較實際判斷的是兩個變數是不是指向同一份數據,它不會去比較實際指向的數據。

關於引用類型,有幾個比較特殊的規定:

  • 切片之間不允許比較。切片只能與nil值比較。
  • map之間不允許比較。map只能與nil值比較。
  • 函數之間不允許比較。函數只能與nil值比較。

接下來我們在仔細看看各個類型的具體介紹。

3.3.1、指針
package main

import (
	"fmt"
)

type Student struct {
	Name string
	Age  int
	Sex  bool
}

func main() {

	s1 := &Student{"xiaoming", 18, false}
	s2 := &Student{"xiaoming", 18, false}
	s3 := s1
	fmt.Println(s1 == s2) // false
	fmt.Println(s1 == s3) // true
}

s1 和 s2 雖然數據一樣,但是他們在記憶體中的地址並不相等,所以他們是不相等的,s1 和 s3 指向的是同一份記憶體地址,所以是相等的。

3.3.2、channel 和 函數類型

接下來我們再看看 channel 和 函數類型:

package main

import "fmt"

type Student struct {
	Name string
	Age  int
	Sex  bool
}

func main() {

	ch1 := make(chan bool, 1)
	ch2 := make(chan bool, 1)
	ch3 := ch1
	fmt.Println(ch1 == ch2) // false
	fmt.Println(ch1 == ch3) // true
	fmt.Println("-----")

	a := TestFunc
	b := TestFunc
	c := a

	// invalid operation: a == b (func can only be compared to nil)
	//fmt.Println(a == b)
	// invalid operation: a == c (func can only be compared to nil)
	//fmt.Println(a == c)

	fmt.Println(a)  // 0x10a3400
	fmt.Println(b)  // 0x10a3400
	fmt.Println(c)  // 0x10a3400
}

func TestFunc() {

}

從上面可以看出來,函數類型不支持直接判等操作。原因是:函數類型不支持直接的判等操作是因為函數類型是一種複雜的類型,它包含了函數的簽名和實現代碼等信息。由於函數可以是閉包,可能捕獲了外部變數,因此函數的判等操作會涉及到比較函數的底層實現和捕獲的變數等細節,這會導致判等操作的複雜性和不確定性。

所以從中也可以看出來 Go 中判斷引用類型是否相等,不是簡單的判斷變數所在的記憶體地址是否一致,而是根據相應的類型,有不同的判斷規則,這裡大家需要註意。

3.3.3、slice

再看看切片。因為切片是引用類型,它可以間接的指向自己。例如:

a := []interface{}{ 1, 2.0 }
a[1] = a
fmt.Println(a)

// !!!
// runtime: goroutine stack exceeds 1000000000-byte limit
// fatal error: stack overflow

上面代碼將a賦值給a[1]導致遞歸引用,fmt.Println(a)語句直接爆棧。

  • 切片如果直接比較引用地址,是不合適的。首先,切片與數組是比較相近的類型,比較方式的差異會造成使用者的混淆。另外,長度和容量是切片類型的一部分,不同長度和容量的切片如何比較?
  • 切片如果像數組那樣比較裡面的元素,又會出現上來提到的迴圈引用的問題。雖然可以在語言層面解決這個問題,但是 golang 團隊認為不值得為此耗費精力。

基於上面兩點原因,golang 直接規定切片類型不可比較。使用==比較切片直接編譯報錯。

例如:

var a []int
var b []int

// invalid operation: a == b (slice can only be compared to nil)
fmt.Println(a == b)

如果實在是需要判斷 slice 中元素是否相等,我們一般是自定義一個 判斷函數或者使用reflect.DeepEqual函數。

package main

import (
	"fmt"
	"reflect"
)

func slicesAreEqual(slice1, slice2 []int) bool {
	if len(slice1) != len(slice2) {
		return false
	}

	for i := 0; i < len(slice1); i++ {
		if slice1[i] != slice2[i] {
			return false
		}
	}

	return true
}

func main() {
	slice1 := []int{1, 2, 3}
	slice2 := []int{1, 2, 3}
	slice3 := []int{4, 5, 6}

	fmt.Println(reflect.DeepEqual(slice1, slice2)) // 輸出: false (reflect.DeepEqual 可以進行值相等判斷)
	fmt.Println(slicesAreEqual(slice1, slice2))   // 輸出: true

	fmt.Println(slicesAreEqual(slice1, slice3)) // 輸出: false
}

註意,在上面的示例中,我們自定義了slicesAreEqual函數來判斷兩個切片是否擁有相同的元素。這個示例中我們使用了reflect.DeepEqual來進行值相等的判斷,但是不推薦在切片的值相等判斷中使用reflect.DeepEqual,因為它會將切片的元素逐個進行深度比較,效率較低,尤其在切片較大時。通常最好手動遍歷比較切片的元素。

3.3.4、map

在 Go 中,map 類型不支持直接的判等操作,因為 map 是一個引用類型,並不存儲在變數中的實際數據,而是一個指向底層數據結構的指針。map 是一種哈希表的實現,它包含了鍵值對的集合。

當你將一個 map 賦值給另一個變數時,它們引用同一個底層的 map 數據。因此,兩個 map 可能引用相同的底層數據,但它們仍然是不同的 map 對象。直接比較兩個 map 是否相等,並不能確定它們是否引用同一個底層數據。

如果你需要判斷兩個 map 是否包含相同的鍵值對,你可以通過手動遍歷比較 map 的鍵值對來實現。這涉及到比較每個鍵值對的鍵和值是否相等。

package main

import (
	"fmt"
	"reflect"
)

func mapsAreEqual(map1, map2 map[string]int) bool {
	if len(map1) != len(map2) {
		return false
	}

	for key, value := range map1 {
		if map2Value, ok := map2[key]; !ok || map2Value != value {
			return false
		}
	}

	return true
}

func main() {
	map1 := map[string]int{"a": 1, "b": 2, "c": 3}
	map2 := map[string]int{"a": 1, "b": 2, "c": 3}
	map3 := map[string]int{"a": 1, "b": 2, "c": 4}
  
  // invalid operation: map1 == map2 (map can only be compared to nil)
	//fmt.Println(map1 == map2)

	fmt.Println(reflect.DeepEqual(map1, map2)) // 輸出: true (reflect.DeepEqual 可以進行值相等判斷)
	fmt.Println(mapsAreEqual(map1, map2))     // 輸出: true

	fmt.Println(mapsAreEqual(map1, map3)) // 輸出: false
}

在上面的示例中,我們自定義了mapsAreEqual函數來判斷兩個 map 是否包含相同的鍵值對。請註意,與前面提到的reflect.DeepEqual一樣,我們也不推薦在 map 的值相等判斷中使用reflect.DeepEqual,因為它會將 map 的鍵值對逐個進行深度比較,效率較低,尤其在 map 較大時。通常最好手動遍歷比較 map 的鍵值對。

註意:

由於map的底層原理是使用到了 hash 表,所以所有不可比較的類型都不能作為mapkey。例如:

// invalid map key type []int
m1 := make(map[[]int]int)

type A struct {
    a []int
    b string
}
// invalid map key type A
m2 := make(map[A]int)

由於切片類型不可比較,不能作為mapkey,編譯時m1 := make(map[[]int]int)報錯。 由於結構體A含有切片欄位,不可比較,不能作為mapkey,編譯報錯。

3.4、介面類型

以下內容來自後面的參考鏈接 深入理解Go之== ,十分感謝原博文作者。

介面類型的值可以是任意一個實現了該介面的類型值,所以介面值除了需要記錄具體之外,還需要記錄這個值屬於的類型。也就是說介面值由“類型”和“值”組成,鑒於這兩部分會根據存入值的不同而發生變化,我們稱之為介面的動態類型動態值

介面值的比較涉及這兩部分的比較,只有當動態類型完全相同且動態值相等(動態值使用==比較),兩個介面值才是相等的。

package main

import "fmt"

func main() {
	var a interface{} = 1
	var b interface{} = 2
	var c interface{} = 1
	var d interface{} = 1.0
	fmt.Println(a == b) // false
	fmt.Println(a == c) // true
	fmt.Println(a == d) // false
}

ab動態類型相同(都是int),動態值也相同(都是1,基本類型比較),故兩者相等。 ac動態類型相同,動態值不等(分別為12,基本類型比較),故兩者不等。 ad動態類型不同,aintdfloat64,故兩者不等。

package main

import "fmt"

func main() {
	type A struct {
		a int
		b string
	}

	var aa interface{} = A{a: 1, b: "test"}
	var bb interface{} = A{a: 1, b: "test"}
	var cc interface{} = A{a: 2, b: "test"}

	fmt.Println(aa == bb) // true
	fmt.Println(aa == cc) // false

	var dd interface{} = &A{a: 1, b: "test"}
	var ee interface{} = &A{a: 1, b: "test"}
	fmt.Println(dd == ee) // false
}

aabb動態類型相同(都是A),動態值也相同(結構體A,見上面複合類型的比較規則),故兩者相等。 aacc動態類型相同,動態值不同,故兩者不等。 ddee動態類型相同(都是*A),動態值使用指針(引用)類型的比較,由於不是指向同一個地址,故不等。

註意:

如果介面的動態值不可比較,強行比較會panic!!!

var a interface{} = []int{1, 2, 3, 4}
var b interface{} = []int{1, 2, 3, 4}
// panic: runtime error: comparing uncomparable type []int
fmt.Println(a == b)

ab的動態值是切片類型,而切片類型不可比較,所以a == bpanic

介面值的比較不要求介面類型(註意不是動態類型)完全相同,只要一個介面可以轉化為另一個就可以比較。例如:

package main

import (
	"fmt"
	"io"
	"os"
)

func main() {
	var f *os.File

	var r io.Reader = f
	var rc io.ReadCloser = f
	fmt.Println(r == rc) // true

	var w io.Writer = f
	// invalid operation: r == w (mismatched types io.Reader and io.Writer)
	fmt.Println(r == w)
}

type ReadCloser interface {
	Reader
	Closer
}

r的類型為io.Reader介面,rc的類型為io.ReadCloser介面。查看源碼,io.ReadCloser的定義如下:

io.ReadCloser可轉化為io.Reader,故兩者可比較。

io.Writer不可轉化為io.Reader,編譯報錯。

4、註意事項

不可比較性:

前面說過,golang 中的切片類型、map類型、函數類型(func)是不可比較的。所有含有切片的類型都是不可比較的。例如:

  • 數組元素是切片類型、map類型、函數類型(func)。
  • 結構體有切片類型、map類型、函數類型(func)的欄位。
  • 指針指向的是切片類型、map類型、函數類型(func)。

不可比較性會傳遞,如果一個結構體由於含有切片欄位不可比較,那麼將它作為元素的數組不可比較,將它作為欄位類型的結構體不可比較

package main

import "fmt"

func main() {

	type T struct {
		a map[string]bool
	}
	t1 := T{
		a: map[string]bool{"ni": true},
	}

	t2 := T{
		a: map[string]bool{"ni": true},
	}

	// invalid operation: t1 == t2 (struct containing map[string]bool cannot be compared)
	fmt.Println(t1 == t2)

	type T1 struct {
		a func()
	}

	t3 := T1{
		a: func() {},
	}

	t4 := T1{
		a: func() {},
	}

	// invalid operation: t1 == t2 (struct containing func() cannot be compared)
	fmt.Println(t3 == t4)
}

關於引用類型,有幾個比較特殊的規定:

  • 切片之間不允許比較。切片只能與nil值比較。
  • map之間不允許比較。map只能與nil值比較。
  • 函數之間不允許比較。函數只能與nil值比較。

參考鏈接:

Go語言基礎之介面

4、interface

深入理解Go之==


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

-Advertisement-
Play Games
更多相關文章
  • 8.03周四 一大早電話吵醒,著急給我媽送卡,早上坐車去延安,順便下來玩玩,和以前的的高中同學打了兩個小時的撞球,又吃了自助,晚上還看了電影,在延安我哥哥家快2點才回去。 8.04周五 昨天睡覺的遲,已經中午快12點才起床,起來吃點,就在家裡玩電腦,因為一直在下雨,晚上雨小了,才出去吃了紙包魚,晚上 ...
  • 如何在Go中使用Makefile 1.Makefile是什麼 Makefile是一種構建工具,用於在項目中定義和執行一系列命令。它通常包含了一些規則和目標,用於編譯、測試、運行和清理項目。 2.Makefile可以用於哪些語言的構建過程 Makefile最初是為了 C程式的構建而設計的,但由於其簡潔 ...
  • 目錄 一、面向對象編程快速入門 二、深刻認識面向對象 三、對象在電腦中的執行原理 四、類和對象的一些註意事項 五、其他語法:this 六、其他語法:構造器 七、其他語法:封裝 八、其他語法:實體JavaBean 九、面向對象編程綜合案例 十、補充知識:成員變數、局部變數的區別小結 前言 Stude ...
  • 昨天看到個視頻,彈幕挺有意思的,於是想著用Python給他全部扒下來。 代碼非常簡單,接下來我們看看 具體操作。 需要準備這些 軟體 Python 3.8 Pycharm 模塊使用 import requests 數據請求 import jieba 分詞 import wordcloud 詞雲 im ...
  • ### 歡迎訪問我的GitHub > 這裡分類和彙總了欣宸的全部原創(含配套源碼):[https://github.com/zq2599/blog_demos](https://github.com/zq2599/blog_demos) ### 本篇概覽 - 本篇的知識點是bean的生命周期回調:在 ...
  • 本文我將給大家介紹一個 apk 打包工具 VasDolly 的使用介紹、原理以及如何在服務端接入 VasDolly 進行服務端打渠道包操作。 # 使用介紹 ![](https://files.mdnice.com/user/40549/f5237f40-854b-43dd-9786-d0f7aff0 ...
  • 傳說中,有人因為只是遠遠的看了一眼法外狂徒張三就進去了😂 我現在是獲取他視頻,豈不是直接終生了🤩 網友:趕緊跑路吧 😏 好了話不多說,我們直接開始今天的內容吧! 你需要準備 環境使用 Python 3.8 Pycharm 模塊使用 import requests import csv impo ...
  • ## 6.1、場景模擬 ### 6.1.1、創建UserDao介面及實現類 ![image](https://img2023.cnblogs.com/blog/2052479/202308/2052479-20230805110743575-178752173.png) ``` package or ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...