为了账号安全,请及时绑定邮箱和手机立即绑定

这就是我两周内学到的所有 Go 语言知识!

标签:
Go

当你需要学习新东西时,你是怎么做的?我有一个很独特的方法,而且在学习Golang的时候,我又一次用这种方法试试。

要聊的内容太多了,但我的目的是只想列出特意认真学习过且觉得有用的东西。

目录
    1. 序言
    1. 了解 CLI
  • 2.1 CLI:go mod

  • 2.2 CLI:go run

    1. 比较不同的语法
  • 3.1 语法:类/结构与 API 封装

  • 3.2 语法:接口实现真奇怪得要命

    1. 标准库:绝对是一套牛逼的工具包
  • 4.1 包:基本类型

  • 4.2 包:有用的工具

    1. Go 中的测试竟这么简单
  • 5.1 测试:基本测试

  • 5.2 测试:Jetbrains 模板

  • 5.3 测试:运行测试

    1. 当心循环导入
    1. 延迟这,延迟那... 但,延迟究竟是啥?
    1. 新手的错误管理入门
    1. 结尾:低延迟编码挑战
1. 序言

这两周我一直在用Golang来学习并且开发一些小应用。到现在,我已经差不多编码了50小时,之前我对这门语言有一些小困惑,但学习到现在感觉非常棒。

在这两周旅程里,我做了以下事情:

  • 一个非常小而极其简单的 shell
  • Redis 的基本实现
  • HTTP 1.1 协议实现
  • 实现了一个 DNS 服务器
  • 以及一个为真的很酷的公司做的工作测试(将在本文结尾部分提供)。

这都因为我老板又一次让我学一门新技术来用于一些ScyllaDB的PoC和演示项目……我对此决定不太满意,但没办法,这就是工作。

去年,我一直在学习Rust,可能对我来说还是有点复杂,但我学到了一些非常酷的概念,这些概念让我转到这门语言Go感觉很顺手!

这篇文章会给你一些加快学习速度的小技巧和建议。

2. 了解 CLI

我是 PHP 开发者,习惯了最好的 CLI(确实,就是 Artisan),在我的开发旅程中,我经历了许多很棒的项目之中,之中有许多项目是等……

  • Cargo (Rust)
  • NPM (JS)
  • Composer (PHP)
  • 等等

当我进入Go环境时,它开始变得非常棘手。至少对我来说,Golang在工具和文档方面的开发者体验还有很大的改进空间。考虑到这一点,我决定介绍3个你一定要掌握的命令。

记住:这只是我个人的一些解释。如果你想了解详细信息,请查看文档 :)
另外:go文档真糟糕,请给那里加上个语法高亮器

2.1 命令行界面:go mod

无论你是想让你的应用程序采用模块化设计,还是想要一个有组织的环境,这个命令在最初阶段都会非常有用。

The go mod 命令管理项目里所有的依赖,并且自动移除不再使用的任何依赖项。

首先,在你新建的空文件夹里,我们用 go mod init 初始化一个新模块。

    mkdir go-fodase
    cd go-fodase

    # 创建并进入名为go-fodase的目录
    # 注意:请将<module-name>替换为你的实际模块名称
    go mod init github.com/danielhe4rt/go-fodase

全屏进入,退出全屏

这将在项目根目录下创建一个名为go.mod的新文件,当前内容如下:

  • 模块名称
  • 你使用的 Go 版本

这里有个文件,你可以自己检查一下啊。

# 路径: ~/go-fodase
cat go.mod

# 模块定义: github.com/danielhe4rt/gofodase
# 
# go 1.23.2

全屏模式,退出全屏

之后,我最喜欢的是go mod tidy,它会自动添加缺少的依赖项并移除未使用的依赖项。

运行 `go mod tidy` 命令

点击全屏 点击退出全屏

这第二个就是要让你记住这个东西的存在,它真的很有用!你的环境每次都会运行它,你会习惯看到导入的东西消失:p

2.2 命令行工具:go run

这可能是你最常使用的命令了,因为你得运行你的项目,下面解释一下它的用法:

  • 你应该指出包含 main() 函数的那个文件。
  • 这个文件不需要放在文件夹的根目录里。

最重要的一点是,当你运行 go run 命令时,它会在当前目录中查找 go.mod 文件,并以此为基础来构建整个项目的映射(如导入和包等)。以下是一些例子:

# 运行 go run <你的主文件> (运行你的主Go文件)

# 环境示例 1
# .
# ├── go.mod
# └── main.go
执行 go run app.go

# 环境示例 2
# .
# ├── go.mod
# └── src
#     └── main.go
执行 go run src/app.go

全屏进入 退出全屏

这里是app.go内容:

    package main

    import (
        "fmt"
    )

    func main() {
        fmt.Println("嗨!别忘了给这篇文章点个赞哦 <3")
    }

进入全屏 退出全屏

现在你知道了做项目的基础知识!字面意思就是“Hello World”。

比较不同语法的差异

我对Go的主要问题一直在于它的书写风格,但经过几个小时的编码后,我意识到它比我想象中的要简单得多。正如你可能猜到的,我有很强的PHP背景,还接触过一些Rust。

当我2023年开始学习Rust时,幸运的是我非常喜欢的一个家伙,Nuno Maduro(Laravel开发者),做了一场名为"PHP for Rust Developers"的演讲,这让我对语法有了基本的了解,正好在我被Rust难倒时给了我喘息的机会。

反正当时对我很有用,所以比较一下又何妨呢?

3.1 语法结构:类/结构体及其在 API 封装技术中的应用

在面向对象编程(OOP)中,我们有类,这是一种非常酷的方式来将代码抽象成小块,而 Golang 则可以被看作是一场奥德赛般的旅程,因为它可以是一场史诗般的开发之旅,变成你想要的样子。

记得,Golang 是一种 "高级编程语言",提供了 "系统级的" 语法,让你可以轻松地处理 "底层" 实现。

在 Go 语法中,你可以编写代码。

  • [结构] 通过在前面加上 type,然后添加你的 "类/结构体" 名称,最后加上 struct 来定义一个结构体。
  • [封装] 通过将类/结构体相关元素的名称设为 大写小写 来定义它们的暴露。

  • [可见性: 公共]:将元素名称设为大写。

  • [可见性: 私有/受保护]:将元素名称设为小写。

你可以用它来使用:Structs结构字段结构方法。更详细地看一下:

    // -----------------------
    // 文件: src/users/user.go
    package users

    type User struct {  
        name string  // 首字母大写表示公开,首字母小写表示私有
        Age  uint8   // 首字母大写表示公开,首字母小写表示私有
    }  

    // 首字母大写表示公开,首字母小写表示私有
    func (u *User) getName() string {  
        return u.name  
    }  

    // 首字母大写表示公开
    func (u *User) Greetings() string {  
        cheering := "记得回关我哦!"

        // 可以调用相同结构体的函数。
        return fmt.Sprintf("嘿,%v (%v)! %v !", u.getName(), u.Age, cheering)
    } 

    // -----------------
    // 文件: src/main.go
    package main  

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

    func main() {  
        // ❌ 记得使用 Setter 函数来设置 'name'。    
        // user := users.User{    
        //     name: "danielhe4rt", // ❌
        //     Age: 25,            // ✅
        // }

        // ✅ 现在你只初始化所需的字段就可以了。    
        user := users.User{Age: 25}  

        // 方法调用如下:
        user.SetName("danielhe4rt") // ✅
        user.getName()              // ❌
        user.Greetings()            // ✅

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

全屏模式,退出全屏

在 Rust 里,你有一种更明确的方式(更类似于面向对象的语言编程风格):

  • [结构体] 使用前缀 struct 定义一个结构体。
  • [封装] 如果你希望让某些内容对其他 "crate" 可见,你可以在你想公开的代码部分加上 pub 关键字。注释:"crate" 是 Rust 中的术语,表示可独立编译的模块。
    // 文件: src/users/mod.rs

    // 公开 `User` 结构体
    pub struct User {
        // 私有字段: 只能在 `users` 模块内部访问
        name: String,
        // 公有字段: 可以从其他模块访问
        pub age: u8,
    }

    impl User {
        // 公有方法用于创建一个带有名字和年龄的新 User
        pub fn new(age: u8) -> Self {
            User {
                name: String::new(), // 用空名字初始化
                age,
            }
        }

        // 公有设置方法用于设置私有 `name` 字段
        pub fn set_name(&mut self, name: String) {
            self.name = name;
        }

        // 私有方法用于获取名字
        fn get_name(&self) -> &str {
            &self.name
        }

        // 公有方法使用公共和私有字段及方法
        pub fn greetings(&self) -> String {
            let cheering = "别忘了回关我!";
            format!(
                "嘿, {} ({} 岁)! {}",
                self.get_name(),
                self.age,
                cheering
            )
        }
    }

    // 文件: src/main.rs
    mod users;  

    use crate::users::User;  

    fn main() {
        // ❌ 你不能直接赋值私有 `name` 字段(field)
        // let user = User {
        //     name: String::from("danielhe4rt"), // ❌
        //     age: 25,                           // ✅
        // };

        // ✅ 使用构造函数初始化 User
        let mut user = User::new(25);

        // ✅ 使用设置方法设置私有 `name` 字段(field)
        user.set_name(String::from("danielhe4rt"));

        let greeting = user.greetings();    // ✅ 调用公共 `greetings` 方法
        let current_age = user.age;         // ✅ 直接访问公有 `age` 字段(field)
        let current_name = user.name;       // ❌ 你不能直接访问私有 `name` 字段(field)
        let current_name = user.get_name(); // ❌ 你不能直接调用私有 `get_name` 方法(method)
    }

全屏 退出全屏

我想让事情像 PHP 和 Java 一样明确,但细想一下,这样其实代码量 更少,但这也影响了可读性。

3.2 语法:真是古怪得要命的接口实现!

说实话,我就是那种尝试将 LARAVEL 放到 Go 环境中的人,但具体怎么操作就不知道了。不过这个已经有人通过 [Goravel]() 实现了。不过,我对 ‘基于接口/契约的开发’ 这个理念很感兴趣,这也是我第一次在这个语言中遇到这种开发模式的困扰。

在 Go 中,接口不会直接在结构体或类中实现,像我这样的面向对象程序员觉得这种设计简直疯狂。来看看具体的期望:

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

    /**

* GitHub OAuth 提供者实现 OAuthContract 接口
     */
    class GithubOAuthProvider implements OAuthContract{
        public function redirect(string $code) {}
        public function authenticate(string $token) {}
    }

    /**

* Spotify OAuth 提供者实现 OAuthContract 接口
     */
    class SpotifyAuthProvider implements OAuthContract{
        public function redirect(string $code) {}
        public function authenticate(string $token) {}
    }

    /**

* 根据 $routeProvider 的值返回相应的 OAuth 接口实现
     */
    function authenticate(string $routeProvider): OAuthContract {
        return match($routeProvider) {
            'github' => new GithubOAuthProvider(),
            'spotify' => new SpotifyAuthProvider(),
            default => throw OAuthProviderException::notFound(),
        };
    }

    authenticate('github');

全屏 退出全屏

现在说到 Go:你没有在结构体内显式实现一个“接口”,这……有点奇怪?相反,你只需要实现接口要求的方法,Go 会在编译时检查这些方法。不过,作为一门编译型语言,这通常不会成为问题,但从我的角度来看,这会影响到开发者体验

import (
    "errors"
    "fmt"
)

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

// GithubOAuthProvider 实现了 GitHub 的 OAuthContract 接口。
type GithubOAuthProvider struct{}

// 处理 GitHub OAuth 重定向逻辑。
func (g *GithubOAuthProvider) Redirect(code string) error {
    // 在此实现 GitHub 认证逻辑。
    return nil
}

// 处理 GitHub 的认证逻辑。
func (g *GithubOAuthProvider) Authenticate(token string) error {
    // 在此实现 GitHub 认证逻辑。
    return nil
}

// SpotifyOAuthProvider 实现了 Spotify 的 OAuthContract 接口。
type SpotifyOAuthProvider struct{}

// 处理 Spotify OAuth 重定向逻辑。
func (s *SpotifyOAuthProvider) Redirect(code string) error {
    // 在此实现 Spotify 认证逻辑。
    fmt.Printf("重定向到 Spotify,代码为: %s\n", code)
    return nil
}

// 处理 Spotify 的认证逻辑。
func (s *SpotifyOAuthProvider) Authenticate(token string) error {
    // 在此实现 Spotify 认证逻辑。
    fmt.Printf("认证 Spotify 令牌: %s\n", token)
    return nil
}

// 认证工厂是一个工厂函数,用于返回 OAuthContract 实例。
func AuthenticateFactory(provider string) (OAuthContract, error) {
    switch provider {
    case "github":
        return &GithubOAuthProvider{}, nil
    case "spotify":
        return &SpotifyOAuthProvider{}, nil
    default:
        return nil, errors.New("未找到提供者 '%s'")
    }
}

全屏查看 退出全屏

无论如何,用这种语言编程一段时间,你就会渐渐熟悉这种语言。现在,让我们来谈谈基本环境能为你提供什么,无需下载任何东西就。

4. 标准库 (Stdlib):绝对是一个酷毙了的工具包

我现在说的是Go自带的标准库,不需要下载任何第三方库。下面是一些时间线供你参考:

  • 第一天: 怎么回事?为什么不像JavaScript/Java那种类型自带所有方法?(我讨厌这两种语言)
  • 第一周: 等等,也许这样挺好的(在理解了基本类型的包之后)
  • 第二周: 怎么回事?为什么其他语言没有这么好的内置库?

我不是在开玩笑,每天我探索go时,我总能找到一些很酷的库,这些库在标准库之外的某个地方。所以,我们来谈谈基本类型吧。

4.1 包:基本类型

就像 PHP,不像许多其他语言(如 Rust、Java、JS 等),Golang 需要 "帮助" 函数来执行大多数 类型相关的操作。我们可以将它们视为“瘦弱类型”,因为它们没有内置有用的工具方法。

    // PHP 示例(带有非常糟糕的 API)
    // 使用内置的 PHP 函数进行字符串操作
    $str = "Hello, World!";

    // 检查是否包含子字符串
    $contains = strpos($str, "World") !== false;
    echo "包含 'World': " . ($contains ? "是" : "否") . "\n"; // 输出: 是

    // 转换为大写
    $upper = strtoupper($str);
    echo "大写: " . $upper . "\n"; // 输出: HELLO, WORLD!

    // 按逗号分割
    $split = explode(",", $str);
    echo "按逗号分割: ";
    echo print_r($split, true) . "\n"; // 输出: Array ( [0] => Hello  [1] =>  World! )

全屏模式 退出全屏

所以如果你在处理一个 "String" 数据类型 的数据,你可以使用如 strconvstrings 这样的包来操作它。但是这里有一个黄金法则,你永远不要忘记检查哪个包:如果你的类型是 string,就找一个复数形式的包,如 strings

简单来说,这将给你带来与 []TypeType 相关的功能和用法:

  • 字符串类型(string) -> import ("strings") 用于操作如“包含(Contains())”,“转大写(Upper())”,“分割(Split())”之类的
  • 字节类型(bytes) -> import ("bytes") 用于操作如“包含(Include())”,“比较(Compare())”,“分割(Split())”之类的
  • 等等

看下代码,你可以自己验证。

    package 主

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

    func main() {
        // 字符串操作

        str := "Hello, World!"

        // 检查字符串是否包含子字符串
        contains := strings.Contains(str, "World")
        fmt.Printf("包含 'World': %v\n", contains) // 输出:true

        // 将字符串转换为大写
        upper := strings.ToUpper(str)
        fmt.Printf("大写: %s\n", upper) // 输出:HELLO, WORLD!

        // 按逗号分割字符串
        split := strings.Split(str, ",")
        fmt.Printf("按逗号分割: %v\n", split) // 输出:[Hello  World!]
    }

全屏模式 退出全屏

這本來應該是個簡單的問題,但我老是搞不定,直到後來才掌握要領。也許是使用 Laravel 和它的輔助功能太多年,讓我忘了沒有框架編程有多難了 :D

4.2 包(packages):实用的东西(小节)

在我探索工具和项目的过程中,我了解了许多项目,并想列出一下每个项目和我使用过的库:

  • 自己动手做Shell挑战:

  • 包:

  • fmt: 输入输出库(从屏幕上读取和写入内容)

  • os: 直接与操作系统交互的功能和助手。

  • strconv: 将特定数据类型转换为字符串或将字符串转换为任何定义的数据类型。

  • 自己动手搭建 (HTTP|DNS) 服务器挑战:

  • 包:

  • net: 与网络 I/O 协议(如 HTTP、TCP、UDP 和 Unix 域套接字)进行集成的中间层

  • [之前的包...]

  • 中级作业任务分配?

  • 包:

  • flag: 捕获命令行参数到变量中

  • log: 向应用程序添加日志通道功能

  • crypto/rand: 安全的加密随机生成器

  • math/rand: 数学随机数生成器

  • time: 时间/日期库

这里有一个可滚动的视图,展示了所有包的实现情况,你可以看看。这里有很多很酷的标准库包可以介绍。

注意:这可是好多代码哦! :p
别忘了给你的最爱功能加个注释哦(除了goroutines和channels之外的) :p

    package main

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

    // =========================
    // 示例 1: fmt 包
    // =========================

    // fmtExample 使用 fmt 包演示基本的输入输出操作。
    func fmtExample() {
        fmt.Println("=== fmt 包示例 ===")
        var name string
        var age int

        // 提示用户输入姓名
        fmt.Print("请输入您的姓名: ")
        _, err := fmt.Scanln(&name)
        if err != nil {
            fmt.Println("读取姓名时出错:", err)
            return
        }

        // 提示用户输入年龄
        fmt.Print("请输入您的年龄: ")
        _, err = fmt.Scanln(&age)
        if err != nil {
            fmt.Println("读取年龄时出错:", err)
            return
        }

        // 显示收集的信息
        fmt.Printf("你好, %s! 你今年 %d 岁。\n", name, age)
    }

    // =========================
    // 示例 2: os 包
    // =========================

    // osExample 展示如何使用 os 包与操作系统进行交互,例如读取环境变量和退出程序。
    func osExample() {
        fmt.Println("=== os 包示例 ===")

        // 读取 HOME 环境变量
        home := os.Getenv("HOME")
        if home == "" {
            fmt.Println("HOME 环境变量未设置。")
            // 用非零状态码退出程序以表示错误
            // 取消下面一行的注释以启用退出
            // os.Exit(1)
        } else {
            fmt.Printf("您的主目录是: %s\n", home)
        }

        // 展示成功退出程序
        // 取消下面几行的注释以启用退出
        /*
            fmt.Println("程序退出码为 0。")
            os.Exit(0)
        */
        fmt.Println()
    }

    // =========================
    // 示例 3: strconv 包
    // =========================

    // strconvExample 展示如何在字符串和其他数据类型之间进行转换。
    func strconvExample() {
        fmt.Println("=== strconv 包示例 ===")

        // 将字符串转换为整数
        numStr := "456"
        num, err := strconv.Atoi(numStr)
        if err != nil {
            fmt.Println("字符串转换为整数时出错:", err)
            return
        }
        fmt.Printf("字符串 '%s' 转换为整数: %d\n", numStr, num)

        // 将整数转换回字符串
        newNumStr := strconv.Itoa(num)
        fmt.Printf("整数 %d 转换回字符串: '%s'\n\n", num, newNumStr)
    }

    // =========================
    // 示例 4: net 包
    // =========================

    // netExample 使用 net 包演示简单的 HTTP GET 请求。
    func netExample() {
        fmt.Println("=== net 包示例 ===")

        // 向公共 API 发出 HTTP GET 请求
        resp, err := http.Get("https://api.github.com")
        if err != nil {
            fmt.Println("发出 HTTP GET 请求时出现错误:", err)
            return
        }
        defer resp.Body.Close()

        // 显示 HTTP 状态码
        fmt.Printf("响应状态: %s\n")
    }

    // =========================
    // 示例 5: flag 包
    // =========================

    // flagExample 展示如何使用 flag 包捕获命令行参数。
    func flagExample() {
        fmt.Println("=== flag 包示例 ===")

        // 定义命令行标志
        name := flag.String("name", "World", "向其打招呼的名字")
        age := flag.Int("age", 0, "你的年龄")

        // 解析标志
        flag.Parse()

        // 使用标志值
        fmt.Printf("你好, %s!\n", *name)
        if *age > 0 {
            fmt.Printf("你今年 %d 岁。\n", *age)
        } else {
            fmt.Println()
        }
    }

    // =========================
    // 示例 6: log 包
    // =========================

    // logExample 使用 log 包演示将日志消息写入文件。
    func logExample() {
        fmt.Println("=== log 包示例 ===")

        // 打开或创建日志文件
        file, err := os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
        if err != nil {
            log.Fatalf("打开日志文件失败: %s", err)
        }
        defer file.Close()

        // 设置日志输出到文件
        log.SetOutput(file)

        // 记录日志消息
        log.Println("应用程序启动")
        log.Printf("在某个时间点记录事件")
        log.Println("应用程序结束")

        fmt.Println("日志已写入 app.log")
    }

    // =========================
    // 示例 7: crypto/rand 包
    // =========================

    // cryptoRandExample 使用 crypto/rand 包生成安全的随机字符串。
    func cryptoRandExample() {
        fmt.Println("=== crypto/rand 包示例 ===")

        // 生成 16 字节(32 个十六进制字符)的安全随机字符串
        randomStr, err := generateSecureRandomString(16)
        if err != nil {
            fmt.Println("生成安全随机字符串时出现错误:", err)
            return
        }
        fmt.Println("安全随机字符串:", randomStr, "\n")
    }

    // generateSecureRandomString 使用 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
    }

    // =========================
    // 示例 8: math/rand 包
    // =========================

    // mathRandExample 使用 math/rand 包生成伪随机数并打乱切片。
    func mathRandExample() {
        fmt.Println("=== math/rand 包示例 ===")

        // 使用当前时间作为随机数生成器的种子,以确保每次运行有不同的输出
        rand.Seed(time.Now().UnixNano())

        // 生成一个介于 0 到 99 之间的随机整数
        randomInt := rand.Intn(100)
        fmt.Printf("随机整数 (0-99): %d\n", randomInt)

        // 生成一个介于 0.0 到 1.0 之间的随机浮点数
        randomFloat := rand.Float64()
        fmt.Printf("随机浮点数 (0.0-1.0): %f\n", randomFloat)

        // 打乱一个整数切片
        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("打乱后的切片: %v\n\n", nums)
    }

    // =========================
    // 示例 9: time 包
    // =========================

    // timeExample 使用 time 包演示当前时间、格式化、解析、睡眠和测量持续时间。
    func timeExample() {
        fmt.Println("=== time 包示例 ===")

        // 当前时间
        now := time.Now()
        fmt.Println("当前时间:", now)

        // 格式化时间
        formatted := now.Format("2006-01-02 15:04:05")
        fmt.Println("格式化时间:", formatted)

        // 从字符串解析时间
        parsed, err := time.Parse("2006-01-02", "2024-10-25")
        if err != nil {
            fmt.Println("解析时间时出错:", err)
            return
        }
        fmt.Println("解析时间:", parsed)

        // 睡眠 2 秒
        fmt.Println("睡眠 2 秒...")
        time.Sleep(2 * time.Second)
        fmt.Println("醒来。")

        // 测量持续时间
        start := time.Now()
        time.Sleep(1 * time.Second)
        elapsed := time.Since(start)
        fmt.Printf("持续时间: %s\n")
    }

    // =========================
    // 主函数
    // =========================

    func main() {
        // 依次执行每个示例
        fmtExample()
        osExample()
        strconvExample()
        netExample()
        flagExample()
        logExample()
        cryptoRandExample()
        mathRandExample()
        timeExample()

        fmt.Println("所有示例已执行。")
    }

全屏(按后退键退出)

说起来,这真是太厉害了!所以,咱们现在继续做测试吧。

5. 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  
        版本 string  
        Path    string  
        Headers map[string]string  
        Params  map[string]string  
        Body    string  
    }  

    func NewRequest(有效负载 []byte) Request {  

        payloadSlices := bytes.Split(有效负载, []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,  
           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]) // 接收  
           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: 测试:基本的测试

在 Go 中,如果在你的测试函数中没有调用 (t *Testing.T).Errorf(),该测试将被视为 PASSED (通过)。换句话说,它也遵循了之前介绍的封装的思想。这是一段 Go 语言中的代码。

  • 以大写字母开头的测试函数,测试运行器会识别。
  • 以小写字母开头的测试函数,将被忽略,(通常是辅助函数)。
    package request

    import (
        "reflect"
        "testing"
    )

    func TestNewRequest(t *testing.T) {
        // 准备阶段
        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")

        // 执行阶段
        got := NewRequest(payload);

        // 断言 
        want := Request{
            HTTP动词:    VerbGet,
            协议版本:    "HTTP/1.1",
            路径:    "/index.html",
            Header: nil,
        }

        if !reflect.DeepEqual(got, want) {
            t.Errorf("失败!NewRequest() = %v, 欲得 %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: "基础请求无参数请求",
            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 # 运行包内所有文件中带有 "_test" 后缀的测试
    # ...
    # PASS
    # ok      github.com/danielhe4rt/go_fodase/requests/request      0.001s

    go test -run=TestNewRequest # 运行特定的测试
    # PASS
    # ok      github.com/danielhe4rt/go_fodase/requests/request      0.001s

进入全屏模式,退出全屏

请写下一些测试用例来测试你的功能。如果你把功能分开来考虑,这其实并不难, XD

6. 小心循环引用

在我最后几年的开发中,我总是努力将所有项目模块化以适应我的需求,而不是被诸如“干净架构”或“领域驱动设计”之类的概念束缚。然而,在我第一次尝试拆分包时,遇到了“循环导入”错误,心想:我有多久没遇到这种错误了?

在我使用 PHP 的两年期间,我也遇到了 导入噩梦 的问题,即在同一个流程中不能重复导入同一个文件。这发生在遇到 [PSR-4 (自动加载)](彻底改变了我的 PHP 之旅!!) 之前,而现在,我在使用 Go 时也遇到了同样的困扰。

我们来考虑一下循环导入的情况:

    // ------------------
    // A.php
    // A 类是一个示例类
    // 这是一个示例代码

    require_once 'B.php';

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

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

    // ------------------
    // B.php
    // B 类是一个示例类
    // 这是一个示例代码

    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();
    // 致命错误:未捕获的错误:函数嵌套级别达到'256'...

切换到全屏 退出全屏

当你尝试编译遇到循环导入错误信息的Go代码时,你将会看到类似的错误信息:

    不允许导入循环
    包 path/to/packageA:
        导入 path/to/packageB
        导入 path/to/packageA

全屏;退出全屏

在这个时候,你必须开始拆解你的依赖项/包以避免这个问题。

TLDR : 不要在会被频繁加载的地方导入相同的包。

7. 拖拖拉拉这个,拖拖拉拉那个...但是,到底什么是“拖延”呢?

我没查过,但这是我第一次在一个编程语言里看到 defer 这个保留字。因为它不属于普通的保留字,我整整一周都没理它!

然后我的一个同事,Dusan,在我苦苦挣扎了几个小时后学习这门语言后,给我讲解了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) {
            // 数据库凭证
            String url = "jdbc:mysql://localhost:3306/mydatabase";
            String user = "username";
            String password = "password";

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

            while (true) {
                try {
                    // 建立连接
                    conn = DriverManager.getConnection(url, user, password);
                    System.out.println("数据库连接成功。");

                    // 创建一个语句
                    stmt = conn.createStatement();

                    // 执行查询
                    String sql = "SELECT id, name, email FROM users";
                    rs = stmt.executeQuery(sql);

                    // 处理结果集
                    while (rs.next()) {
                        int id = rs.getInt("id");
                        String name = rs.getString("name");
                        String email = rs.getString("email");

                        System.out.println("ID: " + id + ", 名字: " + name + ", 邮箱: " + email);
                    }

                    // 故意不关闭资源
                    // rs.close() // <-------------------
                    // 这会导致资源泄露
                } 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("连接失败!")  
        }  
        defer session.Close() // 会在函数结束时延迟调用

        // 迁移表
        fmt.Println("正在迁移表...")
        database.Migrate(session)
        // ... 其余代码

        // 当所有代码执行完毕后,`session.Close()` 会自动调用,无需手动触发。 
    }

切换到全屏模式 退出全屏

你也可以在函数中多次使用 defer,而且DEFER 顺序也很关键!如果你先 defer database2,然后再 defer database1,这两个 defer 的清理将会按你指定的顺序进行。

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

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

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

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

        // ... 代码的其余部分
        // 延迟关闭的顺序
        // ...
        // 首先关闭 appSession
        // 其次关闭 loggingSession
    }

全屏模式 退出全屏

这是一个非常简单的方法来防止你的项目出现内存泄漏问题。请记得在处理任何流的时候都使用它。

8. 新手错误处理

刚开始处理错误时,你会遇到这样的情况:每次使用该函数时,都要检查它是否返回了一个 error 类型,并进行验证。这里举个例子说明:

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

    // 处理错误
    if err != nil {
        // 处理错误
        log.Fatal("出了点小问题。")
    }
    defer appSession.Close()
    // ...

切换到全屏模式/退出全屏

说实话,我真的超级讨厌这种语法。不过,它是语言的一部分,在你编程的时候你经常会碰到它。

错误函数可以返回 error,或者 (T, error),Go 语言绝对不会让你忘记这一点。

    func SumNumbers(a, b int) (int, error) {
        result := a + b 
        if result < 0 {
            return nil, fmt.Errorf("结果是负数。")
        }

        return result, nil
    }

    // 调用方式

* 尽量返回可能的结果

* 尽量详细地描述错误类型
    result, err := SumNumbers(-11, 5);

    if err != nil {
        log.Fatal(err) // 结束应用或按你的方式处理。
    }

全屏模式,退出全屏

在你的代码里到处放 err != nil,这样我保证你不会有事,绝对没问题!:D

9. 结论 特辑:低延迟编码挑战

除了所有的压力和理解环境的时间之外,和我的Twitch观众一起学新语言是一个很有趣的挑战。他们中的许多人都一直催我学新语言,而现在我们终于开始了。

以下几点是我学习这门语言两周以来的一些体会,想和大家分享一下。

9.1 加分环节:编码挑战

最近,我被我的队友向我发起了一个ScyllaDB的挑战,我从中学到了很多关于并行处理、池和速率限制的知识。这种挑战正是许多公司在提升产品性能时经常遇到的!

挑战的目标是创建一个小型的 Go 命令行应用程序,该程序在插入一些随机数据到 ScyllaDB 的同时限制请求速率。

你可以在这里找到挑战:github.com/DanielHe4rt/throttiling-requests-scylladb-go-test。我们在招人哦!我们有职位空缺,快来查看吧!

谢谢大家的阅读!希望这篇文章能帮助你在学习Golang时获得一些有用的见解,欢迎分享你的想法或经历。

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消