golang 入门教程:迷你 Twitter 后端

请记住,这个项目主要是为了稍微熟悉下Golang,您可以复制架构,但该项目缺少适当的 ORM,没有适当的身份验证或授权,我完全无视中间件,也没有测试。
我将在其自己的部分中讨论所有这些问题,但重要的是你要知道这还没有准备好投入生产。

如果我必须从头开始或重新制作项目,我会添加诸如 sqlx 和 Gorm 之类的库。 以及改进 API 和我在下面进行的其他更改。

此外,我想谈谈我正在使用的路由库:Fiber。 请注意,它是版本 2,但是很多教程和博客文章都在展示和谈论 v1,此后发生了一些变化,因此在查找其他信息时确保导入是 github.com/gofiber/fiber/v2。

此外,您绝对应该在项目中有一个配置文件和一个 .env 来隐藏您的数据。

README 中还有一个免责声明:
免责声明
这是一个介绍性的项目,可以稍微了解一下 Golang。 这个项目还没有准备好生产,它有不好的做法,比如分页的工作方式或我们与数据库的交互方式。
通过代码库,您会发现用于调试项目的不同“打印件”,请随意使用它们。 生产就绪缺少什么?
您应该添加适当的记录器、配置、中间件、处理数据的不同方式(可能是 ORM)、处理分页的更好方式以及更好的 API。
请注意,如果您复制并粘贴代码,可能会使自己容易受到 SQL 注入的攻击。 这是出于学习目的而制作的。

Database

您可能应该让 Docker 运行 MySQL(或任何其他 SQL 数据库)。 如果没有,你总是可以在你的系统上安装 MySQL 并使用像 MySQL Workbench 这样的东西来处理它。

Database design

在这里插入图片描述

数据库本身相当简单,你有发布推文的用户和一个关注者表来保存谁关注谁的数据。关注者表主要是为我们的用户实现提要/时间线。

Create Database

在脚本文件夹中,您将找到运行以启动 MySQL 数据库的主要脚本:

use twitterdb;

DROP TABLE IF EXISTS tweets;
DROP TABLE IF EXISTS users;
DROP TABLE IF EXISTS followers;

CREATE TABLE users (
  user_id INT NOT NULL AUTO_INCREMENT,
  user VARCHAR(255) NOT NULL,
  passhash VARCHAR(40) NOT NULL,
  email VARCHAR(255) NOT NULL,
  first_name VARCHAR(255) NOT NULL,
  last_name VARCHAR(255) NOT NULL,
  dob DATE,
  PRIMARY KEY (user_id)
);

CREATE TABLE tweets (
  tweet_id INT NOT NULL AUTO_INCREMENT,
  user_id INT NOT NULL,
  tweet VARCHAR(140) NOT NULL,
  date_tweet DATETIME NOT NULL,
  PRIMARY KEY (tweet_id),
  FOREIGN KEY user_id(user_id) REFERENCES users(user_id) 
  ON UPDATE CASCADE ON DELETE CASCADE
);

CREATE TABLE followers (
  id_user INT NOT NULL REFERENCES users (user_id),
  id_follower INT NOT NULL REFERENCES users (user_id),
  PRIMARY KEY (id_user, id_follower)
);

INSERT INTO users (user, passhash, email, first_name, last_name, dob) VALUES
("foo", "asdsad1", "test@gmail.com", "bob", "bobbinson", "2006-01-01"),
("foo2", "asdsad2", "test2@gmail.com", "bob2", "bobbinson2", "1992-01-01"),
("foo3", "asdsad3", "test3@gmail.com", "bob3", "bobbinson3", "1993-01-01"),
("foo4", "asdsad4", "test4@gmail.com", "bob4", "bobbinson4", "1994-01-01"),
("foo5", "asdsad5", "test5@gmail.com", "bob5", "bobbinson5", "1995-01-01"),
("foo6", "asdsad6", "test6@gmail.com", "bob6", "bobbinson6", "1996-01-01"),
("foo7", "asdsad7", "test7@gmail.com", "bob7", "bobbinson7", "1925-01-01"),
("foo8", "asdsad8", "test8@gmail.com", "bob8", "bobbinson8", "1980-01-01"),
("foo9", "asdsad9", "test9@gmail.com", "bob9", "bobbinson9", "1980-01-01"),
("foo10", "asdsad10", "test10@gmail.com", "bob10", "bobbinson10", "1970-01-01");

INSERT INTO tweets(user_id, tweet, date_tweet) VALUES
(1, "test tweet", "2001-01-01 22:00:00"),
(2, "test tweet2", "2002-01-01 22:00:00"),
(3, "test tweet3", "2003-01-01 22:00:00"),
(4, "test tweet4", "2004-01-01 22:00:00"),
(5, "test tweet5", "2005-01-01 22:00:00");

INSERT INTO followers(id_user, id_follower) VALUES
(5,1),
(4,1),
(3,1),
(2,1),
(6,1),
(2,5),
(4,5);

其他文件是我们稍后将使用的查询示例。

Project Architecture

在这里插入图片描述

如果您熟悉构建软件,如果不熟悉这应该很熟悉,首先让我告诉您缺少什么,然后我们将遍历每个文件夹。

该项目缺少配置、中间件、记录器和测试等文件夹。 如果你能更好地组织控制器,你就会有一个路由文件夹,你可以更好地组织 API。

关于导入包的说明

在这个项目中,您可能会对以下导入感到困惑:

import (
    "goexample/database"
    "goexample/models"
    "goexample/services/utils"
)

这里的 goexample 是项目的名称,我只是在之后重命名了我的 repo 这样它才有意义,所以我们使用“goexample/database”而不是“mini-twitter-clone/database”来导入数据库包。
对于任何其他新项目,只需使用文件夹的名称。

API

路由存储在 controller.go 中:

package api

import (
    "goexample/services"

    "github.com/gofiber/fiber/v2"
)

func SetupRoutes(app *fiber.App) {

    api := app.Group("/api")

    //get all unordened users
    api.Get("/users", services.GetUsers)
    //get all users ordered by age ASC
    api.Get("/users/age", services.GetUsersByAgeAsc)

    //get all unordened tweets from db
    api.Get("/tweets", services.GetTweets)

    //http://localhost:3000/api/feed/1
    //get MOST RECENT feed/timeline for the user
    api.Get("/feed/:id", services.GetFeedTweets)
    //pagination
    api.Get("/feed/:id/:limit/:offset", services.GetFeedTweetsPaginated)
    //can try https://github.com/gofiber/fiber/issues/193#issuecomment-591976894
    //a whole presentation on why you shouldn't do what I did:
    //https://use-the-index-luke.com/no-offset

}

我正在使用类似于 Express 的 Fiber 库,这里我们有几个端点,我们通过 Get 请求调用这些端点,一旦服务器收到请求,它的响应就是调用我从服务包中公开的函数——那就是 我们的业务逻辑在哪里。

注意:分页未正确实现。

这只是一个简单的示例,此端点与其他端点一样存在安全问题,但您应该通过示例了解库和 Golang 的工作方式。

请注意,在 /feed/:id 中,id 参数与我们要为其获取提要的用户有关。

启动项目

您可以通过执行 go run 来启动项目。 文件夹内。
您的终端应如下所示:

在这里插入图片描述
让我们访问不同的端点以查看响应,我将使用普通浏览,但如果您不熟悉调试后端,则应检查 Postman。

让我们从控制器运行端点:

http://127.0.0.1:3000/api/users

在这里插入图片描述
我留下了很多打印输出,所以如果你在每次通话时检查你的终端,你应该会看到如下内容:在这里插入图片描述

接下来是我们按年龄对用户排序的调用:

http://127.0.0.1:3000/api/users/age

当我们请求推文时,我们会得到所有的推文:

http://127.0.0.1:3000/api/tweets

在这里插入图片描述

在这里插入图片描述
现在我们获取 ID 为 1 的用户的提要或时间线:

http://127.0.0.1:3000/api/feed/1

在这里插入图片描述

在这里插入图片描述
请记住,分页尚未准备好生产,但核心概念是相同的:

http://127.0.0.1:3000/api/feed/1/2/1

在这里插入图片描述
由于我们在服务包中的业务逻辑和我们公开的功能,所有调用都能正常工作。

Services

在服务包中,我们有当某人或某物点击其中一个端点时调用的函数。 请记住,我们通过在包中使用大写字母来公开功能。

让我们看一下 timeline_tweets.go,它包含 controller.go 文件中两个不同端点的两个函数:


package services

import (
    "fmt"
    "goexample/database"
    "goexample/models"
    "goexample/services/utils"
    "log"

    "github.com/gofiber/fiber/v2"
)

func GetFeedTweets(c *fiber.Ctx) error {

    //you shouldn't do this by the way, but it's just a demo
    // dbQuery := fmt.Sprintf("SELECT users.user_id, users.user, users.first_name, users.last_name, tweets.tweet, tweets.date_tweet FROM users INNER JOIN tweets ON users.user_id = tweets.user_id INNER JOIN followers ON users.user_id = followers.id_user WHERE followers.id_follower = %s ORDER BY tweets.date_tweet DESC;", c.Params("id"))
    // rows, err := database.DB.Query(dbQuery)

    //avoid the SQL injection by rewriting it like
    dbQuery := "SELECT users.user_id, users.user, users.first_name, users.last_name, tweets.tweet, tweets.date_tweet FROM users INNER JOIN tweets ON users.user_id = tweets.user_id INNER JOIN followers ON users.user_id = followers.id_user WHERE followers.id_follower = ? ORDER BY tweets.date_tweet DESC;"
    rows, err := database.DB.Query(dbQuery, c.Params("id"))

    //check for errors
    if err != nil {
        return utils.DefaultErrorHandler(c, err)
    }
    //close db connection
    defer rows.Close()

    //create a slice of tweets
    var timelineTweets []models.TimelineTweet
    //loop through the result set
    for rows.Next() {
        timelineTweet := models.TimelineTweet{}
        err := rows.Scan(&timelineTweet.User_id, &timelineTweet.User, &timelineTweet.First_name, &timelineTweet.Last_name, &timelineTweet.Tweet, &timelineTweet.Date_tweet)
        if err != nil {
            log.Fatal(err)
        }
        timelineTweets = append(timelineTweets, timelineTweet)
    }
    fmt.Print(timelineTweets)

    utils.ResponseHelperJSON(c, timelineTweets, "timeline", "No timeline found")

    return err
}

func GetFeedTweetsPaginated(c *fiber.Ctx) error {

    // dbQuery := fmt.Sprintf("SELECT users.user_id, users.user, users.first_name, users.last_name, tweets.tweet, tweets.date_tweet FROM users INNER JOIN tweets ON users.user_id = tweets.user_id INNER JOIN followers ON users.user_id = followers.id_user WHERE followers.id_follower = %s ORDER BY tweets.date_tweet DESC LIMIT %s OFFSET %s;", c.Params("id"), c.Params("limit"), c.Params("offset"))
    // avoid a SQL injection by rewriting it like
    dbQuery := "SELECT users.user_id, users.user, users.first_name, users.last_name, tweets.tweet, tweets.date_tweet FROM users INNER JOIN tweets ON users.user_id = tweets.user_id INNER JOIN followers ON users.user_id = followers.id_user WHERE followers.id_follower = ? ORDER BY tweets.date_tweet DESC LIMIT ? OFFSET ?;"

    rows, err := database.DB.Query(dbQuery, c.Params("id"), c.Params("limit"), c.Params("offset"))
    if err != nil {
        return utils.DefaultErrorHandler(c, err)
    }

    defer rows.Close()

    var timelineTweets []models.TimelineTweet
    for rows.Next() {
        timelineTweet := models.TimelineTweet{}
        err := rows.Scan(&timelineTweet.User_id, &timelineTweet.User, &timelineTweet.First_name, &timelineTweet.Last_name, &timelineTweet.Tweet, &timelineTweet.Date_tweet)
        if err != nil {
            log.Fatal(err)
        }
        timelineTweets = append(timelineTweets, timelineTweet)
    }
    //TODO: implement a response with pages and all that pagination jazz
    utils.ResponseHelperJSON(c, timelineTweets, "timeline", "No timeline found")

    return err
}

您首先注意到的是我们如何将上下文传递给 GetFeedTweets,然后我们使用变量“c”来使用 Fiber 库所说的上下文。

之后,我们使用包数据库中公开的变量 DB 打开 DB,读取数据,然后关闭它。

为了正确存储和扫描数据,我们使用模型包中的结构。

之后您将看到 utils 包中的几个函数。 这些函数主要是辅助函数,因此您可以查看 Golang 如何执行循环和某些其他操作。

我们的服务包中的其他两个文件非常相似,我们在这里的工作是从数据库中获取数据,通过我们的结构数组扫描它,然后以 JSON 格式将其发送回用户。 以及我们运行 utils 包中的一些功能。

Utils

这个包包含辅助函数,也许最有趣的是在 user_helper 里面,因为它有与切片数据交互的函数,但是要小心,它们的实现并不像你想象的那么好。

让我们看看最有帮助的,response_helper.go

package utils

import (
    "github.com/gofiber/fiber/v2"
)

//response JSON for services after you loop and scan
func ResponseHelperJSON(c *fiber.Ctx, data any, dataType string, dataError string) {
    if data != nil {
        c.Status(200).JSON(&fiber.Map{
            "success": true,
            dataType:  data,
        })
    } else {
        c.Status(404).JSON(&fiber.Map{
            "success": false,
            "error":   dataError,
        })
    }
}

请注意,此函数包含通用类型的数据,因为我使用关键字 any 并且我正在与 Fiber 通过变量“c”提供的上下文进行交互。

Models

models 文件夹包含我们用作与数据库交互的实体的不同结构,如 services 文件夹中所示。

这是一个示例,请注意,即使您想公开整个结构,所有内容都必须以大写字母开头:

package models

type UserWithAge struct{
    Id         int   `json:"id"`
    User       string `json:"user"`
    Passhash   string `json:"passhash"`
    Email      string `json:"email"`
    First_name string `json:"first_name"`
    Last_name  string `json:"last_name"`
    Age        int    `json:"age"`
}

Database

数据库包非常简单,请记住使用配置文件和 .env 来存储您的敏感数据。

package database

import (
    "database/sql"
    "fmt"
    "log"
)

var DB *sql.DB

func Connect() error{
    var err error
    //use a config file for this
    DB, err = sql.Open("mysql", "root:password@tcp(127.0.0.1:3306)/twitterdb")

    if err != nil {
        log.Fatal(err)
        return err
    }

    if err = DB.Ping(); err != nil {
        log.Fatal(err)
        return err
    }

    fmt.Println("Connected to database")

    return nil

}

Main.go

最后,这是我们启动服务器的地方:

package main

import (
    "github.com/gofiber/fiber/v2"
    "goexample/database"
    "log"

    "goexample/api"

    _ "github.com/go-sql-driver/mysql"
)

func main() {

    if err := database.Connect(); err != nil {
        log.Fatal(err)
    }

    app := fiber.New()
    api.SetupRoutes(app)


    log.Fatal(app.Listen(":3000"))

}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值