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

148 阅读8分钟

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

1.修改猜谜游戏的最终代码,使用fmt.Scanf来简化代码

(1)课堂代码

package main  
  
import (  
"bufio"  
"fmt"  
"math/rand"  
"os"  
"strconv"  
"strings"  
"time"  
)  
  
func main() {  
    maxNum := 100  
    // 随机种子  
    rand.Seed(time.Now().UnixNano())  
    secretNumber := rand.Intn(maxNum)  
  
    fmt.Println("Please input your guess")  
    reader := bufio.NewReader(os.Stdin)  
    // 带入循环,支持多次猜测数字  
    for {  
        input, err := reader.ReadString('\n')  
        if err != nil {  
            fmt.Println("An error occured while reading input. Please try again", err)  
            continue  
        }
 //在这里需要再添加对"\r"的处理,否则将输出Invalid input. Please enter an integer value
        input = strings.TrimSuffix(input, "\n")  
        input = strings.TrimSuffix(input, "\r")  
  
        guess, err := strconv.Atoi(input)  
        if err != nil {  
            fmt.Println("Invalid input. Please enter an integer value")  
            continue  
        }  
        fmt.Println("You guess is", guess)  
        if guess > secretNumber {  
            fmt.Println("Your guess is bigger than the secret number. Please try again")  
        } else if guess < secretNumber {  
            fmt.Println("Your guess is smaller than the secret number. Please try again")  
        } else {  
            fmt.Println("Correct, you Legend!")  
            break  
        }  
    }  
}

(2)实现过程

bufio与fmt.Scanf

bufiofmt.Scanf 都是 Go 语言中用于读取用户输入的方法,它们具有不同的特点和适用场景。

  1. bufiobufio 提供了一个用于缓冲读取的接口,可以方便地读取用户输入的文本行或特定分隔符之前的内容。使用 bufioNewReader 函数创建一个读取器,可以通过调用其 ReadStringReadBytes 等方法进行读取操作。bufio 提供了更灵活的读取方式,并且对于处理大量输入以及逐行读取的情况效果更好。
  2. fmt.Scanffmt.Scanffmt 包提供的一个函数,用于从标准输入读取格式化的输入数据。它根据指定的格式字符串解析输入并将结果存储在变量中。fmt.Scanf 对于简单的格式化输入非常方便,可以针对特定的格式进行快速、类型安全的解析。它适用于对输入格式要求较为严格的情况,比如需要按照特定格式输入的数字、字符串等。

fmt.Scanf用法

golang官方文档对fmt包的详细说明 在 Go 语言官方文档中,

fmt.Scanf 函数的具体描述如下:

func Scanf(format string, a ...interface{}) (n int, err error)
  1. Scanf 根据 format 参数指定的格式字符串从标准输入中读取输入,并根据需要将解析的结果存储到传递给函数的参数中。

  2. 格式字符串 format 包含普通字符(非 '%')和转义序列('%'加上一个特定的字母)。参与解析的参数必须是指针,用于接收解析出的值。对于每个转义序列,Scanf 会读取输入中的连续字符,直到遇到下一个空白字符(默认情况下是空格、制表符和换行符)为止。

Scanf 返回成功解析的参数数量和可能的错误。如果解析过程中发生错误,则停止解析,并返回错误信息。

  1. 在 format 字符串中,可以使用以下格式化字符:
  • %d:以十进制解析一个有符号整数。
  • %s:解析一个字符串,直到遇到下一个空白字符为止。
  • %f:解析一个浮点数。
  • %t:解析一个布尔值(true 或 false)。
  • %v:根据参数的类型进行解析(支持大部分基本类型)。
  1. 除了这些基本的格式化字符之外,还可以使用更复杂的格式化规范,例如指定宽度、精度和填充字符。 此外,Scanf 还支持一些特殊字符,如空白字符、换行符和要求匹配的字符等。

需要注意的是,在使用 fmt.Scanf 进行输入解析时,要确保输入的数据与格式字符串一致,否则可能导致解析错误或得到意外的结果。

修改代码 使用 fmt.Scanf 函数来读取用户输入的整数,避免了使用 bufio.NewReader 和读取字符串的操作。这样可以简化代码并提高用户输入的便捷性。

  • 修改后的代码如下
package main  
  
import (  
"fmt"  
"math/rand"  
"time"  
)  
  
func main() {  
    maxNum := 100  
    // 随机种子  
    rand.Seed(time.Now().UnixNano())  
    secretNumber := rand.Intn(maxNum)  
  
    fmt.Println("Please input your guess")  
  
    var guess int  
    for {  
        _, err := fmt.Scanf("%d", &guess)  
        if err != nil {  
            fmt.Println("Invalid input. Please enter an integer value")  
            continue  
        }  
  
        fmt.Println("Your guess is", guess)  
        if guess > secretNumber {  
            fmt.Println("Your guess is bigger than the secret number. Please try again")  
        } else if guess < secretNumber {  
            fmt.Println("Your guess is smaller than the secret number. Please try again")  
        } else {  
            fmt.Println("Correct, you Legend!")  
            break  
        }  
    }  
}

(3)最终代码

如果运行上面代码可以发现:在上面的代码中,与原先代码一样,没办法简单消除回车键带来的影响,通过官方文档了解,想要消除回车符(\r)的计入,可以使用 fmt.Scanln 函数替代。fmt.Scanln 函数会读取一行输入,并以空格分隔输入的各项内容。

  • -以下为最终代码
package main  
  
import (  
"fmt"  
"math/rand"  
"time"  
)  
  
func main() {  
    maxNum := 100  
    // 随机种子  
    rand.Seed(time.Now().UnixNano())  
    secretNumber := rand.Intn(maxNum)  
  
    fmt.Println("Please input your guess")  
  
    var guess int  
    for {  
        _, err := fmt.Scanln(&guess)  
        if err != nil {  
            fmt.Println("Invalid input. Please enter an integer value")  
            continue  
        }  
  
        fmt.Println("Your guess is", guess)  
        if guess > secretNumber {  
            fmt.Println("Your guess is bigger than the secret number. Please try again")  
        } else if guess < secretNumber {  
            fmt.Println("Your guess is smaller than the secret number. Please try again")  
        } else {  
            fmt.Println("Correct, you Legend!")  
            break  
        }  
    }  
}

2.修改命令行字典的最终代码,加入另一种翻译引擎的支持

(1)课堂代码(节选)

image.png

image.png

image.png

(2)实现过程(百度在线翻译为例)

  1. 导入需要的包:
import "github.com/buger/jsonparser"
  1. 在 query 函数中,创建一个新的 HTTP 请求并发送到百度翻译 API。替换以下代码块:
// 创建请求
req, err := http.NewRequest("POST", "https://api.interpreter.caiyunai.com/v1/dict", data)
if err != nil {
	log.Fatal(err)
}

为:

// 创建请求
req, err := http.NewRequest("GET", "https://fanyi.baidu.com/v2transapi", nil)
if err != nil {
	log.Fatal(err)
}

// 添加查询参数
q := req.URL.Query()
q.Add("from", "en")
q.Add("to", "zh")
q.Add("query", word)
req.URL.RawQuery = q.Encode()
  1. 发送 HTTP 请求并获取响应。替换以下代码块:
resp, err := client.Do(req)
if err != nil {
	log.Fatal(err)
}
defer resp.Body.Close()
bodyText, err := ioutil.ReadAll(resp.Body)
if err != nil {
	log.Fatal(err)
}

为:

// 发送请求
resp, err := client.Do(req)
if err != nil {
	log.Fatal(err)
}
defer resp.Body.Close()

// 读取响应内容
bodyText, err := ioutil.ReadAll(resp.Body)
if err != nil {
	log.Fatal(err)
}

// 解析响应JSON
result, _, _, err := jsonparser.Get(bodyText, "trans_result", "[0]", "dst")
if err != nil {
	log.Fatal(err)
}
translation := string(result)
  1. 将翻译结果打印出来。在 query 函数的最后添加以下代码:
fmt.Println("Translation:", translation)

(3)最终代码:

package main

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"os"
	"strings"

	"github.com/buger/jsonparser"
)

type DictRequest struct {
	TransType string `json:"trans_type"`
	Source    string `json:"source"`
	UserID    string `json:"user_id"`
}

/*中间代码省略*/
func query(word string) {
	client := &http.Client{}
	request := DictRequest{TransType: "en2zh", Source: word}
	buf, err := json.Marshal(request)
	if err != nil {
		log.Fatal(err)
	}
	var data = strings.NewReader(string(buf))
	req, err := http.NewRequest("GET", "https://fanyi.baidu.com/v2transapi", nil)
	if err != nil {
		log.Fatal(err)
	}
	q := req.URL.Query()
	q.Add("from", "en")
	q.Add("to", "zh")
	q.Add("query", word)
	req.URL.RawQuery = q.Encode()

	resp, err := client.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()

	bodyText, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}

	result, _, _, err := jsonparser.Get(bodyText, "trans_result", "[0]", "dst")
	if err != nil {
		log.Fatal(err)
	}
	translation := string(result)

	fmt.Println("Translation:", translation)
}

func main() {
	if len(os.Args) != 2 {
		fmt.Fprintf(os.Stderr, `usage: simpleDict WORD
example: simpleDict hello
		`)
		os.Exit(1)
	}
	word := os.Args[1]
	query(word)
}

在上述步骤的基础上,并行请求两个翻译引擎来提高响应速度

(1)Go语言的goroutinechannel机制

通过上面的百度翻译引擎的实现过程,不难发现需要修改的代码段集中在query 函数之中,但是即使设置两个函数分别对应彩云和百度,输出结果是串行而非并行。那么如何实现并行呢?通过搜索,发现了GO之中的goroutinechannel机制。

goroutinechannel机制的官方文档

在Go语言的官方文档中,goroutinechannel 是并发编程的两个重要概念。它们提供了一种有效的方式来实现并发编程,充分利用多核处理能力,并确保数据的安全传递。下面是对它们的简要描述:

  1. Goroutine(协程):
  • Goroutine 是 Go 语言提供的一种轻量级的并发执行单位。
  • 使用关键字 go 来启动一个 Goroutine,可以将一个函数调用或函数字面量(匿名函数)放在 go 后面即可。
  • Goroutine 在逻辑上类似于线程,但可以更轻松地创建和管理成千上万个 Goroutine。
  • Goroutine 是由 Go 运行时系统调度的,它负责在多个逻辑处理器上进行任务的并发执行。
  • Goroutine 之间通过通信来共享数据。
  1. Channel(通道):
  • Channel 是用来实现 Goroutine 之间通信和同步的机制。
  • Channel 类似于管道,可以在 Goroutine 之间传递数据。
  • 使用 make 函数创建一个 Channel,并使用 <- 操作符进行发送和接收数据。
  • Channel 可以是无缓冲的(阻塞型),也可以是有缓冲的(非阻塞型)。
  • 无缓冲的 Channel 要求发送和接收操作同时准备好,而有缓冲的 Channel 则允许缓冲一定量的元素。

Goroutine 和 Channel 的用法和简单示例:

  1. Goroutine 的用法:
  • 使用关键字 go 启动一个 Goroutine,将函数调用或函数字面量(匿名函数)放在 go 后面即可。
  • Goroutine 在函数返回时自动结束,也可以使用 runtime.Goexit() 提前结束 Goroutine。
  • Goroutine 可以同时执行成千上万个,不会消耗大量的内存。

简单示例:

package main

import (
	"fmt"
	"time"
)

func hello() {
	fmt.Println("Hello Goroutine!")
}

func main() {
	go hello() // 启动一个 Goroutine
	time.Sleep(1 * time.Second) // 等待1秒,确保 Goroutine 执行完
	fmt.Println("Main goroutine exit")
}
  1. Channel 的用法:
  • 使用 make() 函数创建一个 Channel,指定发送和接收数据的类型。
  • 使用 <- 操作符来发送和接收数据。
  • 默认情况下,Channel 是阻塞的,意味着发送和接收操作会阻塞当前 Goroutine 直到对应的操作准备好。
  • 可以通过设置缓冲区来创建非阻塞的 Channel。

简单示例:

package main

import "fmt"

func numbers(ch chan int) {
	for i := 1; i <= 5; i++ {
		ch <- i // 发送数据到通道
	}
	close(ch) // 关闭通道,表示发送数据完成
}

func main() {
	ch := make(chan int) // 创建一个整数类型的无缓冲通道

	go numbers(ch) // 启动一个 Goroutine 来发送数据到通道

	for num := range ch {
		fmt.Println(num) // 从通道接收数据
	}
	fmt.Println("Main goroutine exit")
}

这个示例中,我们创建了一个无缓冲通道 ch,使用 go numbers(ch) 启动一个 Goroutine 来发送数据到通道。主函数中通过 for range 循环从通道接收数据,直到通道被关闭。

(2)最终代码

因此对原先代码,我们可以使用goroutine来并行发送两个翻译引擎的请求,并将翻译结果发送到一个通道中。然后,我们使用一个循环来等待并接收所有翻译结果,并进行打印处理。

package main

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"os"
	"strings"

	"github.com/buger/jsonparser"
)

type DictRequest struct {
	TransType string `json:"trans_type"`
	Source    string `json:"source"`
	UserID    string `json:"user_id"`
}

/*中间部分省略*/

func query(word string, translationChan chan string) {
	client := &http.Client{}
	// 请求百度翻译引擎
	baiduReq, err := http.NewRequest("GET", "https://fanyi.baidu.com/v2transapi", nil)
	if err != nil {
		log.Fatal(err)
	}
	baiduQuery := baiduReq.URL.Query()
	baiduQuery.Add("from", "en")
	baiduQuery.Add("to", "zh")
	baiduQuery.Add("query", word)
	baiduReq.URL.RawQuery = baiduQuery.Encode()

	go func() {
		resp, err := client.Do(baiduReq)
		if err != nil {
			log.Fatal(err)
		}
		defer resp.Body.Close()

		bodyText, err := ioutil.ReadAll(resp.Body)
		if err != nil {
			log.Fatal(err)
		}

		result, _, _, err := jsonparser.Get(bodyText, "trans_result", "[0]", "dst")
		if err != nil {
			log.Fatal(err)
		}
		translation := string(result)

		// 发送翻译结果到通道
		translationChan <- translation
	}()

	// 请求其他(彩云)翻译引擎,这里省略具体请求的代码

	// 等待所有并发请求完成
	// 这里只请求了两个翻译引擎,如果有多个引擎,可以根据实际情况调整
	for i := 0; i < 2; i++ {
		select {
		case translation := <-translationChan:
			fmt.Println("Translation:", translation)
		}
	}
}

func main() {
	if len(os.Args) != 2 {
		fmt.Fprintf(os.Stderr, `usage: simpleDict WORD
example: simpleDict hello
		`)
		os.Exit(1)
	}
	word := os.Args[1]

	// 创建翻译结果通道
	translationChan := make(chan string)

	// 启动查询
	query(word, translationChan)
	close(translationChan)
}

总结

bufiofmt.Scanffmt.Scanln

当处理用户输入或从标准输入读取数据时,Go语言中的bufio、fmt.Scanf和fmt.Scanln都是常用的工具。下面是对它们的总结性描述:

  1. bufio包:
  • bufio是Go语言标准库中的一个包,提供了高效的I/O缓冲机制。
  • 使用bufio可以提高读取和写入性能,减少I/O操作次数。
  • bufio主要提供了三种类型的缓冲器:ScannerWriterReader
  • Scanner用于从输入源读取数据,并将其拆分为指定的分隔符。
  • Writer用于将数据写入输出源。
  • Reader用于从输入源读取数据。
  1. fmt.Scanf函数:
  • fmt.Scanf是Go语言fmt包中的一个函数,用于从标准输入中读取数据并根据格式字符串进行解析。
  • 它通过格式化的方式来读取输入,可以按照指定的格式从用户输入中提取数据。
  • 格式字符串中使用占位符来指定所需数据类型和顺序,并使用空格或其他分隔符分隔各个值。
  1. fmt.Scanln函数:
  • fmt.Scanln是Go语言fmt包中的一个函数,用于从标准输入中读取数据并按照空格分隔存储到提供的变量中。
  • 它会一直等待用户输入,并在用户按下回车键后将数据存储到提供的变量中。
  • 它适用于需要从标准输入中读取多个值并将它们以空格分隔的情况。

这些工具在处理用户输入或从标准输入读取数据时非常有用。使用bufio可以在读写大量数据时提高性能,而fmt.Scanf和fmt.Scanln则提供了根据特定格式读取和解析输入的便捷方法。

GO语言中的串行并行

在Go语言中,串行和并行是两种处理任务的方式。

  1. 串行处理:
  • 串行处理是指按照顺序逐个执行任务或操作。
  • 在串行处理中,每个任务必须等待前一个任务完成后才能开始执行。
  • 串行处理适合于单核或单线程环境,任务之间可能会有依赖关系。
  1. 并行处理:
  • 并行处理是指同时执行多个任务或操作。
  • 在并行处理中,多个任务可以并发执行,不需要等待其他任务完成。
  • 并行处理可以充分利用多核处理器的能力,提高程序的执行效率。
  • 并行处理适合于需要同时处理大量独立任务的场景。

在Go语言中,可以使用goroutinechannel实现并行处理:

  • Goroutine是Go语言中轻量级的执行单元,可以同时执行成千上万个。
  • 使用关键字"go"可以启动一个goroutine,在其后放置函数调用或函数字面量(匿名函数)即可。
  • Goroutine在函数返回时自动结束,也可以使用runtime.Goexit()提前结束goroutine。
  • 使用channel来进行goroutine之间的通信和同步。
  • Channel是一种类型安全的通信机制,可以在goroutine之间传递数据。