• 《Go 简易速速上手小册》第8章:网络编程(2024 最新版)


    在这里插入图片描述

    8.1 HTTP 客户端与服务端编程 - Go 语言的网络灯塔与探航船

    8.1.1 基础知识讲解

    在Go语言的广阔海域中,HTTP客户端与服务端编程是连接世界的桥梁。Go通过其标准库提供了强大而灵活的工具,使得构建HTTP服务和发起HTTP请求变得简单直接。

    服务端编程

    Go的http包提供了构建HTTP服务的必要工具。通过定义处理函数并将其注册到路由上,Go应用可以响应各种HTTP请求:

    http.HandleFunc("/greeting", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprint(w, "Hello, Go Navigator!")
    })
    http.ListenAndServe(":8080", nil)
    
    • 1
    • 2
    • 3
    • 4

    这段代码启动了一个HTTP服务,监听8080端口,并对/greeting路径的请求返回问候信息。

    客户端编程

    同样地,Go的http包也支持发起HTTP请求。这允许Go应用作为客户端,与其他HTTP服务进行交互:

    resp, err := http.Get("https://api.example.com/data")
    if err != nil {
        log.Fatalf("Cannot retrieve data: %v", err)
    }
    defer resp.Body.Close()
    // 解析响应体...
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    这段代码向https://api.example.com/data发起GET请求,并处理返回的响应。

    8.1.2 重点案例:简易博客服务

    在这个扩展案例中,我们将构建一个简易的博客服务,该服务将支持文章的创建和列出所有文章的功能。此外,我们将提供一个简单的客户端示例,展示如何与这个服务进行交云。

    服务端实现

    我们的服务端将提供两个HTTP端点:一个用于接收新文章的提交(POST请求),另一个用于获取所有文章的列表(GET请求)。

    // blogserver/main.go
    
    package main
    
    import (
        "encoding/json"
        "fmt"
        "log"
        "net/http"
        "sync"
    )
    
    type Article struct {
        ID      int    `json:"id"`
        Title   string `json:"title"`
        Content string `json:"content"`
    }
    
    var (
        articles []Article
        mu       sync.Mutex
        nextID   = 1
    )
    
    func postArticleHandler(w http.ResponseWriter, r *http.Request) {
        var article Article
        if err := json.NewDecoder(r.Body).Decode(&article); err != nil {
            http.Error(w, "Invalid request body", http.StatusBadRequest)
            return
        }
    
        mu.Lock()
        article.ID = nextID
        nextID++
        articles = append(articles, article)
        mu.Unlock()
    
        w.WriteHeader(http.StatusCreated)
        json.NewEncoder(w).Encode(article)
    }
    
    func listArticlesHandler(w http.ResponseWriter, r *http.Request) {
        mu.Lock()
        defer mu.Unlock()
        json.NewEncoder(w).Encode(articles)
    }
    
    func main() {
        http.HandleFunc("/articles", func(w http.ResponseWriter, r *http.Request) {
            switch r.Method {
            case "GET":
                listArticlesHandler(w, r)
            case "POST":
                postArticleHandler(w, r)
            default:
                w.WriteHeader(http.StatusMethodNotAllowed)
            }
        })
    
        fmt.Println("Blog server started on :8080")
        log.Fatal(http.ListenAndServe(":8080", nil))
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62

    客户端实现

    客户端代码将展示如何发起请求以创建新文章,以及如何获取文章列表。

    // blogclient/main.go
    
    package main
    
    import (
        "bytes"
        "encoding/json"
        "fmt"
        "io/ioutil"
        "net/http"
    )
    
    func createArticle(title, content string) {
        article := map[string]string{
            "title":   title,
            "content": content,
        }
        articleJSON, _ := json.Marshal(article)
    
        resp, err := http.Post("http://localhost:8080/articles", "application/json", bytes.NewBuffer(articleJSON))
        if err != nil {
            fmt.Println("Error creating article:", err)
            return
        }
        defer resp.Body.Close()
    
        body, _ := ioutil.ReadAll(resp.Body)
        fmt.Println("Create Article Response:", string(body))
    }
    
    func getArticles() {
        resp, err := http.Get("http://localhost:8080/articles")
        if err != nil {
            fmt.Println("Error fetching articles:", err)
            return
        }
        defer resp.Body.Close()
    
        body, _ := ioutil.ReadAll(resp.Body)
        fmt.Println("Articles List:", string(body))
    }
    
    func main() {
        createArticle("Go Concurrency", "Understanding Goroutines and Channels")
        createArticle("Go Web Programming", "Building Web Apps with Go")
    
        getArticles()
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48

    这个简易的客户端示例首先创建两篇文章,然后获取并打印所有文章的列表。

    运行示例

    1. 启动服务端:在blogserver目录下运行go run main.go,启动博客服务。
    2. 运行客户端:在blogclient目录下运行go run main.go,通过客户端与服务端交互。

    通过本案例,我们探索了如何使用Go的http包来构建一个简单的HTTP服务端和客户端。这个简易博客服务的实现展示了Go在网络编程领域的强大能力,提供了对HTTP请求的处理和响应的清晰示例。继续利用Go构建更多网络应用,探索更广阔的编程海洋吧!

    8.1.3 拓展案例 1:增加文章评论功能

    在我们的简易博客服务中,增加文章评论功能将使读者能够对文章进行互动。这要求我们在服务端添加新的逻辑来接收、存储和展示评论。

    功能描述

    1. 接收评论:允许用户对特定文章添加评论。
    2. 存储评论:将评论与对应的文章关联存储。
    3. 展示评论:在请求文章详情时,一并返回其评论。

    服务端实现

    首先,我们需要在服务端扩展我们的数据模型和HTTP处理函数,以支持评论功能:

    // blogserver/main.go
    
    package main
    
    import (
        "encoding/json"
        "fmt"
        "log"
        "net/http"
        "sync"
    )
    
    type Article struct {
        ID      int       `json:"id"`
        Title   string    `json:"title"`
        Content string    `json:"content"`
        Comments []string `json:"comments,omitempty"`
    }
    
    var (
        articles []Article
        mu       sync.Mutex
        nextID   = 1
    )
    
    // 新增添加评论的处理函数
    func addCommentHandler(w http.ResponseWriter, r *http.Request) {
        articleID := r.URL.Query().Get("id")
        var comment string
        if err := json.NewDecoder(r.Body).Decode(&comment); err != nil {
            http.Error(w, "Invalid request body", http.StatusBadRequest)
            return
        }
    
        mu.Lock()
        defer mu.Unlock()
        for i, article := range articles {
            if article.ID == articleID {
                articles[i].Comments = append(articles[i].Comments, comment)
                json.NewEncoder(w).Encode(article)
                return
            }
        }
    
        http.Error(w, "Article not found", http.StatusNotFound)
    }
    
    func main() {
        http.HandleFunc("/articles", articlesHandler) // Assume articlesHandler handles both GET for listing and POST for creating articles.
        http.HandleFunc("/add_comment", addCommentHandler)
    
        fmt.Println("Blog server started on :8080")
        log.Fatal(http.ListenAndServe(":8080", nil))
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54

    客户端实现

    对于客户端,我们需要提供一个新的函数来发起添加评论的请求:

    // blogclient/main.go
    
    package main
    
    import (
        "bytes"
        "encoding/json"
        "fmt"
        "io/ioutil"
        "net/http"
        "strconv"
    )
    
    func addComment(articleID int, comment string) {
        commentJSON, _ := json.Marshal(comment)
    
        resp, err := http.Post("http://localhost:8080/add_comment?id="+strconv.Itoa(articleID), "application/json", bytes.NewBuffer(commentJSON))
        if err != nil {
            fmt.Println("Error adding comment:", err)
            return
        }
        defer resp.Body.Close()
    
        body, _ := ioutil.ReadAll(resp.Body)
        fmt.Println("Add Comment Response:", string(body))
    }
    
    func main() {
        // 示例:向ID为1的文章添加评论
        addComment(1, "Great article!")
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    通过这个扩展案例,我们为简易博客服务增加了文章评论功能,使得用户可以对文章进行评论,并在请求文章时查看到这些评论。这个案例展示了如何在现有的Go HTTP服务中添加新的功能,以及如何处理更复杂的数据关联和更新逻辑。继续探索Go在网络编程方面的能力,构建更加丰富和互动的应用吧!

    8.1.4 拓展案例 2:实现文章的搜索功能

    在我们的简易博客服务中,实现文章的搜索功能将极大地提升用户体验,允许用户根据关键词快速找到他们感兴趣的文章。这要求我们在服务端添加一个新的端点来处理搜索请求,并在文章数据中进行匹配。

    功能描述

    1. 搜索文章:根据用户输入的关键词返回匹配的文章列表。
    2. 关键词匹配:在文章的标题和内容中查找包含关键词的文章。
    3. 返回结果:返回匹配的文章列表给用户。

    服务端实现

    首先,我们需要在服务端实现搜索功能的逻辑:

    // blogserver/main.go
    
    package main
    
    import (
        "encoding/json"
        "fmt"
        "log"
        "net/http"
        "strings"
        "sync"
    )
    
    type Article struct {
        ID      int    `json:"id"`
        Title   string `json:"title"`
        Content string `json:"content"`
    }
    
    var (
        articles []Article
        mu       sync.Mutex
    )
    
    // 实现搜索文章的处理函数
    func searchArticlesHandler(w http.ResponseWriter, r *http.Request) {
        query := r.URL.Query().Get("q")
        if query == "" {
            http.Error(w, "Query parameter 'q' is required", http.StatusBadRequest)
            return
        }
    
        mu.Lock()
        defer mu.Unlock()
        var matchedArticles []Article
        for _, article := range articles {
            if strings.Contains(article.Title, query) || strings.Contains(article.Content, query) {
                matchedArticles = append(matchedArticles, article)
            }
        }
    
        json.NewEncoder(w).Encode(matchedArticles)
    }
    
    func main() {
        http.HandleFunc("/search", searchArticlesHandler)
    
        fmt.Println("Blog server started on :8080")
        log.Fatal(http.ListenAndServe(":8080", nil))
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50

    在上述代码中,我们为博客服务添加了/search端点,用户可以通过这个端点传递一个查询参数q来搜索文章。搜索逻辑简单地检查文章标题和内容中是否包含了查询关键词,然后返回匹配的文章列表。

    客户端示例

    客户端可以通过发起HTTP GET请求到/search端点并传递查询参数来实现文章搜索:

    // 示例:在客户端代码中搜索文章
    
    package main
    
    import (
        "fmt"
        "io/ioutil"
        "net/http"
        "net/url"
    )
    
    func searchArticles(query string) {
        baseURL := "http://localhost:8080/search"
        queryParams := url.Values{}
        queryParams.Set("q", query)
        searchURL := baseURL + "?" + queryParams.Encode()
    
        resp, err := http.Get(searchURL)
        if err != nil {
            fmt.Println("Error searching articles:", err)
            return
        }
        defer resp.Body.Close()
    
        body, err := ioutil.ReadAll(resp.Body)
        if err != nil {
            fmt.Println("Error reading response body:", err)
            return
        }
    
        fmt.Printf("Search Results for '%s':\n%s\n", query, string(body))
    }
    
    func main() {
        searchArticles("Go") // 搜索包含"Go"的文章
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    通过这个扩展案例,我们为简易博客服务增加了搜索功能,允许用户根据关键词搜索文章。这个案例演示了如何在Go HTTP服务中处理查询参数,并根据这些参数执行简单的搜索逻辑。随着你的Go应用变得更加复杂,考虑引入更高级的搜索技术,如全文搜索引擎,以提供更快速、更精确的搜索能力。继续探索Go在网络编程方面的强大功能,开发出更加丰富和高效的网络应用。

    8.2 使用 Goroutines 处理并发请求 - Go 语言的并发船队

    在Go语言的并发海洋中,goroutines是轻巧且强大的帆船,能够高效地在处理器的海浪上航行。通过goroutines,Go应用可以同时处理数百、数千乃至更多的任务,而不会因为阻塞操作而停滞不前。

    8.2.1 基础知识讲解

    Goroutines 的启动

    goroutine是Go中实现并发的基础。启动一个goroutine就像是给帆船一阵风,让它启航:

    go func() {
        // 并发执行的代码
    }()
    
    • 1
    • 2
    • 3

    只需在函数调用前加上go关键字,该函数就会在新的goroutine中异步执行,主程序会继续向下执行而不会等待。

    并发的 HTTP 请求处理

    在HTTP服务中使用goroutines可以让我们并发地处理多个请求,大大提高了服务的吞吐量。每当收到一个新的请求,我们就可以为其分配一个goroutine,这样即使某些请求的处理时间较长,也不会阻塞其他请求的处理。

    8.2.2 重点案例:并发下载服务

    在这个案例中,我们将构建一个并发下载服务,该服务通过goroutines允许用户同时下载多个文件。这种并发处理模式显著提高了应用的效率,特别是在处理多个网络IO密集型任务时。

    服务端实现

    我们的服务端提供一个HTTP接口,接受一个包含多个文件URLs的请求,并为每个下载任务启动一个goroutine

    // downloadservice/main.go
    
    package main
    
    import (
        "fmt"
        "io"
        "net/http"
        "os"
        "sync"
    )
    
    // downloadFile函数接收文件URL和目标文件名,完成文件下载
    func downloadFile(url, filename string, wg *sync.WaitGroup) {
        defer wg.Done()
    
        resp, err := http.Get(url)
        if err != nil {
            fmt.Printf("Failed to download %s: %v\n", url, err)
            return
        }
        defer resp.Body.Close()
    
        out, err := os.Create(filename)
        if err != nil {
            fmt.Printf("Failed to create file %s: %v\n", filename, err)
            return
        }
        defer out.Close()
    
        _, err = io.Copy(out, resp.Body)
        if err != nil {
            fmt.Printf("Failed to write to file %s: %v\n", filename, err)
            return
        }
    
        fmt.Printf("Successfully downloaded %s to %s\n", url, filename)
    }
    
    // downloadHandler处理下载请求,解析请求中的URLs并启动goroutines进行下载
    func downloadHandler(w http.ResponseWriter, r *http.Request) {
        urls := r.URL.Query()["url"]
        if len(urls) == 0 {
            http.Error(w, "No URLs provided", http.StatusBadRequest)
            return
        }
    
        var wg sync.WaitGroup
    
        for i, url := range urls {
            wg.Add(1)
            go downloadFile(url, fmt.Sprintf("download%d", i+1), &wg)
        }
    
        wg.Wait()
        fmt.Fprintln(w, "All download tasks completed.")
    }
    
    func main() {
        http.HandleFunc("/download", downloadHandler)
        fmt.Println("Download service started on :8080")
        http.ListenAndServe(":8080", nil)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63

    在这个实现中,我们定义了downloadFile函数来处理单个文件的下载逻辑。downloadHandler函数负责解析HTTP请求中包含的多个下载URL,为每个URL启动一个goroutine执行downloadFile函数,并使用sync.WaitGroup等待所有下载任务完成。

    测试服务

    为了测试这个并发下载服务,你可以使用以下方式发起一个包含多个下载URLs的请求:

    1. 使用curl工具:
    curl "http://localhost:8080/download?url=http://example.com/file1.jpg&url=http://example.com/file2.jpg"
    
    • 1
    1. 或者,使用Postman等API测试工具发起GET请求,并在请求的URL参数中添加多个url参数。

    通过这个案例,我们演示了如何在Go中利用goroutinessync.WaitGroup构建一个支持并发处理的下载服务。这种模式不仅适用于文件下载,还可以应用于其他需要并发执行多个任务的场景,如批量数据处理、并发API请求等。随着你继续探索Go语言的并发特性,你将能够构建出更加强大和高效的应用。

    8.2.3 拓展案例 1:并发图片处理服务

    在这个案例中,我们将构建一个并发图片处理服务,该服务允许用户上传图片,并且并行执行多种图片处理任务,如调整大小、应用滤镜等。利用goroutines实现并发处理,可以显著提高服务的处理效率。

    功能描述

    1. 图片上传:允许用户上传图片到服务。
    2. 并发图片处理:对上传的图片并发执行多种处理任务。
    3. 返回处理结果:将处理后的图片保存并提供下载链接或直接返回给用户。

    服务端实现

    为了简化示例,我们假设图片已经上传到服务器上的某个目录,服务端的任务是读取这些图片并并发地进行处理。

    首先,我们需要一个库来帮助我们处理图片,这里我们使用github.com/disintegration/imaging库:

    go get -u github.com/disintegration/imaging
    
    • 1

    接着,实现图片处理服务的核心逻辑:

    // imageprocessingservice/main.go
    
    package main
    
    import (
        "fmt"
        "github.com/disintegration/imaging"
        "net/http"
        "os"
        "path/filepath"
        "sync"
    )
    
    func processImage(filePath string, wg *sync.WaitGroup) {
        defer wg.Done()
    
        srcImage, err := imaging.Open(filePath)
        if err != nil {
            fmt.Printf("Failed to open image %s: %v\n", filePath, err)
            return
        }
    
        // 示例:调整图片大小为200x200,并应用高斯模糊滤镜
        dstImage := imaging.Resize(srcImage, 200, 200, imaging.Lanczos)
        dstImage = imaging.Blur(dstImage, 2.0)
    
        outputPath := filepath.Join("processed", filepath.Base(filePath))
        err = imaging.Save(dstImage, outputPath)
        if err != nil {
            fmt.Printf("Failed to save processed image %s: %v\n", outputPath, err)
            return
        }
    
        fmt.Printf("Processed and saved image to %s\n", outputPath)
    }
    
    func imageHandler(w http.ResponseWriter, r *http.Request) {
        var wg sync.WaitGroup
    
        // 假设图片存储在"./uploads"目录
        files, err := filepath.Glob("./uploads/*")
        if err != nil {
            http.Error(w, "Failed to list images", http.StatusInternalServerError)
            return
        }
    
        for _, file := range files {
            wg.Add(1)
            go processImage(file, &wg)
        }
    
        wg.Wait()
        fmt.Fprintln(w, "All images processed.")
    }
    
    func main() {
        http.HandleFunc("/process-images", imageHandler)
        fmt.Println("Image processing service started on :8080")
        http.ListenAndServe(":8080", nil)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60

    在上述代码中,processImage函数负责打开一个图片文件,执行一系列处理任务(在这个示例中是调整大小和应用模糊滤镜),然后保存处理后的图片到processed目录。imageHandler函数并发地对uploads目录中的所有图片调用processImage函数。

    测试服务

    启动服务后,你可以通过向/process-images端点发送HTTP请求来触发图片处理过程。这可以通过浏览器访问,或使用工具如curl进行:

    curl http://localhost:8080/process-images
    
    • 1

    这个并发图片处理服务展示了如何利用goroutines提高处理效率,通过并行处理来快速完成大量的图片处理任务。你可以根据具体需求添加更多的图片处理功能,如调整亮度、对比度,或者应用更复杂的图像分析算法。继续探索Go语言的并发特性,为你的应用带来更强大的处理能力。

    8.2.4 拓展案例 2:实时股票行情服务

    在这个案例中,我们将构建一个实时股票行情服务,该服务使用goroutines并发查询多个股票代码的实时价格,并将查询结果合并后返回给客户端。这种并发处理方式能够显著提高数据获取的速度,特别适合需要实时响应的金融应用。

    功能描述

    1. 并发查询股票价格:对于客户端提供的股票代码列表,服务端并发查询每个股票的实时价格。
    2. 合并查询结果:将所有查询结果合并后,一次性返回给客户端。
    3. 错误处理:对于查询失败的股票代码,返回错误信息。
    服务端实现

    为了模拟股票价格查询,我们假设有一个简单的函数fetchStockPrice模拟从外部API获取股票价格。实际应用中,你需要替换为真实的股票价格API调用。

    // stockservice/main.go
    
    package main
    
    import (
        "encoding/json"
        "fmt"
        "math/rand"
        "net/http"
        "sync"
        "time"
    )
    
    type StockPrice struct {
        Symbol string  `json:"symbol"`
        Price  float64 `json:"price"`
        Error  string  `json:"error,omitempty"`
    }
    
    func fetchStockPrice(symbol string) StockPrice {
        // 模拟网络延迟
        time.Sleep(time.Duration(rand.Intn(300)) * time.Millisecond)
    
        // 模拟偶尔的查询失败
        if rand.Intn(10) > 7 {
            return StockPrice{Symbol: symbol, Error: "Failed to fetch price"}
        }
    
        // 模拟股票价格
        return StockPrice{Symbol: symbol, Price: rand.Float64() * 100}
    }
    
    func stockHandler(w http.ResponseWriter, r *http.Request) {
        symbols := r.URL.Query()["symbol"]
        var wg sync.WaitGroup
        results := make([]StockPrice, len(symbols))
    
        for i, symbol := range symbols {
            wg.Add(1)
            go func(i int, symbol string) {
                defer wg.Done()
                results[i] = fetchStockPrice(symbol)
            }(i, symbol)
        }
    
        wg.Wait()
        json.NewEncoder(w).Encode(results)
    }
    
    func main() {
        rand.Seed(time.Now().UnixNano())
        http.HandleFunc("/stocks", stockHandler)
        fmt.Println("Stock service started on :8080")
        http.ListenAndServe(":8080", nil)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55

    在这段代码中,stockHandler函数接收一个包含股票代码的查询参数,为每个股票代码启动一个goroutine执行fetchStockPrice函数,并发地获取股票价格。使用sync.WaitGroup等待所有的查询任务完成,然后将结果合并后以JSON格式返回给客户端。

    测试服务

    你可以通过向/stocks端点发送HTTP GET请求并附带股票代码作为查询参数来测试服务:

    curl "http://localhost:8080/stocks?symbol=AAPL&symbol=GOOGL&symbol=MSFT"
    
    • 1

    这个简单的实时股票行情服务展示了如何利用Go的并发特性来提高数据处理的速度。通过goroutines并发执行任务和sync.WaitGroup来同步任务结果,我们可以构建出响应迅速、性能卓越的实时服务。在实际应用中,你可以将此模式应用于任何需要并发数据获取和处理的场景,充分发挥Go在并发编程方面的优势。

    8.3 WebSocket 与 RPC - Go 语言的深海通信线

    在Go语言的并发海洋中,WebSocket和RPC (Remote Procedure Call) 是两种深海通信线,允许我们跨越深渊,进行实时和跨服务的通信。

    8.3.1 基础知识讲解

    WebSocket

    WebSocket提供了一个全双工通信渠道,允许客户端和服务器之间建立持久连接并实时交换数据。这对于需要实时功能的应用来说非常有用,比如在线聊天室、实时数据仪表板等。

    在Go中,gorilla/websocket是一个流行的库,用于在HTTP服务器上添加WebSocket支持。

    RPC

    RPC允许客户端执行远程服务器上的函数就像是执行本地函数一样,隐藏了网络请求的复杂性。Go标准库中的net/rpc包提供了构建RPC系统的基础。

    8.3.2 重点案例:实时聊天应用

    在这个扩展案例中,我们将构建一个简单的实时聊天应用,使用WebSocket实现客户端和服务器之间的实时通信。这个应用将允许用户通过Web浏览器连接到聊天服务器,发送消息并实时接收来自其他用户的消息。

    服务端实现

    我们使用gorilla/websocket库来处理WebSocket连接。首先,需要安装这个库:

    go get -u github.com/gorilla/websocket
    
    • 1

    然后,我们实现聊天服务器的核心逻辑:

    // chatserver/main.go
    
    package main
    
    import (
        "github.com/gorilla/websocket"
        "net/http"
        "log"
    )
    
    var clients = make(map[*websocket.Conn]bool) // 连接到服务器的客户端
    var broadcast = make(chan []byte)            // 广播通道
    
    var upgrader = websocket.Upgrader{
        CheckOrigin: func(r *http.Request) bool {
            return true
        },
    }
    
    func main() {
        http.HandleFunc("/ws", handleConnections)
        go handleMessages()
    
        log.Println("Chat server started on :8080")
        log.Fatal(http.ListenAndServe(":8080", nil))
    }
    
    func handleConnections(w http.ResponseWriter, r *http.Request) {
        ws, err := upgrader.Upgrade(w, r, nil)
        if err != nil {
            log.Fatal(err)
        }
        defer ws.Close()
    
        clients[ws] = true
    
        for {
            _, msg, err := ws.ReadMessage()
            if err != nil {
                log.Printf("Error: %v", err)
                delete(clients, ws)
                break
            }
            broadcast <- msg
        }
    }
    
    func handleMessages() {
        for {
            msg := <-broadcast
            for client := range clients {
                err := client.WriteMessage(websocket.TextMessage, msg)
                if err != nil {
                    log.Printf("Error: %v", err)
                    client.Close()
                    delete(clients, client)
                }
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60

    在这个实现中,handleConnections函数处理新的WebSocket连接,读取来自客户端的消息,并将它们放入broadcast通道。handleMessages函数监听broadcast通道,将接收到的消息发送给所有连接的客户端。

    客户端实现

    客户端可以是一个简单的HTML页面,使用JavaScript与WebSocket服务器进行通信:

    
    
    DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Go Chat Apptitle>
        <script>
            document.addEventListener("DOMContentLoaded", function() {
                var ws = new WebSocket("ws://localhost:8080/ws");
                var messages = document.getElementById("messages");
    
                ws.onmessage = function(event) {
                    var message = document.createElement("p");
                    message.textContent = event.data;
                    messages.appendChild(message);
                };
    
                document.getElementById("sendBtn").onclick = function() {
                    var message = document.getElementById("messageInput").value;
                    ws.send(message);
                    document.getElementById("messageInput").value = "";
                };
            });
        script>
    head>
    <body>
        <div id="messages">div>
        <input id="messageInput" type="text">
        <button id="sendBtn">Sendbutton>
    body>
    html>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    这个HTML页面包含一个消息列表、一个文本输入框和一个发送按钮。当用户点击发送按钮时,当前的消息会通过WebSocket发送到服务器,并且清空输入框。当服务器通过WebSocket发送消息时,它们会被添加到页面的消息列表中。

    运行示例

    1. 启动服务端:在chatserver目录下运行go run main.go,启动聊天服务器。
    2. 打开客户端:在浏览器中打开chatclient/index.html文件,连接到聊天服务器。

    通过这个案例,我们展示了如何使用Go和WebSocket构建一个简单的实时聊天应用。这个应用能够让多个用户通过Web界面实时交换消息,体验到实时通信的魅力。随着你继续探索WebSocket和Go在网络编程方面的更多可能性,你将能够构建更加复杂和强大的实时Web应用。

    8.3.3 拓展案例 1:股票行情实时更新服务

    在这个案例中,我们将构建一个基于WebSocket的股票行情实时更新服务。该服务允许客户端通过WebSocket订阅特定的股票代码,并在股票价格发生变动时接收实时更新。

    服务端实现

    为了简化演示,我们假设股票价格的变动是随机模拟的。在实际应用中,你可能需要连接到真实的股市数据API来获取实时行情。

    首先,安装gorilla/websocket库:

    go get -u github.com/gorilla/websocket
    
    • 1

    接着,实现WebSocket服务端逻辑:

    // stockservice/main.go
    
    package main
    
    import (
        "encoding/json"
        "github.com/gorilla/websocket"
        "log"
        "math/rand"
        "net/http"
        "time"
    )
    
    var upgrader = websocket.Upgrader{
        CheckOrigin: func(r *http.Request) bool {
            return true
        },
    }
    
    type StockUpdate struct {
        Symbol string  `json:"symbol"`
        Price  float64 `json:"price"`
    }
    
    func handleConnections(w http.ResponseWriter, r *http.Request) {
        ws, err := upgrader.Upgrade(w, r, nil)
        if err != nil {
            log.Fatal(err)
        }
        defer ws.Close()
    
        // 模拟股票代码列表
        stocks := []string{"AAPL", "GOOGL", "MSFT"}
    
        for {
            // 等待客户端消息并读取订阅的股票代码
            _, msg, err := ws.ReadMessage()
            if err != nil {
                log.Printf("error: %v", err)
                break
            }
            symbol := string(msg)
    
            // 如果订阅的股票代码在模拟的列表中,开始发送更新
            for _, s := range stocks {
                if s == symbol {
                    for {
                        // 随机生成股票价格并发送
                        price := rand.Float64() * 1000
                        update := StockUpdate{Symbol: symbol, Price: price}
                        updateJSON, err := json.Marshal(update)
                        if err != nil {
                            log.Printf("error: %v", err)
                            break
                        }
    
                        if err := ws.WriteMessage(websocket.TextMessage, updateJSON); err != nil {
                            log.Printf("error: %v", err)
                            break
                        }
    
                        // 每隔一秒发送一次更新
                        time.Sleep(1 * time.Second)
                    }
                    break
                }
            }
        }
    }
    
    func main() {
        rand.Seed(time.Now().UnixNano())
        http.HandleFunc("/ws", handleConnections)
        log.Println("Stock service started on :8080")
        log.Fatal(http.ListenAndServe(":8080", nil))
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76

    客户端示例

    客户端可以是一个简单的HTML页面,通过JavaScript与WebSocket服务器建立连接,并发送订阅请求:

    
    
    DOCTYPE html>
    <html>
    <head>
        <title>Real-time Stock Updatestitle>
        <script>
            document.addEventListener("DOMContentLoaded", () => {
                const ws = new WebSocket("ws://localhost:8080/ws");
    
                ws.onopen = () => {
                    console.log("Connected to the server");
                    // 订阅AAPL股票的更新
                    ws.send("AAPL");
                };
    
                ws.onmessage = (event) => {
                    const stockUpdate = JSON.parse(event.data);
                    console.log(`Stock update for ${stockUpdate.symbol}: $${stockUpdate.price.toFixed(2)}`);
                };
    
                ws.onerror = (error) => {
                    console.log("WebSocket error: " + error.message);
                };
    
                ws.onclose = () => {
                    console.log("Disconnected from the server");
                };
            });
        script>
    head>
    <body>
        <h1>Real-time Stock Updatesh1>
        <p>Open the console to view the stock updates.p>
    body>
    html>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    在这个HTML页面中,当页面加载完成后,客户端通过WebSocket连接到服务器并发送一个订阅请求(在此例中为"AAPL"股票)。然后,它将监听服务器发送的更新,并在控制台中显示更新的股票价格。

    运行示例

    1. 启动服务端:在stockservice

    录下运行go run main.go,启动股票行情更新服务。

    1. 打开客户端:在Web浏览器中打开stockclient/index.html文件,并查看浏览器控制台以接收实时的股票价格更新。

    通过这个拓展案例,我们演示了如何使用WebSocket在Go中实现一个实时的股票行情更新服务。客户端可以订阅感兴趣的股票代码,并接收关于这些股票的实时价格更新,展现了WebSocket在构建实时通信应用中的强大能力。继续探索WebSocket和Go的其他网络编程特性,为你的应用带来更加丰富的实时交互体验。

    8.3.4 拓展案例 2:远程系统监控

    在这个案例中,我们将构建一个基于RPC的远程系统监控工具,允许管理员从中央服务器调用远程机器上的函数,以获取系统状态信息,如CPU使用率、内存使用等。这个工具将使用Go的net/rpc包来实现RPC通信。

    功能描述

    1. 远程获取系统状态:允许管理员远程调用函数,获取目标机器的系统状态信息。
    2. 支持多种状态查询:支持查询CPU使用率、内存使用情况等不同类型的系统状态。
    3. 简单的认证机制:实现一个简单的认证机制,确保只有授权的用户可以查询系统状态。

    服务端实现

    首先,我们需要定义提供的远程调用方法和服务:

    // monitorserver/main.go
    
    package main
    
    import (
        "errors"
        "net"
        "net/rpc"
        "runtime"
    )
    
    type Args struct {
        AuthToken string
    }
    
    type SystemStats struct {
        CPU    string
        Memory string
    }
    
    type Monitor int
    
    func (t *Monitor) GetSystemStats(args *Args, reply *SystemStats) error {
        if args.AuthToken != "secret" {
            return errors.New("unauthorized")
        }
    
        // 模拟获取系统状态
        reply.CPU = "2.4 GHz"
        reply.Memory = "8 GB"
        return nil
    }
    
    func main() {
        monitor := new(Monitor)
        rpc.Register(monitor)
        rpc.HandleHTTP()
    
        l, err := net.Listen("tcp", ":1234")
        if err != nil {
            panic(err)
        }
    
        for {
            conn, err := l.Accept()
            if err != nil {
                continue
            }
            go rpc.ServeConn(conn)
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51

    在这个实现中,我们定义了一个Monitor类型,它有一个方法GetSystemStats,用于远程获取系统状态。我们使用了一个简单的认证机制,通过检查传递的AuthToken来授权访问。

    客户端实现

    客户端将通过RPC调用服务端的GetSystemStats方法来获取系统状态:

    // monitorclient/main.go
    
    package main
    
    import (
        "fmt"
        "log"
        "net/rpc"
    )
    
    type Args struct {
        AuthToken string
    }
    
    type SystemStats struct {
        CPU    string
        Memory string
    }
    
    func main() {
        client, err := rpc.DialHTTP("tcp", "localhost:1234")
        if err != nil {
            log.Fatal("Dialing:", err)
        }
    
        args := &Args{"secret"}
        var reply SystemStats
    
        err = client.Call("Monitor.GetSystemStats", args, &reply)
        if err != nil {
            log.Fatal("Monitor error:", err)
        }
    
        fmt.Printf("CPU: %s\nMemory: %s\n", reply.CPU, reply.Memory)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    在这个客户端实现中,我们创建了一个RPC客户端,连接到服务端,并调用Monitor.GetSystemStats方法,传递认证令牌并获取系统状态信息。

    运行示例

    1. 启动服务端:在monitorserver目录下运行go run main.go启动系统监控服务。
    2. 运行客户端:在monitorclient目录下运行go run main.go来从服务端获取系统状态信息。

    通过这个拓展案例,我们演示了如何使用Go的net/rpc包实现一个基于RPC的远程系统监控工具。这个工具允许管理员远程获取系统状态信息,展现了RPC在构建分布式系统和服务通信中的实用性。继续探索Go的RPC和其他网络编程特性,为你的分布式应用提供强大的后端支持。

  • 相关阅读:
    MySQL常用语句(CURD)
    计算机硬件和软件之间的区别
    Fortran编程(基础语法)——笔记3
    简单的服务间通信
    MySQL笔记(九):存储引擎
    sql语句实现查询
    微信视频号视频保存方法大全,轻松存储到手机相册
    Servlet入门接口、类和配置学习
    strimzi实战之一:简介和准备
    deepin(深度)系统下qt5.12.0的程序打包发布到linux云服务器上
  • 原文地址:https://blog.csdn.net/qq_41340258/article/details/136141563