當你需要學習新事物時你的方法是什麼?我有一個非常具體的,我在學習 Golang 時再次測試了它!

有太多的內容要講,但我在這裡的目的是列出我發現有用並且我專門花時間正確學習的內容。

目錄

  1. 序言

在過去的兩周里,我一直在使用 Golang 學習和建立小型應用程式。目前,透過許多直播已經完成了近 50 小時的程式碼,能夠學到一些我之前在語言方面遇到的一些小問題的東西真是太棒了。

在這兩週的旅程中,我精心設計了:

  • 一個小而非常簡單的外殼

  • Redis 基本實現

  • HTTP 1.1 協定實現

  • DNS伺服器的實現

  • 以及針對一家非常酷的公司的工作測試(將在本文末尾提供)。

而這一切都是因為我的老闆再次要求我學習一項新技術來處理一些ScyllaDB PoC 和演示......我對這個決定不太滿意,但好吧,這是我的工作。

去年,我一直在學習 Rust,它對我來說可能仍然太複雜,但我學到了一些非常酷的概念,這些概念讓我轉向 Go 變得非常有魅力!

在這篇文章中,我將為您提供一些技巧和建議,以加快您的學習流程。

  1. 認識 CLI

我是一名PHP 開發人員,我習慣了有史以來最好的CLI(是的,它是Artisan),但是在我作為開發人員的旅程中,我經歷過一些很棒的專案,其中許多是… :

  • 貨物(生鏽)

  • 國家公共管理 (JS)

  • 作曲家 (PHP)

  • 等等...

當我進入 Go 環境時,它開始成為一個真正的問題。至少對我來說,Golang 在工具和文件方面的開發者體驗還可以好很多。考慮到這一點,我決定從一開始就學習 3 個你必須學習的命令。

請記住:這只是一個帶有我自己解釋的演練。如果您需要詳細訊息,請打開文件:)

另外:go docs 很糟糕,請有人在那裡放一支語法螢光筆

2.1 CLI:go mod

根據您是否想要模組化應用程式或擁有一個有組織的環境,這首先將是最有用的命令。

go mod指令管理專案中的所有依賴項,並負責autoremoving不再使用的任何內容。

首先,在新的空資料夾中,讓我們使用go mod init在專案中初始化一個新模組:

mkdir go-fodase
cd go-fodase

# go mod init <module-name>
go mod init github.com/danielhe4rt/go-fodase

這將在專案根目錄中建立一個名為go.mod的新文件,它基本上是目前的內容:

  • 模組名稱

  • 你的 Go 版本

這是該文件,如果您想自己檢查一下:

# folder: ~/go-fodase
cat go.mod

# module github.com/danielhe4rt/gofodase
# 
# go 1.23.2

之後,我真正喜歡的下一個東西是go mod tidy ,它基本上加入了所有缺少的依賴項並刪除了未使用的依賴項。

go mod tidy

第二個只是讓您記住它的存在並且非常有用!也許你的環境每次都會執行它,你會習慣看到導入消失:p

2.2 CLI:執行

這可能是您將使用的最常用命令,因為您必須執行專案,但它的工作原理如下:

  • 您應該指向包含main()函數的檔案。

  • 該檔案不必位於資料夾的根目錄中。

關於此命令要記住的最重要的一點是,當您執行go run命令時,它將在當前目錄中查找go.mod文件,並將其用作映射整個專案(導入、包等)的基礎。以下是一些範例:

# go run <your-main-file>

# Environment 1
# .
# ├── go.mod
# └── main.go
go run app.go

# Environment 2
# .
# ├── go.mod
# └── src
#     └── main.go
go run src/app.go

這是我們的app.go內容:

package main

import(
    "fmt"
)

func main () {
    fmt.Println("whats up! don't forget to like this article <3")
}

現在您已經了解了執行專案的基礎知識!從字面上看,你好世界!

  1. 比較不同的語法:

我對 Go 的問題一直是它的編寫方式,但經過幾個小時的編碼後,我意識到它比我想像的要簡單。正如您可能已經猜到的,我擁有深厚的 PHP 背景和一些 Rust 經驗。

當我在 2023 年開始學習 Rust 時,幸運的是,我非常喜歡的一個人Nuno Maduro (Laravel)做了一個名為“PHP for Rust Developers”的演講,它給了我一些關於語法的基本介紹給了我一些知識。

反正當時對我有用,為什麼不做一些比較呢?

3.1 語法:類別/結構體和 API 封裝

在 OOP 中,我們有類,這是將程式碼抽象化成小塊的一種非常酷的方式,並且您有「類似的」東西。 Golang 可以被視為一場奧德賽,因為將環境變成你想要的任何東西都可以是史詩般的開發。

請記住, Golang是一種“高級語言”,它提供“系統級”語法,使您可以輕鬆地使用“低級”實現。

在Go語法下,你可以

  • [結構] 透過在結構體前面加上type前綴,加上「類別/結構體」名稱,然後加上struct後綴來定義結構體。

  • [封裝] 透過以大寫小寫名稱開頭來定義與類別/結構相關的元素的公開。

* [Visibility: "public"]: Set the item name to uppercase.
* [Visibility: "private/protected"]: Set the item name in lower case.

您可以將其用於: StructsStruct FieldsStruct Methods 。仔細看看:

// -----------------------
// file: src/users/user.go
package users

type User struct {  
    name string  // starts with uppercase: public
    Age  uint8  // starts with uppercase: public
}  

// Starts with lowercase: private
func (u *User) getName() string {  
    return u.name  
}  

// Starts with uppercase: public
func (u *User) Greetings() string {  
    cheering := "Don't forget to follow me back!"

    // Can consume same struct functions.
    return fmt.Sprintf("Hey, %v (%v)! %v !", u.getName(), u.Age, cheering)
} 

// -----------------
// file: src/main.go
package main  

import "github.com/danielhe4rt/go-fodase/src/users"  

func main() {  
    // ❌ You CAN'T start 'name'. Use a Setter Function for it.    
    // user := users.User{    
    //     name: "danielhe4rt", // ❌
    //     Age:  25,            // ✅
    // }

    // ✅ Now you're only initializing what you need.    
    user := users.User{Age: 25}  

    // Methods Calls  
    user.SetName("danielhe4rt") // ✅
    user.getName()              // ❌
    user.Greetings()            // ✅

    currentName := user.name() // ❌ 
    currentAge := user.Age     // ✅ 
}

在 Rust 中,您有一種更明確的方法(更像 oop 的語言),其中:

  • [Struct] 使用前綴struct定義一個結構,加入您的「類別/結構」名稱即可。

  • [封裝] 如果你想讓某些東西對其他「板條箱」公開,你應該在你想要公開的程式碼部分加上pub前綴。

// file: src/users/mod.rs

// Make the struct `User` public
pub struct User {
    // Private field: accessible only within the `users` module
    name: String,
    // Public field: accessible from other modules
    pub age: u8,
}

impl User {
    // Public method to create a new User with a name and age
    pub fn new(age: u8) -> Self {
        User {
            name: String::new(), // Initialize with an empty name
            age,
        }
    }

    // Public setter method to set the private `name` field
    pub fn set_name(&mut self, name: String) {
        self.name = name;
    }

    // Private method to get the name
    fn get_name(&self) -> &str {
        &self.name
    }

    // Public method that uses both public and private fields/methods
    pub fn greetings(&self) -> String {
        let cheering = "Don't forget to follow me back!";
        format!(
            "Hey, {} ({} years old)! {}",
            self.get_name(),
            self.age,
            cheering
        )
    }
}

// file: src/main.rs
mod users;  

use crate::users::User;  

fn main() {
    // ❌ You CAN'T directly set the private `name` field
    // let user = User {
    //     name: String::from("danielhe4rt"), // ❌
    //     age: 25,                           // ✅
    // };

    // ✅ Initialize the User using the constructor
    let mut user = User::new(25);

    // ✅ Use the setter method to set the private `name`
    user.set_name(String::from("danielhe4rt"));

    let greeting = user.greetings();    // ✅ Call the public `greetings` method
    let current_age = user.age;         // ✅ Access the public `age` field directly
    let current_name = user.name;       // ❌ You CAN'T access the private `name` field directly
    let current_name = user.get_name(); // ❌ You CAN'T call the private `get_name` method directly
}

我想讓 PHP、Java 等內容變得明確,但如果你停下來想想,寫的程式碼更少,但它也會影響可讀性。

3.2 語言:介面實作很WEIRD AS FUC*

說實話,我是那種會嘗試將 LARAVEL 放入 Go 環境中的人,我不知道,但這已經在<a href="">Goravel</a>中完成了。不管怎樣,我真的很喜歡使用「介面/契約驅動開發」的想法,這是我第一次在一種語言中陷入困境。

在 Go 中,介面並不是在結構/類別中「實現」的,對於像我這樣的 OOP 人員來說,讓這樣的設計決策融入我的頭腦真是太瘋狂了。看看預期會發生什麼:

interface OAuthContract {
    public function redirect(string $code); 
    public function authenticate(string $token);
}

class GithubOAuthProvider implements OAuthContract{
    public function redirect(string $code) {}
    public function authenticate(string $token) {}
}

class SpotifyAuthProvider implements OAuthContract{
    public function redirect(string $code) {}
    public function authenticate(string $token) {}
}

function authenticate(string $routeProvider): OAuthContract {
    return match($provider) {
        'github' => new GithubOAuthProvider(),
        'spotify' => new SpotifyAuthProvider(),
        default => throw OAuthProviderException::notFound(),
    };
}

authenticate('github');

現在,說到去:你在結構內部沒有「介面」的明確實現,那就是,嗯......奇怪嗎?相反,您只需實作介面所需的方法,這些方法將在編譯時為您進行檢查。公平地說,這是一種編譯語言,它永遠不會成為問題,但我正在談論我對開發人員體驗的看法!

import (
    "errors"
    "fmt"
)

type OAuthContract interface {
    Redirect(code string) error
    Authenticate(token string) error
}

// GithubOAuthProvider implements the OAuthContract interface for GitHub.
type GithubOAuthProvider struct{}

// Redirect handles the redirection logic for GitHub OAuth.
func (g *GithubOAuthProvider) Redirect(code string) error {
    // Implement GitHub-specific redirection logic here.
    return nil
}

// Authenticate handles the authentication logic for GitHub OAuth.
func (g *GithubOAuthProvider) Authenticate(token string) error {
    // Implement GitHub-specific authentication logic here.
    return nil
}

// SpotifyOAuthProvider implements the OAuthContract interface for Spotify.
type SpotifyOAuthProvider struct{}

// Redirect handles the redirection logic for Spotify OAuth.
func (s *SpotifyOAuthProvider) Redirect(code string) error {
    // Implement Spotify-specific redirection logic here.
    fmt.Printf("Redirecting to Spotify with code: %s\n", code)
    return nil
}

// Authenticate handles the authentication logic for Spotify OAuth.
func (s *SpotifyOAuthProvider) Authenticate(token string) error {
    // Implement Spotify-specific authentication logic here.
    fmt.Printf("Authenticating Spotify token: %s\n", token)
    return nil
}

// AuthenticateFactory is a factory function that returns an instance of OAuthContract
func AuthenticateFactory(provider string) (OAuthContract, error) {
    switch provider {
    case "github":
        return &GithubOAuthProvider{}, nil
    case "spotify":
        return &SpotifyOAuthProvider{}, nil
    default:
        return nil, &OAuthProviderError{Provider: provider}
    }
}

無論如何,在用該語言編碼一段時間後,您就會習慣它。現在,我們來談談基礎環境為您提供的無需下載任何內容的功能!

  1. Stdlib:絕對是個很棒的工具包

現在我談論的是 Go 透過標準庫為您提供的一切,無需下載第三方套件。以下是一些按時間順序排列的時間表:

  • 第一天:什麼?為什麼不像 JS/Java 那樣型別包含所有方法? (而且我討厭他們兩個)

  • 第一周:等等,也許這很好(在了解了原始類型的包之後)

  • 第二週:什麼?為什麼其他語言預設沒有這麼好的函式庫?

我不是開玩笑,每天我探索 go 時,我都會在標準庫下發現一些很酷的庫。那麼,讓我們開始討論原始類型。

4.1 包:原始型

與 PHP 一樣,與許多其他語言(Rust、Java、JS 等)不同,Golang 需要「輔助」函數來執行大多數相關的類型操作。我們可以將它們視為「貧血」類型,因為它們沒有附加的「實用性」。

// PHP Example (with a REALLY UGLY API)
// String manipulation using built-in PHP functions
$str = "Hello, World!";

// Check if the string contains a substring
$contains = strpos($str, "World") !== false;
echo "Contains 'World': " . ($contains ? "true" : "false") . "\n"; // Output: true

// Convert the string to uppercase
$upper = strtoupper($str);
echo "Uppercase: " . $upper . "\n"; // Output: HELLO, WORLD!

// Split the string by comma
$split = explode(",", $str);
echo "Split by comma: ";
print_r($split); // Output: Array ( [0] => Hello  [1] =>  World! )

因此,如果您正在使用“String”類型,則可以使用其他套件(例如strconvstrings來操作它!但這裡有一條黃金法則,永遠不要忘記要查看哪個套件:如果您的類型是string ,請尋找具有複數名稱的相同套件!

簡而言之,這將為您提供與[]TypeType相關的函數:

  • 字串類型-> import ("strings")用於以下操作: Contains()Upper()Split() ...

  • 位元組類型-> import ("bytes")用於Include()Compare()Split() ... 等操作

  • 等等!

看一下程式碼,大家可以自己驗證一下:

package main

import (
    "bytes"
    "fmt"
    "strings"
)

func main() {
    // String manipulation using the "strings" package
    str := "Hello, World!"

    // Check if the string contains a substring
    contains := strings.Contains(str, "World")
    fmt.Printf("Contains 'World': %v\n", contains) // Output: true

    // Convert the string to uppercase
    upper := strings.ToUpper(str)
    fmt.Printf("Uppercase: %s\n", upper) // Output: HWith this intELLO, WORLD!

    // Split the string by comma
    split := strings.Split(str, ",")
    fmt.Printf("Split by comma: %v\n", split) // Output: [Hello  World!]
}

這應該很簡單,但我為此掙扎了一段時間,直到我進入我的腦海。也許使用 Laravel 及其輔助函數太多年讓我忘記了在沒有框架的情況下編碼是多麼困難:D

4.2 包:有用的東西

當我探索工具和專案時,我對許多專案都有了很好的介紹,我想列出每個專案以及我使用過的程式庫:

  • 建立您自己的 Shell 挑戰:
* **packages:** 
    * **fmt:** I/O library (Scan/Read and Write stuff on your screen)
    * **os:** functions and helpers that talks directly with your Operational System.
    * **strconv**: cast specific data-types to string or cast string to any defined type.
  • 建立您自己的 (HTTP|DNS) 伺服器挑戰:
* **packages:**
    * **net:** integration layer with network I/O protocols such as HTTP, TCP, UDP and Unix Domain Sockets
    * [previous packages...]
  • 中級家庭作業任務分配?
* **packages:**
    * **flag:** Captures your CLI arguments into variables 
    * **log:** Adds Log's channels to your application
    * **crypto/rand:** Secure Cryptographic Random Generator
    * **math/rand:** Math Numbers Random Generator
    * **time:** Duration/Time Lib

這是所有套件實現的可捲動視圖,以便您可以查看它們。這裡有很多很酷的標準包可以引用。

注意:這是很多程式碼! :p

不要忘記評論您最喜歡的功能(除了 goroutine 和通道):p

package main

import (
    crypto "crypto/rand"
    "encoding/hex"
    "flag"
    "fmt"
    "log"
    "math/rand"
    "net/http"
    "os"
    "strconv"
    "time"
)

// =========================
// Example 1: fmt Package
// =========================

// fmtExample demonstrates basic input and output operations using the fmt package.
func fmtExample() {
    fmt.Println("=== fmt Package Example ===")
    var name string
    var age int

    // Prompt the user for their name
    fmt.Print("Enter your name: ")
    _, err := fmt.Scanln(&name)
    if err != nil {
        fmt.Println("Error reading name:", err)
        return
    }

    // Prompt the user for their age
    fmt.Print("Enter your age: ")
    _, err = fmt.Scanln(&age)
    if err != nil {
        fmt.Println("Error reading age:", err)
        return
    }

    // Display the collected information
    fmt.Printf("Hello, %s! You are %d years old.\n\n", name, age)besides
}

// =========================
// Example 2: os Package
// =========================

// osExample showcases how to interact with the operating system, such as reading environment variables and exiting the program.
func osExample() {
    fmt.Println("=== os Package Example ===")

    // Retrieve the HOME environment variable
    home := os.Getenv("HOME")
    if home == "" {
        fmt.Println("HOME environment variable not set.")
        // Exit the program with a non-zero status code to indicate an error
        // Uncomment the next line to enable exiting
        // os.Exit(1)
    } else {
        fmt.Printf("Your home directory is: %s\n", home)
    }

    // Demonstrate exiting the program successfully
    // Uncomment the next lines to enable exiting
    /*
        fmt.Println("Exiting program with status code 0.")
        os.Exit(0)
    */
    fmt.Println()
}

// =========================
// Example 3: strconv Package
// =========================

// strconvExample demonstrates converting between strings and other data types.
func strconvExample() {
    fmt.Println("=== strconv Package Example ===")

    // Convert string to integer
    numStr := "456"
    num, err := strconv.Atoi(numStr)
    if err != nil {
        fmt.Println("Error converting string to int:", err)
        return
    }
    fmt.Printf("Converted string '%s' to integer: %d\n", numStr, num)

    // Convert integer back to string
    newNumStr := strconv.Itoa(num)
    fmt.Printf("Converted integer %d back to string: '%s'\n\n", num, newNumStr)
}

// =========================
// Example 4: net Package
// =========================

// netExample demonstrates making a simple HTTP GET request using the net package.
func netExample() {
    fmt.Println("=== net Package Example ===")

    // Make an HTTP GET request to a public API
    resp, err := http.Get("https://api.github.com")
    if err != nil {
    let greeting = user.greetings();
        fmt.Println("Error making HTTP GET request:", err)
        return
    }
    defer resp.Body.Close()

    // Display the HTTP status code
    fmt.Printf("Response Status: %s\n\n", resp.Status)
}

// =========================
// Example 5: flag Package
// =========================

// flagExample demonstrates how to capture command-line arguments using the flag package.
func flagExample() {
    fmt.Println("=== flag Package Example ===")

    // Define command-line flags
    name := flag.String("name", "World", "a name to say hello to")
    age := flag.Int("age", 0, "your age")

    // Parse the flags
    flag.Parse()

    // Use the flag values
    fmt.Printf("Hello, %s!\n", *name)
    if *age > 0 {
        fmt.Printf("You are %d years old.\n\n", *age)
    } else {
        fmt.Println()
    }
}

// =========================
// Example 6: log Package
// =========================

// logExample demonstrates logging messages to a file using the log package.
func logExample() {
    fmt.Println("=== log Package Example ===")

    // Open or create a log file
    file, err := os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
    if err != nil {
        log.Fatalf("Failed to open log file: %s", err)
    }
    defer file.Close()

    // Set the log output to the file
    log.SetOutput(file)

    // Log messages
    log.Println("Application started")
    log.Printf("Logging an event at %s", "some point")
    log.Println("Application finished")

    fmt.Println("Logs have been written to app.log\n")
}

// =========================
// Example 7: crypto/rand Package
// =========================

// cryptoRandExample demonstrates generating a secure random string using the crypto/rand package.
func cryptoRandExample() {
    fmt.Println("=== crypto/rand Package Example ===")

    // Generate a secure random string of 16 bytes (32 hex characters)
    randomStr, err := generateSecureRandomString(16)
    if err != nil {
        fmt.Println("Error generating secure random string:", err)
        return
    }
    fmt.Println("Secure Random String:", randomStr, "\n")
}

// generateSecureRandomString generates a hexadecimal string of the specified byte length using crypto/rand.
func generateSecureRandomString(length int) (string, error) {
    bytes := make([]byte, length)
    _, err := crypto.Read(bytes)
    if err != nil {
        return "", err
    }
    return hex.EncodeToString(bytes), nil
}

// =========================
// Example 8: math/rand Package
// =========================

// mathRandExample demonstrates generating pseudo-random numbers and shuffling a slice using the math/rand package.
func mathRandExample() {
    fmt.Println("=== math/rand Package Example ===")

    // Seed the random number generator to ensure different outputs each run
    rand.Seed(time.Now().UnixNano())

    // Generate a random integer between 0 and 99
    randomInt := rand.Intn(100)
    fmt.Printf("Random integer (0-99): %d\n", randomInt)

    // Generate a random float between 0.0 and 1.0
    randomFloat := rand.Float64()
    fmt.Printf("Random float (0.0-1.0): %f\n", randomFloat)

    // Shuffle a slice of integers
    nums := []int{1, 2, 3, 4, 5}
    rand.Shuffle(len(nums), func(i, j int) {
        nums[i], nums[j] = nums[j], nums[i]
    })
    fmt.Printf("Shuffled slice: %v\n\n", nums)
}

// =========================
// Example 9: time Package
// =========================

// timeExample demonstrates working with current time, formatting, parsing, sleeping, and measuring durations using the time package.
func timeExample() {
    fmt.Println("=== time Package Example ===")

    // Current time
    now := time.Now()
    fmt.Println("Current time:", now)

    // Formatting time
    formatted := now.Format("2006-01-02 15:04:05")
    fmt.Println("Formatted time:", formatted)

    // Parsing time from a string
    parsed, err := time.Parse("2006-01-02", "2024-10-25")
    if err != nil {
        fmt.Println("Error parsing time:", err)
        return
    }
    fmt.Println("Parsed time:", parsed)

    // Sleep for 2 seconds
    fmt.Println("Sleeping for 2 seconds...")
    time.Sleep(2 * time.Second)
    fmt.Println("Awake!")

    // Measuring duration
    start := time.Now()
    time.Sleep(1 * time.Second)
    elapsed := time.Since(start)
    fmt.Printf("Elapsed time: %s\n\n", elapsed)
}

// =========================
// Main Function
// =========================

func main() {
    // Execute each example function sequentially
    fmtExample()
    osExample()
    strconvExample()
    netExample()
    flagExample()
    logExample()
    cryptoRandExample()
    mathRandExample()
    timeExample()

    fmt.Println("All examples executed.")
}

說真的,這太棒了!那麼,現在讓我們繼續進行測試。

  1. Go 中的測試就是這麼簡單

在我使用 Go 的第二個專案中,我看到了在建立請求和回應物件時學習測試的機會。在 PHP 環境中,您可能正在使用第三方函式庫,例如PHPUnitPest 。正確的?在 Go 環境中,這很簡單!您需要做的就是:

  • 在套件內建立一個檔案:person.go中,您將編寫要測試的函數;

  • 為您的套件建立一個測試檔案:** 建立一個名為person_test.go的檔案並開始編寫您自己的測試!

假設我們的套件資料夾中有requests.gorequests_test.go ,其中requests.go是:

package request  

import (  
    "bytes"  
    "fmt")  

type VerbType string  

const (  
    VerbGet  VerbType = "GET"  
    VerbPost VerbType = "POST"  
)  

type Request struct {  
    Verb    VerbType  
    Version string  
    Path    string  
    Headers map[string]string  
    Params  map[string]string  
    Body    string  
}  

func NewRequest(payload []byte) Request {  

    payloadSlices := bytes.Split(payload, []byte("\r\n"))  

    verb, path, version := extractRequestLine(payloadSlices[0])  
    headers := extractHeaders(payloadSlices[1 : len(payloadSlices)-2])  
    body := payloadSlices[len(payloadSlices)-1]  
    req := Request{  
       Verb:    VerbType(verb),  
       Version: version,  
       Path:    path,  
       Headers: headers,  
       Params:  map[string]string{},  
       Body:    string(body),  
    }  
    return req  
}  

func extractHeaders(rawHeaders [][]byte) map[string]string {  
    headers := make(map[string]string)  

    for _, headerBytes := range rawHeaders {  
       fmt.Printf("%v\n", string(headerBytes))  
       data := bytes.SplitN(headerBytes, []byte(": "), 2)  

       key, value := string(data[0]), string(data[1]) // Accept (?)  
       headers[key] = value  
    }  

    return headers  
}  

func extractRequestLine(requestLine []byte) (string, string, string) {  

    splitRequestLine := bytes.Split(requestLine, []byte(" "))  
    verb := string(splitRequestLine[0])  
    path := string(splitRequestLine[1])  
    version := string(splitRequestLine[2])  
    return verb, path, version  
}

5.1 測試:基本測試

如果(t *Testing.T).Errorf()未在測試函數中呼叫,則 Go 中的測試被視為PASSED (green) 。它也遵循前面介紹的相同封裝概念:

  • 以大寫字母開頭的測試函數由測試執行器辨識

  • 以小寫字母開頭的測試函數將被忽略(通常是輔助函數)

package request

import (
    "reflect"
    "testing"
)

func TestNewRequest(t *testing.T) {
    // Prepare
    payload := []byte("GET /index.html HTTP/1.1\r\nHost: localhost:4221\r\nUser-Agent: curl/7.64.1\r\nAccept: */*\r\n\r\n")

    // Act
    got := NewRequest(payload);

    // Assert 
    want := Request{
        Verb:    VerbGet,
        Version: "HTTP/1.1",
        Path:    "/index.html",
        Headers: nil,
    }

    if !reflect.DeepEqual(got, want) {
        t.Errorf("FAILED! NewRequest() = %v, want %v", got, want)
    }
}

您可以做自己的輔助函數來測試。只要確保在這些測試中不要侵入模組域即可!

5.2 測試:Jetbrains 樣板

我從第一天起就開始使用Goland ,所以大多數事情對我來說都更容易。因此,每次開始新測試時,我都會得到這個帶有預設並行執行(goroutines)的 Unity 測試結構的樣板。

package request

import (
    "reflect"
    "testing"
)

func TestNewRequest(t *testing.T) {
    type args struct {
        payload []byte
    }
    tests := []struct {
        name string
        args args
        want Request
    }{
        {
            name: "Base Request Argless",
            args: args{
                payload: []byte("GET /index.html HTTP/1.1\r\nHost: localhost:4221\r\nUser-Agent: curl/7.64.1\r\nAccept: */*\r\n\r\n"),
            },
            want: Request{
                Verb:    VerbGet,
                Version: "HTTP/1.1",
                Path:    "/index.html",
                Headers: nil,
            },
        },
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            if got := NewRequest(tt.args.payload); !reflect.DeepEqual(got, tt.want) {
                t.Errorf("NewRequest() = %v, want %v", got, tt.want)
            }
        })
    }
}

5.3 測試:執行測試

好的,現在我們知道用 Go編寫測試多麼容易,但是執行它們呢?簡單的任務!您需要做的就是導航到套件資料夾並執行:

# ~/go_fodase
cd requests
ls
# requests.go request_test.go
go test # Run all tests inside the package in all files with suffix "_go"
# ...
# PASS
# ok      github.com/danielhe4rt/go_fodase/requests/request      0.001s

go test -run=TestNewRequest # Filter an specific test
# PASS
# ok      github.com/danielhe4rt/go_fodase/requests/request      0.001s

請寫下一些對你的東西的測試。如果你解耦所需的東西,這並不難:p

6.謹防循環導入

在我過去幾年的開發過程中,我一直試圖以適合我的需求的方式模組化我的所有專案,而不是陷入「乾淨的架構」或「領域驅動設計」的東西。然而,在我第一次嘗試拆分包時,我收到了「循環導入」錯誤,並心想:我多久沒見過類似的東西了?

在我使用 PHP 的 2 年中,我遇到了同樣的import hell問題,你不能在特定的流程中匯入同一個檔案兩次。那是在我遇到PSR-4(自動加載)之前(它永遠改變了我的 PHP 時代!!),而現在,幾年前,我在 Go 中遇到了這個問題。

讓我們考慮一個循環導入的場景:

// ------------------
// A.php

require_once 'B.php';

class A {
    public function __construct() {
        $this->b = new B();
    }

    public function sayHello() {
        echo "Hello from A\n";
    }
}

// ------------------
// B.php

require_once 'A.php';

class B {
    public function __construct() {
        $this->a = new A();
    }

    public function sayHello() {
        echo "Hello from B\n";
    }
}
// ------------------
// index.php

require_once 'A.php';

$a = new A();
$a->sayHello();
// Fatal error: Uncaught Error: Maximum function nesting level of '256' reached...

當您嘗試在 Go 中編譯標記Cyclic Imports內容時,您將收到以下錯誤:

import cycle not allowed
package path/to/packageA
    imports path/to/packageB
    imports path/to/packageA

此時此刻,您必須開始分解您的依賴項/套件以避免它。

TLDR :不要在將被多次載入的地方匯入相同的套件。

  1. 延後這個,延後那個……但是,什麼是延後?

我沒有查,但這是我第一次在程式語言中看到保留字defer 。而且由於它不是“通用保留字”的一部分,我整整一周都忽略了它!

然後,我的一位同事Dusan在看到我在 Go 語言上掙扎了幾個小時後,給我上了一堂 Go 語言的記憶體管理課。 (是的,這是一個大喊大叫:p)

問題是:每當您開啟某個物件的緩衝區/連線時,您都應該關閉它!我記得2014年我使用MapleStory伺服器(Java)時,最常見的問題是記憶體洩漏,只是因為開發人員沒有關閉與資料庫的連線。

這可以忘記!但通過程式碼審查是不行的,哈哈

這是 Java 中的一個範例:

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;
import java.sql.SQLException;

public class UnclosedConnectionExample {
    public static void main(String[] args) {
        // Database credentials
        String url = "jdbc:mysql://localhost:3306/mydatabase";
        String user = "username";
        String password = "password";

        Connection conn = null;
        Statement stmt = null;
        ResultSet rs = null;

        while {
            try {
                // Establishing the connection
                conn = DriverManager.getConnection(url, user, password);
                System.out.println("Database connected successfully.");

                // Creating a statement
                stmt = conn.createStatement();

                // Executing a query
                String sql = "SELECT id, name, email FROM users";
                rs = stmt.executeQuery(sql);

                // Processing the result set
                while (rs.next()) {
                    int id = rs.getInt("id");
                    String name = rs.getString("name");
                    String email = rs.getString("email");

                    System.out.println("ID: " + id + ", Name: " + name + ", Email: " + email);
                }

                // Intentionally not closing the resources
                // rs.close() // <-------------------
                // This leads to resource leaks
            } catch (SQLException e) {
                // rs.close() // <-------------------
                e.printStackTrace();
            }
        }
    }
}

在編寫 Golang 程式碼時,他們提供了這個defer屬性,以便您記住在開啟內容後立即將其關閉。

Defer 代表“延遲”,這是在程式碼的特定部分執行結束後“清理”資源的一種方法。

import(
    "github.com/gocql/gocql"
)

func main() {
    cluster := gocql.NewCluster("localhost:9042")  
    session, err := cluster.CreateSession()  

    if err != nil {  
        log.Fatal("Connection Refused!")  
    }  
    defer session.Close() // After the function ends, the method will be called

    // Migrate stuff  
    fmt.Println("Migrating Tables...")
    database.Migrate(session)
    // ... rest of your code

    // After the last line of code, the function `session.Close()` will run without you trigger it. 
}

您也可以在一個函數內有許多defer ,並且DEFER ORDER很重要!如果您先延遲database2 ,然後推遲database1 ,則兩個進程都將以相同的順序進行清理。

import(
    "github.com/gocql/gocql"
)

func main() {
    cluster := gocql.NewCluster("localhost:9042")  

    loggingSession, _ := cluster.CreateSession()  
    appSession, _ := cluster.CreateSession()  

    defer appSession.Close()
    defer loggingSession.Close()

    // ... rest of your code
    // Defer Order
    // ...
    // appSession.close() 1st
    // loggingSession.close() 2nd
}

這是一個非常簡單的方法不要搞砸防止你的專案出現記憶體洩漏。請記住在您傳輸任何內容時使用它。

8.菜鳥錯誤管理

錯誤處理首先類似於:檢查您正在使用的函數是否返回error類型並每次每次都驗證它!這是我正在談論的一個例子:

cluster := gocql.NewCluster("localhost:9042")  
appSession, err := cluster.CreateSession()  

// NIL STUFF
if err != nil {
    // do something
    log.Fatal("Well, something went wrong.")
}
defer appSession.close()
// Keep your flow. 
// ...

老實說,我討厭這種語法。然而,它是語言的一部分,並且是您在編碼期間會遇到的東西。

有錯誤的函數可以回傳error(T, error) ,幸運的是 Go 不會讓你忘記這一點。

func SumNumbers(a, b int) (int, error) {
    result := a + b 
    if result < 0 {
        return nil, fmt.Errorf("The result is negative.")
    }

    return result, nil
}

// Using it
* always return when possible
    * spam the type error 
result, err := SumNumbers(-11, 5);

if err != nil {
    log.Fatal(err) // Ends the app or handle in the way you want.
}

err != nil發送垃圾郵件你的程式碼,你會沒事的,我保證! :D

  1. 結論壯舉:低延遲程式碼挑戰

除了花在理解環境上的所有壓力和時間之外,與我的Twitch 觀眾一起學習新語言是一個很酷的挑戰。他們中的許多人長期以來一直要求我檢查一下,我們就在這裡。

所有這些點都反映了我個人對該語言的發展經驗,目的是分享我在這兩週的學習過程中所經歷的事情。

9.1 獎勵:程式設計挑戰

最近,我的隊友要求我完成 ScyllaDB 挑戰,它教會了我很多關於並行性、池和速率限制的知識。這是許多公司為了讓自己的產品表現更好而面臨的挑戰!

挑戰的目標是建立一個小型 Go 命令列應用程式,將一些隨機資料插入 ScyllaDB,同時限制請求數量。

您可以在此處找到儲存庫挑戰: github.com/basementdevs/throttle-requests-scylla-test 。我們還招人呢!您可以在我們的職業部分找到空缺職位!

感謝您的閱讀!我希望這篇文章能為學習 Golang 提供有價值的見解。請隨意分享您的想法或經驗。


原文出處:https://dev.to/danielhe4rt/this-is-all-what-ive-learned-about-go-in-two-weeks-1bf2


共有 0 則留言