跳转到主要内容

欢迎大家! 在本教程中,我们将获得一些乐趣,我们将在 Go 中创建一个实时的 YouTube 统计监控系统。

因此,我们将在本教程中研究许多不同的主题,例如创建 WebSocket 服务器和使用 WebSockets 与前端应用程序进行实时通信,以及如何与现有的 REST API 进行交互以 获取我们需要的订阅者统计信息。

目标


在本教程结束时:

  • 您将对如何在自己的 Go 应用程序中使用 WebSockets 有更好的理解。
  • 您将了解如何与 YouTube API 交互以检索您自己的 YouTube 频道的统计信息。

先决条件

 

  • 您需要在开发机器上安装 Go 版本 1.11+。

视频教程


如果您愿意,本教程可在此处以视频格式提供:

https://youtu.be/n3BQLHtsrkM

入门


首先,我们要创建一个新目录来工作。我们将其命名为 youtube-stats/。

$ mkdir -p youtube-stats
$ cd youtube-stats/


在这个新的项目目录中,您将需要运行以下命令来使用 go modules 初始化您的项目。

$ go mod init github.com/elliotforbes/youtube-stats

在这个新目录中,我们将创建 main.go 文件,该文件将成为 Go 程序的主要入口点。

// youtube-stats/main.go
package main

import (
    "fmt"
)

func main() {
    fmt.Println("YouTube Subscriber Monitor")
}

让我们继续创建一个简单的基于 net/http 的服务器,该服务器在 http://localhost:8080 上运行。这将作为前端客户端连接到的 WebSocket 服务器的基础,以便实时获取统计信息。

// youtube-stats/main.go
package main

import (
    "fmt"
    "log"
    "net/http"
)

// homePage will be a simple "hello World" style page
func homePage(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello World")
}

// setupRoutes handles setting up our servers
// routes and matching them to their respective
// functions
func setupRoutes() {
    http.HandleFunc("/", homePage)
    // here we kick off our server on localhost:8080
    log.Fatal(http.ListenAndServe(":8080", nil))
}

// our main function
func main() {
    fmt.Println("YouTube Subscriber Monitor")
    // calls setup routes
    setupRoutes()
}

然后我们可以打开一个终端并使用 go run main.go 运行它。启动后,尝试在浏览器中导航到 http://localhost:8080,您应该希望看到 Hello World 在浏览器中打印出来!

太棒了,所以我们现在有了一个可以建立的坚实基础!

YouTube API


能够与 YouTube API 交互将成为这个迷你项目的关键部分。为了连接到这个 API,我们首先需要在 Google Developers Console 中创建一个项目。

注意 - 如果您之前没有使用过 API,您可能需要启用 YouTube V3 Data API

一旦我们创建了一个项目,我们就可以在开发人员仪表板的凭据部分中创建一个新的 API 密钥。

API端点


我们将要与之交互的 API 是可以在此处找到的频道/列表 API - https://developers.google.com/youtube/v3/docs/channels/list#try-it。这应该返回我们频道的所有统计信息以及描述和其他一些信息。

使用您的 API 密钥,我们可以构造一个对这个 API 端点的请求,并通过一个简单的 curl 命令来测试是否一切正常。将此命令的 API-KEY 部分替换为您自己的 API 密钥,然后尝试在终端中运行此命令。

curl -i -G -d "id=UCwFl9Y49sWChrddQTD9QhRA&part=snippet%2CcontentDetails%2Cstatistics&key=API-KEY" https://www.googleapis.com/youtube/v3/channels


如果一切都按预期工作,我们应该会在终端中看到一个相当大的 JSON 对象,其中包含我们需要的一切!

提示 - curl 是测试 API 端点的绝佳工具,老实说,我真希望在我刚开始我的职业生涯时投入更多的时间来学习它。一个简单的 curl 命令可以为您节省大量调试时间。

认证


因此,由于我们将使用来自我们自己的个人 Google 帐户的 API 密钥,因此如果我们将项目提交给 Git,我们希望确保不会将这些密钥暴露给世界其他地方。防止这种情况发生的一个很好的方法是永远不要对任何凭据进行硬编码并使用环境变量。

让我们在 MacOS 上使用 export 命令设置 YOUTUBE_KEY 和 CHANNEL_ID 环境变量,如下所示:

$ export YOUTUBE_KEY=YOUR-KEY-FROM-DEVELOPER-CONSOLE
$ export CHANNEL_ID=UCwFl9Y49sWChrddQTD9QhRA


现在我们已经完成了,我们可以使用 os.Getenv() 函数来在我们的代码库中需要它们时检索这些值。

检索我们的统计信息


现在我们已经获得了 API 密钥,我们有了一个可以点击的 API,并且我们通过 curl 看到了来自该 API 的 200 响应,我们可以开始编写我们的 youtube 包,它将处理我们的应用程序的所有交互YouTube API。

在您的项目中创建一个名为 youtube 的新目录,并在其中创建一个名为 youtube.go 的新文件。

我们将要定义一个 GetSubscribers() 函数,该函数将返回一个 Items 结构,我们稍后可以将其编组为 JSON。

// youtube-stats/youtube/youtube.go
package youtube

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

// Response models the JSON structure
// that we get back from the YouTube API
type Response struct {
    Kind  string  `json:"kind"`
    Items []Items `json:"items"`
}

// Items stores the ID + Statistics for
// a given channel
type Items struct {
    Kind  string `json:"kind"`
    Id    string `json:"id"`
    Stats Stats  `json:"statistics"`
}

// Stats stores the information we care about
// so how many views the channel has, how many subscribers
// how many video etc.
type Stats struct {
    Views       string `json:"viewCount"`
    Subscribers string `json:"subscriberCount"`
    Videos      string `json:"videoCount"`
}

func GetSubscribers() (Items, error) {
    var response Response
    // We want to craft a new GET request that will include the query parameters we want
    req, err := http.NewRequest("GET", "https://www.googleapis.com/youtube/v3/channels", nil)
    if err != nil {
        fmt.Println(err)
        return Items{}, err
    }

    // here we define the query parameters and their respective values
    q := req.URL.Query()
    // notice how I'm using os.Getenv() to pick up the environment
    // variables that we defined earlier. No hard coded credentials here
    q.Add("key", os.Getenv("YOUTUBE_KEY"))
    q.Add("id", os.Getenv("CHANNEL_ID"))
    q.Add("part", "statistics")
    req.URL.RawQuery = q.Encode()

    // finally we make the request to the URL that we have just
    // constructed
    client := &http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        return Items{}, err
    }
    defer resp.Body.Close()

    fmt.Println("Response Status: ", resp.Status)
    // we then read in all of the body of the
    // JSON response
    body, _ := ioutil.ReadAll(resp.Body)
    // and finally unmarshal it into an Response struct
    err = json.Unmarshal(body, &response)
    if err != nil {
        return Items{}, err
    }
    // we only care about the first Item in our
    // Items array, so we just send that back
    return response.Items[0], nil
}

太棒了,我们现在可以在一个独立的包中访问 YouTube API,我们可以在项目的其他部分引用它。

设置 WebSocket 端点


下一步将公开我们能够通过 WebSocket 端点从 YouTube API 检索的统计信息。

// youtube-stats/websocket/websocket.go
package websocket

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "time"

    "github.com/elliotforbes/youtube-stats/youtube"
    "github.com/gorilla/websocket"
)

// We set our Read and Write buffer sizes
var upgrader = websocket.Upgrader{
    ReadBufferSize:  1024,
    WriteBufferSize: 1024,
}

// The Upgrade function will take in an incoming request and upgrade the request
// into a websocket connection
func Upgrade(w http.ResponseWriter, r *http.Request) (*websocket.Conn, error) {
    // this line allows other origin hosts to connect to our
    // websocket server
    upgrader.CheckOrigin = func(r *http.Request) bool { return true }

    // creates our websocket connection
    ws, err := upgrader.Upgrade(w, r, nil)
    if err != nil {
        log.Println(err)
        return ws, err
    }
    // returns our new websocket connection
    return ws, nil
}

然后,我们将要创建一个 Writer 函数,该函数将接收指向我们最近打开的 WebSocket 连接的指针 - websocket.Conn。随后将使用 time 包中的一个非常方便的代码,每 5 秒开始从我们新定义的 youtube 包中调用 youtube.GetSubscribers():

// websocket.go
func Writer(conn *websocket.Conn) {
    // we want to kick off a for loop that runs for the
    // duration of our websockets connection
    for {
        // we create a new ticker that ticks every 5 seconds
        ticker := time.NewTicker(5 * time.Second)

        // every time our ticker ticks
        for t := range ticker.C {
            // print out that we are updating the stats
            fmt.Printf("Updating Stats: %+v\n", t)
            // and retrieve the subscribers
            items, err := youtube.GetSubscribers()
            if err != nil {
                fmt.Println(err)
            }
            // next we marshal our response into a JSON string
            jsonString, err := json.Marshal(items)
            if err != nil {
                fmt.Println(err)
            }
            // and finally we write this JSON string to our WebSocket
            // connection and record any errors if there has been any
            if err := conn.WriteMessage(websocket.TextMessage, []byte(jsonString)); err != nil {
                fmt.Println(err)
                return
            }
        }
    }
}

现在我们已经准备好了,我们只需要在我们的服务器上创建一个新的端点来调用这两个函数,我们就可以开始了!

我们的新端点


最后,我们要更新我们的 main.go 文件以公开我们新的 WebSocket API 端点。为此,我们将向 setupRoutes() 函数添加一个名为 /stats 的新路由,该路由将映射到我们将定义的 stats 函数。

// youtube-stats/main.go
package main

import (
    "fmt"
    "log"
    "net/http"

    "github.com/elliotforbes/youtube-stats/websocket"
)

func homePage(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello World")
}

// our new stats function which will expose any YouTube
// stats via a websocket connection
func stats(w http.ResponseWriter, r *http.Request) {
    // we call our new websocket package Upgrade
    // function in order to upgrade the connection
    // from a standard HTTP connection to a websocket one
    ws, err := websocket.Upgrade(w, r)
    if err != nil {
        fmt.Fprintf(w, "%+v\n", err)
    }
    // we then call our Writer function
    // which continually polls and writes the results
    // to this websocket connection
    go websocket.Writer(ws)
}

func setupRoutes() {
    http.HandleFunc("/", homePage)
    http.HandleFunc("/stats", stats)
    log.Fatal(http.ListenAndServe(":8080", nil))
}

func main() {
    fmt.Println("YouTube Subscriber Monitor")
    setupRoutes()
}

这应该是我们的服务器工作所需的一切!我们现在可以尝试通过调用 go run main.go 来运行它,它应该会在 http://localhost:8080 上再次启动我们的服务器。

前端


拼图的最后一块是前端。在这种情况下,我们将创建一个非常简单的前端 index.html 页面,只是为了突出您如何与我们的 WebSocket 服务器交互。

注意 - 如果您想增加一点趣味并引入 React 之类的框架,那么可能值得查看我关于使用 React 和 Go 构建实时聊天应用程序的课程

所以,我们需要添加一些 JavaScript,它会首先为我们打开一个 WebSocket 连接,然后监听 onopen 事件、onerror 事件和 onmessage 事件。

  • onopen - 将在 WebSocket 连接成功建立时触发。
  • onerror - 如果连接到我们的 WebSocket 服务器有任何错误将被触发
  • onmessage - 当我们从 WebSocket 服务器收到消息时触发。

我们最关心的是 onmessage 事件处理程序,因为我们希望从该事件中获取订阅者统计信息作为 JSON 对象。然后,我们希望在我们的页面上使用订阅者计数填充我们的 <h1 id="subs"> 元素:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <link rel="stylesheet" href="style.css" />
    <link
      rel="stylesheet"
      href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
      integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T"
      crossorigin="anonymous"
    />
    <title>Document</title>
  </head>
  <body>
    <div class="container">
      <h2>YouTube Subscribers</h2>
      <h1 id="subs"></h1>
    </div>

    <script>
      let subscribers = {};
      const websocket = new WebSocket("ws://localhost:8080/stats");

      websocket.onopen = function(event) {
        console.log("Successfully connected to websocket server");
      };

      websocket.onerror = function(error) {
        console.log("Error connecting to websocket server");
        console.log(error);
      };

      websocket.onmessage = function(event) {
        // parse the event data sent from our websocket server
        subscribers = JSON.parse(event.data);
        // populate our `sub` element with the total subscriber counter for our
        // channel
        document.getElementById("subs").innerText =
          subscribers.statistics.subscriberCount;
      };
    </script>
  </body>
</html>

太棒了,您现在已经为您的实时监控系统工作做好了一切准备!如果您在浏览器中打开此 index.html 页面,您将看到它连接到您的服务器。然后,您的服务器将开始每 5 秒调用一次 YouTube API,并将结果发送回您的前端 index.html 页面以进行渲染!

结论


在本教程中,我们介绍了一些很酷的主题,例如使用 gorilla/mux 包进行 WebSocket 通信以及处理来自 API 的 JSON 响应。

这对我来说是一个非常有趣的项目,我希望你喜欢跟随它!如果您对这个项目有任何建议或意见,那么我很乐意通过下面的建议框听到他们的意见。

如果您想支持我所做的工作,请随时与您的朋友和家人分享我的工作!每一点都有帮助! :)

文章链接