GO语言工程实践课课后作业 | 青训营

88 阅读9分钟

Go语言原理与实践

Go语言的实战案例课后作业

修改第一个猜谜游戏

使用fmt.Scanf()优化代码,原来使用的input, err := reader.ReadString('\n')读取到行尾,结果保存为字符串,然后使用strings.Trim(input, "\r\n")去掉后面的换行符。再使用strconv.Atoi()转换为整数。 优化:

        var guess int
        _, err := fmt.Scanf("%d\n", &guess)
        if err != nil {
                fmt.Println("输入有误", err)
        }

修改第二个例子,增加另一种翻译引擎支持

选择腾讯翻译。 进行抓包,将curl转化为go代码。 新建一个func,放置这些代码。

var data = strings.NewReader(`source=en&target=zh&sourceText=` + word + `...`)
req, err := http.NewRequest("POST", "https://fanyi.qq.com/api/translate", data)

使用字符串拼接,查询传入的单词。 然后解析返回的json,构建结构,提取翻译。

image.png

实现并行请求两个翻译引擎来提高响应速度

最简单的实现是使用go直接开启协程,然后在主协程中等待1秒。

	go query(word)
	go queryTencent(word)
	time.Sleep(time.Second)

这里Sleep时间固定,如果超时任务未完成,也会结束,或者早已完成阻塞在sleep浪费时间。 使用WaitGroup进行阻塞:

    var wg sync.WaitGroup
    wg.Add(2)
    query(word, wg.Done)
    queryTencent(word, wg.Done)
    wg.Wait()

然后给两个函数开始设置defer done()

Go语言工程实践课后实践

要求:

  • 支持发布帖子的功能
  • 保证id唯一性
  • Append文件,更新索引,处理Map的并发问题
    实现思路:
    数据层Repository:使用UUID保证id的唯一性,每次发布新帖子的时候,追加到文件,并更新索引。
    逻辑层Service:处理业务逻辑。

HTTP框架课后作业

为什么HTTP框架要做分层设计,分层设计有哪些优势和劣势

主要原因是为了提高系统的可扩展性、可维护性和可重用性。HTTP的分层设计可以将应用程序的功能分成多个抽象层次,每个层次都有不同的职责和功能,从而更好地实现了系统的模块化和解耦。这种分层设计可以使得系统的不同层次之间的变更和修改相互独立,从而更容易实现系统的维护和扩展。
优势:

  • 模块化:将系统分成多个模块,每个模块都可以独立开发、测试和维护,从而提高了系统的可维护性
  • 解耦:不同层次之间的解耦可以使得系统的不同模块之间的变化不会对其他模块造成影响,从而提高了系统的可重用性和可扩展性
  • 可替换性:分层设计可以使得系统的不同层次之间可以更容易地进行替换和升级,从而提高了系统的灵活性
  • 可拓展性:分层设计可以使得系统的不同层次之间可以更好地进行扩展和升级,从而更容易适应不同的需求和变化

劣势:

  • 性能问题:分层设计可能会导致系统的性能下降,因为每个层次都需要进行额外的处理和通信
  • 复杂性:分层设计可能会增加系统的复杂性,因为需要考虑不同层次之间的交互和通信
  • 开发难度:分层设计可能会增加系统的开发难度,因为需要考虑每个层次之间的接口和交互

现有开源社区HTTP框架有哪些优势与不足

优势:

  • 开源:开源HTTP框架可以让开发者自由地使用、修改和分发代码,从而降低了开发成本和维护成本
  • 社区:开源HTTP框架通常有活跃的社区支持,可以提供技术支持、文档和示例代码等资源,从而加速开发进程
  • 拓展插件:开源HTTP框架通常具有丰富的功能和插件,可以满足各种需求和场景
  • 可定制:开源HTTP框架可以根据具体需求进行定制和扩展,从而更好地满足用户的需求

不足:

  • 质量:质量可能不稳定,因为代码质量和维护水平可能存在差异
  • 安全:可能存在安全漏洞,因为代码可能存在缺陷或者被恶意修改
  • 文档:文档可能不全面或者不够清晰,需要开发者自行探索和解决问题
  • 社区:社区可能维护不足,无法及时解决问题或者提供技术支持

以Spring MVC为例,以上开源框架的优势都有,但也存在学习曲线陡峭、配置复杂以及性能问题。针对这些优势与不足,需要开发者根据项目需求和技术水平对HTTP框架进行选择。

中间件的其他实现方式

链式调用模型:中间件被组织成一个链表结构,每个中间件只处理自己负责的部分,然后将请求传递给下一个中间件,直到请求被处理完成。这种模式可以使得每个中间件只关注自己的任务。

class Middleware {
    constructor(nextMiddleware) {
        this.nextMiddleware = nextMiddleware;
    }

    handleRequest(request) {
        // 处理请求
        // ...

        // 转发请求给下一个中间件
        if (this.nextMiddleware) {
            this.nextMiddleware.handleRequest(request);
        }
    }
}

// 构建中间件链
const middleware1 = new Middleware();
const middleware2 = new Middleware(middleware1);
const middleware3 = new Middleware(middleware2);

// 处理请求
middleware3.handleRequest(request);

消息队列模式:将中间件作为消息的中转站。请求被转换成消息并发送到消息队列中,然后中间件从消息队列中获取消息并处理请求。这种模式可以实现异步处理和解耦。

// 发送消息
messageQueue.send(request);

// 处理消息
messageQueue.on('message', (message) => {
    // 处理请求
    // ...
});

过滤器模式:将中间件视为过滤器。请求被传递给中间件,中间件根据特定的规则进行过滤和处理,然后将请求传递给下一个中间件。这种模式可以实现请求过滤和验证。

class Middleware {
    constructor() {
        this.filters = [];
    }

    addFilter(filter) {
        this.filters.push(filter);
    }

    handleRequest(request) {
        // 应用过滤器
        for (let filter of this.filters) {
            if (!filter.filter(request)) {
                // 请求被过滤,终止处理
                return;
            }
        }

        // 处理请求
        // ...
    }
}

class Filter {
    filter(request) {
        // 过滤请求
        // ...
        return true; // 返回true表示请求通过过滤器
    }
}

// 创建中间件对象并添加过滤器
const middleware = new Middleware();
middleware.addFilter(new Filter());

// 处理请求
middleware.handleRequest(request);

基于前缀路由树的注册与查找功能

前缀路由树(Prefix Trie)是一种经典的数据结构,常用于实现字符串的快速查找、匹配和前缀匹配等功能。在网络编程中,它也经常被用于实现路由表、域名解析等功能。
注册就是将一个字符串添加到前缀路由树中,具体步骤:

  1. 创建树的根节点
  2. 从字符串的第一个字符开始,依次遍历字符串中的每个字符,如果当前字符在前缀路由树中不存在,则在前缀路由树中创建一个新的节点,并将当前字符添加到该节点中
  3. 如果当前字符在前缀路由树中已经存在,则直接跳到下一个字符
  4. 遍历完整个字符串后,在最后一个字符所对应的节点中设置一个标记,表示该节点是一个单词的结尾

查找就是找到一个字符串,具体步骤:

  1. 从前缀路由树的根节点开始,依次遍历字符串中的每个字符,如果当前字符在前缀路由树中不存在,则表示该字符串在前缀路由树中不存在,直接返回 false
  2. 如果当前字符在前缀路由树中存在,则跳到下一个字符所对应的节点中
  3. 遍历完整个字符串后,检查最后一个字符所对应的节点是否设置了标记,如果设置了标记,则表示该字符串在前缀路由树中存在,返回 true,否则返回 false

算法特点:插入和查找的时间复杂度为O(m),m为字符串长度;适用于静态数据集;空间利用率高但空间占用大,因为每个节点都会占用空间;可以用于前缀匹配、自动补全等功能。
代码模拟示例:

package main

import (
    "fmt"
)

// 前缀路由树的节点结构
type TrieNode struct {
    children map[byte]*TrieNode  // 子节点
    isWordEnd bool               // 是否为单词结尾
}

// 创建前缀路由树的根节点
func NewTrieNode() *TrieNode {
    return &TrieNode{
        children: make(map[byte]*TrieNode),
        isWordEnd: false,
    }
}

// 向前缀路由树中插入一个单词
func Insert(root *TrieNode, word string) {
    node := root
    for i := 0; i < len(word); i++ {
        ch := word[i]
        if _, ok := node.children[ch]; !ok {
            node.children[ch] = NewTrieNode()
        }
        node = node.children[ch]
    }
    node.isWordEnd = true
}

// 在前缀路由树中查找一个单词
func Search(root *TrieNode, word string) bool {
    node := root
    for i := 0; i < len(word); i++ {
        ch := word[i]
        if _, ok := node.children[ch]; !ok {
            return false
        }
        node = node.children[ch]
    }
    return node.isWordEnd
}

func main() {
    root := NewTrieNode()
    words := []string{"apple", "banana", "orange", "pear"}
    for _, word := range words {
        Insert(root, word)
    }
    fmt.Println(Search(root, "apple"))    // true
    fmt.Println(Search(root, "banana"))   // true
    fmt.Println(Search(root, "orange"))   // true
    fmt.Println(Search(root, "pear"))     // true
    fmt.Println(Search(root, "grape"))    // false
    fmt.Println(Search(root, "app"))      // false
}
  • 定义了前缀路由树的节点结构 TrieNode,其中包含一个 children 字段,用于存储子节点,以及一个 isWordEnd 字段,用于表示当前节点是否为单词结尾。
  • NewTrieNode 函数,用于创建前缀路由树的根节点。
  • Insert 函数,用于向前缀路由树中插入一个单词。
  • Search 函数,用于在前缀路由树中查找一个单词。

路由的其他实现方式

除了Trie树,还有很多其他的实现方式。
哈希表:每个路由规则可以看作是一个键值对,其中键为 URL(或者路径),值为处理该 URL 的处理器函数。在路由表中,可以将 URL 作为键,处理器函数作为值,将它们存储在哈希表中。当请求到达时,可以根据请求的 URL 在哈希表中查找对应的处理器函数,然后执行该函数。
优点是查找快,时间复杂度是O(1),但空间占用大。
正则表达式:将正则表达式作为键,处理器函数作为值,将它们存储在一个集合中。当请求到达时,可以遍历集合中的正则表达式,找到第一个匹配的表达式,然后执行该表达式对应的处理器函数。
优点是可以实现非常灵活的路由匹配规则,适用于需要精确匹配的场景。缺点是匹配效率低,时间复杂度为O(n)。
有限状态机:将每个路由规则看作是一个状态,将 URL 看作是状态转换的输入。当请求到达时,可以根据当前状态和输入,转换到下一个状态,并执行该状态对应的处理器函数。
优点是匹配效率高,时间复杂度是O(1)。缺点是实现复杂度较高,需要在设计阶段考虑清楚所有可能的状态转换。