Go 介面:Go中最強大的魔法,介面應用模式或慣例介紹

来源:https://www.cnblogs.com/taoxiaoxin/archive/2023/11/09/17822823.html
-Advertisement-
Play Games

Go 介面:Go中最強大的魔法,介面應用模式或慣例介紹 目錄Go 介面:Go中最強大的魔法,介面應用模式或慣例介紹一、前置原則二、一切皆組合2.1 一切皆組合2.2 垂直組合2.2.1 第一種:通過嵌入介面構建介面2.2.2 第二種:通過嵌入介面構建結構體類型2.2.3 第三種:通過嵌入結構體類型構 ...


Go 介面:Go中最強大的魔法,介面應用模式或慣例介紹

目錄

一、前置原則

在瞭解介面應用模式之前,我們還先要瞭解一個前置原則,那就是在實際真正需要的時候才對程式進行抽象。再通俗一些來講,就是不要為了抽象而抽象介面本質上是一種抽象,它的功能是解耦,所以這條原則也在告訴我們:不要為了使用介面而使用介面。舉一個簡單的例子,如果我們要給一個計算器添加一個整數加法的功能特性,本來一個函數就可以實現:

func Add(a int64, b int64) int64 {
  return a+b
}

但如果你非要引入一個介面,結果代碼可能就變成了這樣:

type Adder interface {
    Add(int64, int64) int64
}

func Add(adder Adder, a int64, b int64) int64 {
  return adder.Add(a, b)
}

這就會產生一種“過設計”的味道了。

要註意,介面的確可以實現解耦,但它也會引入“抽象”的副作用,或者說介面這種抽象也不是免費的,是有成本的,除了會造成運行效率的下降之外,也會影響代碼的可讀性。不過這裡你就不要拿我之前講解中的實戰例子去對號入座了,那些例子更多是為了讓你學習 Go 語法的便利而構建的。

在多數情況下,在真實的生產項目中,介面都能給應用設計帶來好處。那麼如果要用介面,我們應該怎麼用呢?怎麼藉助介面來改善程式的設計,讓系統實現我們常說的高內聚和低耦合呢?這就要從 Go 語言的“組合”的設計哲學說起。

二、一切皆組合

2.1 一切皆組合

Go 語言之父 Rob Pike 曾說過:如果 C++Java 是關於類型層次結構和類型分類的語言,那麼 Go 則是關於組合的語言。如果把 Go 應用程式比作是一臺機器的話,那麼組合關註的就是如何將散落在各個包中的“零件”關聯並組裝到一起。組合是 Go 語言的重要設計哲學之一,而正交性則為組合哲學的落地提供了更為方便的條件。

正交(Orthogonality)是從幾何學中借用的術語,說的是如果兩條線以直角相交,那麼這兩條線就是正交的,比如我們在代數課程中經常用到的坐標軸就是這樣。用向量術語說,這兩條直線互不依賴,沿著某一條直線移動,你投影到另一條直線上的位置不變。

在電腦技術中,正交性用於表示某種不相依賴性或是解耦性。如果兩個或更多事物中的一個發生變化,不會影響其他事物,那麼這些事物就是正交的。比如,在設計良好的系統中,資料庫代碼與用戶界面是正交的:你可以改動界面,而不影響資料庫;更換資料庫,而不用改動界面。

編程語言的語法元素間和語言特性也存在著正交的情況,並且通過將這些正交的特性組合起來,我們可以實現更為高級的特性。在語言設計層面,Go 語言就為廣大 Gopher 提供了諸多正交的語法元素供後續組合使用,包括:

  • Go 語言無類型體系(Type Hierarchy),沒有父子類的概念,類型定義是正交獨立的;
  • 方法和類型是正交的,每種類型都可以擁有自己的方法集合,方法本質上只是一個將 receiver 參數作為第一個參數的函數而已;
  • 介面與它的實現者之間無“顯式關聯”,也就說介面與 Go 語言其他部分也是正交的。

在這些正交語法元素當中,介面作為 Go 語言提供的具有天然正交性的語法元素,在 Go 程式的靜態結構搭建與耦合設計中扮演著至關重要的角色。 而要想知道介面究竟扮演什麼角色,我們就先要瞭解組合的方式。

構建 Go 應用程式的靜態骨架結構有兩種主要的組合方式,如下圖所示:

WechatIMG277

我們看到,這兩種組合方式分別為垂直組合和水平組合,那這兩種組合的各自含義與應用範圍是什麼呢?下麵我們分別看看這兩種組合。

2.2 垂直組合

垂直組合更多用在將多個類型(如上圖中的 T1I1 等)通過“類型嵌入(Type Embedding)”的方式實現新類型(如 NT1)的定義。

傳統面向對象編程語言(比如:C++)大多是通過繼承的方式建構出自己的類型體系的,但 Go 語言並沒有類型體系的概念。Go 語言通過類型的組合而不是繼承讓單一類型承載更多的功能。由於這種方式與硬體配置升級的垂直擴展很類似,所以這裡我們叫它垂直組合

又因為不是繼承,那麼通過垂直組合定義的新類型與被嵌入的類型之間就沒有所謂“父子關係”的概念了,也沒有向上、向下轉型(Type Casting),被嵌入的類型也不知道將其嵌入的外部類型的存在。調用方法時,方法的匹配取決於方法名字,而不是類型。

這樣的垂直組合更多應用在新類型的定義方面。通過這種垂直組合,我們可以達到方法實現的復用、介面定義重用等目的。

在實現層面,Go 語言通過類型嵌入(Type Embedding)實現垂直組合,組合方式主要有以下幾種。

2.2.1 第一種:通過嵌入介面構建介面

通過在介面定義中嵌入其他介面類型,實現介面行為聚合,組成大介面。這種方式在標準庫中非常常見,也是 Go 介面類型定義的慣例。

比如這個 ReadWriter 介面類型就採用了這種類型嵌入方式:

// $GOROOT/src/io/io.go
type ReadWriter interface {
    Reader
    Writer
}

2.2.2 第二種:通過嵌入介面構建結構體類型

這裡我們直接來看一個通過嵌入介面類型創建新結構體類型的例子:

type MyReader struct {
  io.Reader // underlying reader
  N int64   // max bytes remaining
}

在結構體中嵌入介面,可以用於快速構建滿足某一個介面的結構體類型,來滿足某單元測試的需要,之後我們只需要實現少數需要的介面方法就可以了。尤其是將這樣的結構體類型變數傳遞賦值給大介面的時候,就更能體現嵌入介面類型的優勢了。

2.2.3 第三種:通過嵌入結構體類型構建新結構體類型

在結構體中嵌入介面類型名和在結構體中嵌入其他結構體,都是“委派模式(delegate)”的一種應用。對新結構體類型的方法調用,可能會被“委派”給該結構體內部嵌入的結構體的實例,通過這種方式構建的新結構體類型就“繼承”了被嵌入的結構體的方法的實現。

現在我們可以知道,包括嵌入介面類型在內的各種垂直組合更多用於類型定義層面,本質上它是一種類型組合,也是一種類型之間的耦合方式。

接著,我們來看看水平組合

2.3 水平組合

當我們通過垂直組合將一個個類型建立完畢後,就好比我們已經建立了整個應用程式骨架中的“器官”,那這些器官手、手臂等,那麼這些“器官”之間又是通過關節連接在一起的。

在 Go 應用靜態骨架中,什麼元素經常扮演著“關節”的角色呢?我們先來看個例子,假設現在我們有一個任務,要編寫一個函數,實現將一段數據寫入磁碟的功能。通常我們都可以很容易地寫出下麵的函數:

func Save(f *os.File, data []byte) error

我們看到,這個函數使用一個 *os.File 來表示數據寫入的目的地,這個函數實現後可以工作得很好。但這裡依舊存在一些問題,我們來看一下。

首先,這個函數很難測試。os.File 是一個封裝了磁碟文件描述符(又稱句柄)的結構體,只有通過打開或創建真實磁碟文件才能獲得這個結構體的實例,這就意味著,如果我們要對 Save 這個函數進行單元測試,就必須使用真實的磁碟文件。測試過程中,通過 Save 函數寫入文件後,我們還需要再次操作文件、讀取剛剛寫入的內容來判斷寫入內容是否正確,並且每次測試結束前都要對創建的臨時文件進行清理,避免給後續的測試帶去影響。

其次,Save 函數違背了介面分離原則。根據業界廣泛推崇的 Robert Martin(Bob 大叔)的介面分離原則(ISP 原則,Interface Segregation Principle),也就是客戶端不應該被迫依賴他們不使用的方法,我們會發現 os.File 不僅包含 Save 函數需要的與寫數據相關的 Write 方法,還包含了其他與保存數據到文件操作不相關的方法。比如,你也可以看下 *os.File 包含的這些方法:

func (f *File) Chdir() error
func (f *File) Chmod(mode FileMode) error
func (f *File) Chown(uid, gid int) error
... ...

這種讓 Save 函數被迫依賴它所不使用的方法的設計違反了 ISP 原則。

最後,Save 函數對 os.File 的強依賴讓它失去了擴展性。像 Save 這樣的功能函數,它日後很大可能會增加向網路存儲寫入數據的功能需求。但如果到那時我們再來改變 Save 函數的函數簽名(參數列表 + 返回值)的話,將影響到 Save 函數的所有調用者。

綜合考慮這幾種原因,我們發現 Save 函數所在的“器官”與 os.File 所在的“器官”之間採用了一種硬連接的方式,而以 os.File 這樣的結構體作為“關節”讓它連接的兩個“器官”喪失了相互運動的自由度,讓它與它連接的兩個“器官”構成的聯結體變得“僵直”。

那麼,我們應該如何更換“關節”來改善 Save 的設計呢?我們來試試介面。新版的 Save 函數原型如下:

func Save(w io.Writer, data []byte) error

可以看到,我們用 io.Writer 介面類型替換掉了 *os.File。這樣一來,新版 Save 的設計就符合了介面分離原則,因為 io.Writer 僅包含一個 Write 方法,而且這個方法恰恰是 Save 唯一需要的方法。

另外,這裡我們以 io.Writer 介面類型表示數據寫入的目的地,既可以支持向磁碟寫入,也可以支持向網路存儲寫入,並支持任何實現了 Write 方法的寫入行為,這讓 Save 函數的擴展性得到了質的提升。

還有一點,也是之前我們一直強調的,介面本質是契約,具有天然的降低耦合的作用。基於這點,我們對 Save 函數的測試也將變得十分容易,比如下麵示例代碼:

func TestSave(t *testing.T) {
    b := make([]byte, 0, 128)
    buf := bytes.NewBuffer(b)
    data := []byte("hello, golang")
    err := Save(buf, data)
    if err != nil {
        t.Errorf("want nil, actual %s", err.Error())
    }

    saved := buf.Bytes()
    if !reflect.DeepEqual(saved, data) {
        t.Errorf("want %s, actual %s", string(data), string(saved))
    }
}

在這段代碼中,我們通過 bytes.NewBuffer 創建了一個 *bytes.Buffer 類型變數 buf,由於 bytes.Buffer 實現了 Write 方法,進而實現了 io.Writer 介面,我們可以合法地將變數 buf 傳遞給 Save 函數。之後我們可以從 buf 中取出 Save 函數寫入的數據內容與預期的數據做比對,就可以達到對 Save 函數進行單元測試的目的了。在整個測試過程中,我們不需要創建任何磁碟文件或建立任何網路連接。

看到這裡,你應該感受到了,用介面作為“關節(連接點)”的好處很多!像上面圖中展示的那樣,介面可以將各個類型水平組合(連接)在一起。通過介面的編織,整個應用程式不再是一個個孤立的“器官”,而是一幅完整的、有靈活性和擴展性的靜態骨架結構。

現在,我們已經確定了介面承擔了應用骨架的“關節”角色,接下來我們來看看介面是如何演好這一角色的。

三、介面應用的幾種模式

前面已經說了,以介面為“關節”的水平組合方式,可以將各個垂直組合出的類型“耦合”在一起,從而編織出程式靜態骨架。而通過介面進行水平組合的基本模式就是:使用接受介面類型參數的函數或方法。在這個基本模式基礎上,還有其他幾種“衍生品”。我們先從基本模式說起,再往外延伸。

3.1 基本模式

接受介面類型參數的函數或方法是水平組合的基本語法,形式是這樣的:

func YourFuncName(param YourInterfaceType)

我們套用骨架關節的概念,用這幅圖來表示上面基本模式語法的運用方法:

WechatIMG279

我們看到,函數 / 方法參數中的介面類型作為“關節(連接點)”,支持將位於多個包中的多個類型與 YourFuncName 函數連接到一起,共同實現某一新特性。

同時,介面類型和它的實現者之間隱式的關係卻在不經意間滿足了:依賴抽象(DIP)、里氏替換原則(LSP)、介面隔離(ISP)等代碼設計原則,這在其他語言中是需要很“刻意”地設計謀劃的,但對 Go 介面來看,這一切卻是自然而然的。

這一水平組合的基本模式在 Go 標準庫、Go 社區第三方包中有著廣泛應用,其他幾種模式也是從這個模式衍生的。下麵我們看一下其他的各個衍生模式。

3.2 創建模式

Go 社區流傳一個經驗法則:“接受介面,返回結構體(Accept interfaces, return structs)”,這其實就是一種把介面作為“關節”的應用模式。我這裡把它叫做創建模式,是因為這個經驗法則多用於創建某一結構體類型的實例。

下麵是 Go 標準庫中,運用創建模式創建結構體實例的代碼摘錄:

// $GOROOT/src/sync/cond.go
type Cond struct {
    ... ...
    L Locker
}

func NewCond(l Locker) *Cond {
    return &Cond{L: l}
}

// $GOROOT/src/log/log.go
type Logger struct {
    mu     sync.Mutex 
    prefix string     
    flag   int        
    out    io.Writer  
    buf    []byte    
}

func New(out io.Writer, prefix string, flag int) *Logger {
    return &Logger{out: out, prefix: prefix, flag: flag}
}

// $GOROOT/src/log/log.go
type Writer struct {
    err error
    buf []byte
    n   int
    wr  io.Writer
}

func NewWriterSize(w io.Writer, size int) *Writer {
    // Is it already a Writer?
    b, ok := w.(*Writer)
    if ok && len(b.buf) >= size {
        return b
    }
    if size <= 0 {
        size = defaultBufSize
    }
    return &Writer{
        buf: make([]byte, size),
        wr:  w,
    }
}

我們看到,創建模式在 synclogbufio 包中都有應用。以上面 log 包的 New 函數為例,這個函數用於實例化一個 log.Logger 實例,它接受一個 io.Writer 介面類型的參數,返回 *log.Logger。從 New 的實現上來看,傳入的 out 參數被作為初值賦值給了 log.Logger 結構體欄位 out

創建模式通過介面,在 NewXXX 函數所在包與介面的實現者所在包之間建立了一個連接。大多數包含介面類型欄位的結構體的實例化,都可以使用創建模式實現。這個模式比較容易理解,我們就不再深入了。

3.3 包裝器模式

在基本模式的基礎上,當返回值的類型與參數類型相同時,我們能得到下麵形式的函數原型:

func YourWrapperFunc(param YourInterfaceType) YourInterfaceType

通過這個函數,我們可以實現對輸入參數的類型的包裝,並在不改變被包裝類型(輸入參數類型)的定義的情況下,返回具備新功能特性的、實現相同介面類型的新類型。這種介面應用模式我們叫它包裝器模式,也叫裝飾器模式。包裝器多用於對輸入數據的過濾、變換等操作。

下麵就是 Go 標準庫中一個典型的包裝器模式的應用:

// $GOROOT/src/io/io.go
func LimitReader(r Reader, n int64) Reader { return &LimitedReader{r, n} }

type LimitedReader struct {
    R Reader // underlying reader
    N int64  // max bytes remaining
}

func (l *LimitedReader) Read(p []byte) (n int, err error) {
    // ... ...
}

通過上面的代碼,我們可以看到,通過 LimitReader 函數的包裝後,我們得到了一個具有新功能特性的 io.Reader 介面的實現類型,也就是 LimitedReader。這個新類型在 Reader 的語義基礎上實現了對讀取位元組個數的限制。

接下來我們再具體看 LimitReader 的一個使用示例:

func main() {
    r := strings.NewReader("hello, gopher!\n")
    lr := io.LimitReader(r, 4)
    if _, err := io.Copy(os.Stdout, lr); err != nil {
        log.Fatal(err)
    }
}

運行這個示例,我們得到了這個結果:

hell

我們看到,當採用經過 LimitReader 包裝後返回的 io.Reader 去讀取內容時,讀到的是經過 LimitedReader 約束後的內容,也就是只讀到了原字元串前面的 4 個位元組:“hell”。

由於包裝器模式下的包裝函數(如上面的 LimitReader)的返回值類型與參數類型相同,因此我們可以將多個接受同一介面類型參數的包裝函數組合成一條鏈來調用,形式是這樣的:

YourWrapperFunc1(YourWrapperFunc2(YourWrapperFunc3(...)))

我們在上面示例的基礎上自定義一個包裝函數:CapReader,通過這個函數的包裝,我們能得到一個可以將輸入的數據轉換為大寫的 Reader 介面實現:

func CapReader(r io.Reader) io.Reader {
    return &capitalizedReader{r: r}
}

type capitalizedReader struct {
    r io.Reader
}

func (r *capitalizedReader) Read(p []byte) (int, error) {
    n, err := r.r.Read(p)
    if err != nil {
        return 0, err
    }

    q := bytes.ToUpper(p)
    for i, v := range q {
        p[i] = v
    }
    return n, err
}

func main() {
    r := strings.NewReader("hello, gopher!\n")
    r1 := CapReader(io.LimitReader(r, 4))
    if _, err := io.Copy(os.Stdout, r1); err != nil {
        log.Fatal(err)
    }
}

這裡,我們將 CapReaderio.LimitReader 串在了一起形成一條調用鏈,這條調用鏈的功能變為:截取輸入數據的前四個位元組並將其轉換為大寫字母。這個示例的運行結果與我們預期功能也是一致的:

HELL

3.4 適配器模式

適配器模式不是基本模式的直接衍生模式,但這種模式是後面中間件模式的前提,所以我們需要簡單介紹下這個模式。

適配器模式的核心是適配器函數類型(Adapter Function Type)。適配器函數類型是一個輔助水平組合實現的“工具”類型。這裡我要再強調一下,它是一個類型。它可以將一個滿足特定函數簽名的普通函數,顯式轉換成自身類型的實例,轉換後的實例同時也是某個介面類型的實現者。

這裡,我們來看一個應用 http.HandlerFunc 的例子:

func greetings(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Welcome!")
}

func main() {
    http.ListenAndServe(":8080", http.HandlerFunc(greetings))
}

我們可以看到,這個例子通過 http.HandlerFunc 這個適配器函數類型,將普通函數 greetings 快速轉化為滿足 http.Handler 介面的類型。而 http.HandleFunc 這個適配器函數類型的定義是這樣的:

// $GOROOT/src/net/http/server.go
type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

type HandlerFunc func(ResponseWriter, *Request)

func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
    f(w, r)
}

經過 HandlerFunc 的適配轉化後,我們就可以將它的實例用作實參,傳遞給接收 http.Handler 介面的 http.ListenAndServe 函數,從而實現基於介面的組合。

3.5 中間件(Middleware)

最後,我們看下中間件這個應用模式。中間件(Middleware)這個詞的含義可大可小。在 Go Web 編程中,“中間件”常常指的是一個實現了 http.Handler 介面的 http.HandlerFunc 類型實例。實質上,這裡的中間件就是包裝模式和適配器模式結合的產物。

我們來看一個例子:

func validateAuth(s string) error {
    if s != "123456" {
        return fmt.Errorf("%s", "bad auth token")
    }
    return nil
}

func greetings(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Welcome!")
}

func logHandler(h http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        t := time.Now()
        log.Printf("[%s] %q %v\n", r.Method, r.URL.String(), t)
        h.ServeHTTP(w, r)
    })
}

func authHandler(h http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        err := validateAuth(r.Header.Get("auth"))
        if err != nil {
            http.Error(w, "bad auth param", http.StatusUnauthorized)
            return
        }
        h.ServeHTTP(w, r)
    })

}

func main() {
    http.ListenAndServe(":8080", logHandler(authHandler(http.HandlerFunc(greetings))))
}

我們看到,所謂中間件(如:logHandlerauthHandler)本質就是一個包裝函數(支持鏈式調用),但它的內部利用了適配器函數類型(http.HandlerFunc),將一個普通函數(比如例子中的幾個匿名函數)轉型為實現了 http.Handler 的類型的實例。

運行這個示例,並用 curl 工具命令對其進行測試,我們可以得到下麵結果:

$curl http://localhost:8080
bad auth param

$curl -H "auth:123456" localhost:8080/ 
Welcome!

從測試結果上看,中間件 authHandler 起到了對 HTTP 請求進行鑒權的作用。

四、介面使用的註意事項

儘量避免使用空介面作為函數參數類型

Go 語言之父 Rob Pike 曾說過:空介面不提供任何信息(The empty interface says nothing)。我們應該怎麼理解這句話的深層含義呢?

在 Go 語言中,一方面你不用像 Java 那樣顯式聲明某個類型實現了某個介面,但另一方面,你又必須聲明這個介面,這又與介面在 Java 等靜態類型語言中的工作方式更加一致。

這種不需要類型顯式聲明實現了某個介面的方式,可以讓種類繁多的類型與介面匹配,包括那些存量的、並非由你編寫的代碼以及你無法編輯的代碼(比如:標準庫)。Go 的這種處理方式兼顧安全性和靈活性,其中,這個安全性就是由 Go 編譯器來保證的,而為編譯器提供輸入信息的恰恰是介面類型的定義。

比如我們看下麵的介面:

// $GOROOT/src/io/io.go
type Reader interface {
  Read(p []byte) (n int, err error)
}

Go 編譯器通過解析這個介面定義,得到介面的名字信息以及它的方法信息,在為這個介面類型參數賦值時,編譯器就會根據這些信息對實參進行檢查。這時你可以想一下,如果函數或方法的參數類型為空介面 interface{},會發生什麼呢?

這恰好就應了 Rob Pike 的那句話:“空介面不提供任何信息”。這裡“提供”一詞的對象不是開發者,而是編譯器。在函數或方法參數中使用空介面類型,就意味著你沒有為編譯器提供關於傳入實參數據的任何信息,所以,你就會失去靜態類型語言類型安全檢查的“保護屏障”,你需要自己檢查類似的錯誤,並且直到運行時才能發現此類錯誤。

所以,建議 Gopher 儘可能地抽象出帶有一定行為契約的介面,並將它作為函數參數類型,儘量不要使用可以“逃過”編譯器類型安全檢查的空介面類型(interface{})。

在這方面,Go 標準庫已經為我們作出了“表率”。全面搜索標準庫後,你可以發現以 interface{} 為參數類型的方法和函數少之甚少。不過,也還有,使用 interface{} 作為參數類型的函數或方法主要有兩類:

  • 容器演算法類,比如:container 下的 heaplistring 包、sort 包、sync.Map 等;
  • 格式化 / 日誌類,比如:fmt 包、log 包等。

這些使用 interface{} 作為參數類型的函數或方法都有一個共同特點,就是它們面對的都是未知類型的數據,所以在這裡使用具有“泛型”能力的 interface{} 類型。

五、小結

在使用介面前一定要搞清楚自己使用介面的原因,千萬不能為了使用介面而使用介面。

介面與 Go 的“組合”的設計哲學息息相關。在 Go 語言中,組合是 Go 程式間各個部分的主要耦合方式。垂直組合可實現方法實現和介面定義的重用,更多用於在新類型的定義方面。而水平組合更多將介面作為“關節”,將各個垂直組合出的類型“耦合”在一起,從而編製出程式的靜態骨架。

通過介面進行水平組合的基本模式,是“使用接受介面類型參數的函數或方法”,在這一基本模式的基礎上,我們還瞭解了幾個衍生模式:創建模式、包裝器模式與中間件模式。此外,我們還學習了一個輔助水平組合實現的“工具”類型:適配器函數類型,它也是實現中間件模式的前提。

最後需要我們牢記的是:我們要儘量避免使用空介面作為函數參數類型。一旦使用空介面作為函數參數類型,你將失去編譯器為你提供的類型安全保護屏障。

分享是一種快樂,開心是一種態度!
您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 在寫頁面的時候,發現表單裡面有一個省市區的 options 組件要寫,因為表單很多地方都會用到這個地址選擇,我便以為很簡單嘛。 雖然很簡單的一個功能,但是網路上能搜索到的教程大多都是需要配合 elementUI 等各種 UI 庫的,但是我 ...
  • 我們有沒有想過,是否有一種技術,伺服器可以主動將數據推送給客戶端進行渲染,而不再是客戶端向伺服器發出請求等待返回結果呢?接下來,讓我們一起瞭解weboskcet ...
  • 前言 最近新開了個項目,以前老項目都是vue2+vuex開發的,都說用vue3+pinia爽得多,那新項目就vue3+pinia吧。這裡記錄一下pinia的使用。 使用方法 安裝pinia: npm i pinia main.js中引入pinia: //main.js import { create ...
  • 作者:WangMin 格言:努力做好自己喜歡的每一件事 jQuery.js 是什麼? jQuery是一個快速簡潔、免費開源易用的JavaScript框架,倡導寫更少的代碼,做更多的事情 。它封裝JavaScript常用的功能代碼,提供了一種簡便的JavaScript設計模式,以及我們開發中常用到的操 ...
  • 在日常一些需求中,總會遇到一些需要前端進行手動計算的場景,那麼這裡需要優先考慮的則是數字精度問題!具體請看下麵截圖 如圖所示,在JavaScript進行浮點型數據計算當中,會出現計算結果“不正確”的現象。 我們知道浮點型數據類型主要有:單精度float、雙精度double。 浮點型簡單來說就是表示帶 ...
  • 從接觸領域驅動設計的初學階段,到實現一個舊系統改造到DDD模型,再到按DDD規範落地的3個的項目。對於領域驅動模型設計研發,從開始的各種疑惑到吸收各種先進的理念,目前在技術實施這一塊已經基本比較成熟。在既往經驗中總結了一些在開發中遇到的技術問題和解決方案進行分享。 ...
  • 三、基本數據類型和計算(一) 1、常量和變數 1)常量和變數定義 常量 值不會改變 變數 值可能改變 2)常量及變數名起名必須遵守的規則 1 不能重名 2 不能和C/C++語言里的關鍵字重名 3 必須是字母或者字母和數字的組合,符號僅_可以使用 4 名字不能用數字開頭 3)起名字建議遵守的規則 1 ...
  • 創建表格 要在MySQL中創建表格,請使用"CREATE TABLE"語句。 確保在創建連接時定義了資料庫的名稱。 示例創建一個名為 "customers" 的表格: import mysql.connector mydb = mysql.connector.connect( host="local ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...