百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

Golang Gin 入门 (三)_golang 快速入门

myzbx 2025-09-29 08:32 38 浏览

Golang Gin 入门 (二)

接上,预期中的调用流程如下图

简单逻辑代码

web/user.go

func (c *UserHandler) RegisterRoutes(server *gin.Engine) {
    // 分组注册
    up := server.Group("/users")
    up.POST("/signup", c.SignUp)
    up.POST("/login", c.Login)
    up.POST("/edit", c.Edit)
    up.GET("/profile", c.Profile)
}

type UserHandler struct {
    svc              *service.UserService
    emailRegexExp    *regexp2.Regexp
    PasswordRegexExp *regexp2.Regexp
}

const (
    // 定义邮箱的正则表达式
    emailRegex = `^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}Golang Gin 入门 (三) - 今日头条
// 定义密码的正则表达式 至少8个字符 //包含大写字母 //包含小写字母 //包含数字 //包含特殊字符 passwordRegex = `^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}Golang Gin 入门 (三) - 今日头条
) func NewUserHandler(svc *service.UserService) *UserHandler { return &UserHandler{ svc: svc, emailRegexExp: regexp2.MustCompile(emailRegex, regexp2.None), PasswordRegexExp: regexp2.MustCompile(passwordRegex, regexp2.None), } } func (u *UserHandler) SignUp(context *gin.Context) { type SingUpReq struct { Email string `json:"email"` ConfirmPassword string `json:"confirmPassword"` Password string `json:"password"` } var req SingUpReq // Bind 方法会根据 Content-Type 来解析你的数据req里面 // 解析错了,就直接回写一个 400(http.StatusBadRequest)的错误. c.AbortWithError(http.StatusBadRequest, err).SetType(ErrorTypeBind) if err := context.Bind(&req); err != nil { return } isEmail, err := u.emailRegexExp.MatchString(req.Email) if err != nil { context.String(http.StatusOK, "系统错误 %v", err) return } if !isEmail { context.String(http.StatusOK, "邮箱校验不通过") return } isPassword, err := u.PasswordRegexExp.MatchString(req.Password) if err != nil { context.String(http.StatusOK, "系统错误 %v", err) return } if !isPassword { context.String(http.StatusOK, "密码校验不通过") return } if req.ConfirmPassword != req.Password { context.String(http.StatusOK, "两次输入的密码不一致") return } err = u.svc.Signup(context, domain.User{ Email: req.Email, Password: req.Password, }) if err != nil { context.String(http.StatusOK, "系统错误 %v", err) return } context.String(http.StatusOK, "注册成功") //log.Printf("%v\n", req) }

service/user.go

type UserService struct {
    repo *repository.UserRepository
}

func NewUserService(repo *repository.UserRepository) *UserService {
    return &UserService{repo: repo}
}

func (scv *UserService) Signup(ctx context.Context, u domain.User) error {
    // 要考虑加密放在哪  然后存起来
    return scv.repo.Create(ctx, u)
}

repository/user.go

type UserRepository struct {
    dao *dao.UserDAO
}

func NewUserRepository(dao *dao.UserDAO) *UserRepository {
    return &UserRepository{dao: dao}
}


func (r *UserRepository) Create(ctx context.Context, u domain.User) error {
    return r.dao.Insert(ctx, dao.User{
        Email:    u.Email,
        Password: u.Password,
    })
}

func (r *UserRepository) FindById(int64) {
    // 先从cache里找
    // 再从dao里面找
    // 找到了在回写cache
}

repository/dao/user.go

type UserDAO struct {
    db *gorm.DB
}

func NewUserDAO(db *gorm.DB) *UserDAO {
    return &UserDAO{db: db}
}

func (dao *UserDAO) Insert(ctx context.Context, u User) error {
    now := time.Now().UnixMilli() // 存毫秒
    u.Ctime = now
    u.Utime = now
    return dao.db.WithContext(ctx).Create(&u).Error
}

func InitTable(db *gorm.DB) error {
    return db.AutoMigrate(&User{})
}

// User在DAO里直接对应数据库表
type User struct {
    Id       int64
    Email    string
    Password string
    // 创建时间和更新时间毫秒数,用time.time会涉及到时区,在数据库和应用层都会涉及到时区,可以选择用UTC时间戳,在前端转时区
    Ctime int64
    Utime int64
}

main.go组装好全部的东西,在抽取到不同的方法里面

func initWebServer() *gin.Engine {
    engine := gin.Default()
    engine.Use(cors.New(cors.Config{
        //AllowOrigins:     []string{"http://localhost:3000"},
        AllowMethods: []string{"PUT", "PATCH", "GET", "POST", "OPTIONS"},
        AllowHeaders: []string{"Content-Type", "authorization"},
        //ExposeHeaders:    []string{"x-jwt-token"},
        AllowCredentials: true, //是否允许你带cookie之类的
        AllowOriginFunc: func(origin string) bool {
            if strings.HasPrefix(origin, "http://localhost") {
                return true // 开发环境
            }
            return strings.Contains(origin, "your.company.com")
        },
        MaxAge: 12 * time.Hour,
    }))
    return engine
}

func initDB() *gorm.DB {
    db, err := gorm.Open(mysql.Open("root:ssqj@easyviews.pw@tcp(10.1.125.32:3307)/webook"))
    if err != nil {
        panic(err)
    }
    err = dao.InitTable(db)
    if err != nil {
        panic(err)
    }
    return db
}

func initUser(server *gin.Engine, db *gorm.DB) {
    ud := dao.NewUserDAO(db)
    ur := repository.NewUserRepository(ud)
    us := service.NewUserService(ur)
    c := web.NewUserHandler(us)
    c.RegisterRoutes(server)
}

func main() {

    db := initDB()
    server := initWebServer()
    initUser(server, db)

    if err := server.Run(":8080"); err != nil {
        log.Printf("Listen err: %v\n", err)
    }
}

调用注册接口,查看数据库是否写入

mysql> show tables;
+------------------+
| Tables_in_webook |
+------------------+
| users            |
+------------------+
1 row in set (0.01 sec)

mysql> select * from users;
+----+-------------------+-------------+---------------+---------------+
| id | email             | password    | ctime         | utime         |
+----+-------------------+-------------+---------------+---------------+
|  1 | 2864048202@qq.com | Rrootroot1* | 1730300023836 | 1730300023836 |
+----+-------------------+-------------+---------------+---------------+
1 row in set (0.01 sec)

密码加密

密码怎么加密? 代码看上去没有问题,但是好像忘了一件事情:密码是敏感信息,需要加密存储。 问题来了:谁来加密?service 还是 repository 还是 dao? 怎么加密?怎么选择一个安全的加密算法? 敏感信息你要防两类人:研发,包括你和你的同事。攻击者。 PS:敏感信息应该是连日志都不能打。

关于密码加密的位置存在多种选择且各有理由:

  • service 加密:加密被认为是业务概念,不是存储概念,这里选择 service 加密是因为认为加密是业务逻辑的一部分但不是 domain 应该管的。
  • repository 加密:加密可被看作是存储概念,即 “加密存储”。
  • dao 加密:加密是数据库概念,可以利用数据库本身的加密功能实现。
  • domain 加密:加密是业务概念,认为用户(“User”)自己才知道怎么加密。这是编程中比较无正确答案的实践问题,不同的加密位置会影响到其他接口(如登录)的实现细节。

这种就是编程里面比较无聊的、没有正确答案的实践问题。这里我选择 service 加密,也就是认为加密是业务逻辑的一部分,但是它不是 domain 应该管的。如果你选择不同的加密位置,那么它会影响到你别的接口的实现细节,比如说登录。

加密算法的选择对系统安全性至关重要,攻击者拿到密码可能为所欲为。选择加密算法的标准是难破解,需考虑以下问题:

  • 相同密码加密后结果应不同,防止很多用户使用简单密码(如 123456)时数据库存储值相同。
  • 难以通过碰撞、彩虹表来破解。

常见加密算法安全性逐步提高的有:

  1. md5之类的哈希算法。
  2. 在哈希算法基础上引入盐值或进行多次哈希。
  3. PBKDF2BCrypt这一类随机盐值的加密算法,同样的文本加密后的结果都不同。

bcrypt被称为号称最安全的加密算法,具有以下优点:

  • 不需要自己生成盐值。
  • 不需要额外存储盐值。
  • 可以通过控制cost来控制加密性能。
  • 同样的文本加密后的结果不同。 如果要使用它,需要使用golang.org/x/crypto。由于bcrypt限制密码长度不能超过 72 字节,所以在校验时要校验这个长度,只需要修改一下正则表达式即可。

使用bcrypt加密后无法解密,只能通过比较加密后的值来确定两个值是否相等。

func (scv *UserService) Signup(ctx context.Context, u domain.User) error {
    // 要考虑加密放在哪  然后存起来
    hash, err := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost)
    if err != nil {
        return err
    }
    u.Password = string(hash)
    
    return scv.repo.Create(ctx, u)
}

登录功能

大多数网站的资源需要登录才能访问,比如编辑和查看用户信息。

登录功能分为两件事:

  • 实现登录功能
  • 登录态校验

登录请求会被发送到/users/login

在登录接口实现中,可以看出 service 和 repository 的分界线。service 会调用 repository 查找邮箱对应的用户,然后 service 会匹配输入的密码和数据库中保存的密码是否一致。如果用户没找到或者密码错误,都返回同一个 error。

// dao层
func (dao *UserDAO) FindByEmail(ctx context.Context, email string) (u User, err error) {
    // err = dao.db.WithContext(ctx).First(&u,"email = ?",email).Error        // 二选一
    err = dao.db.WithContext(ctx).Where("email = ?", email).First(&u).Error
    return u, err
}

// repository层
func (r *UserRepository) FindByEmail(ctx context.Context, email string) (domain.User, error) {
    u, err := r.dao.FindByEmail(ctx, email)
    if err != nil {
        return domain.User{}, err
    }
    return domain.User{
        Email:    u.Email,
        Password: u.Password,
    }, err
}

//service层
func (scv *UserService) Login(ctx context.Context, email, password string) error {
    // 先看有没有这个用户
    u, err := scv.repo.FindByEmail(ctx, email)
    if err != nil {
        return ErrInvalidUserOrPaswword
    }
    //
    err = bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(password))
    if err != nil {
        return ErrInvalidUserOrPaswword
    }
    return nil
}

//web
func (c *UserHandler) Login(context *gin.Context) {
    type LoginReq struct {
        Email    string `json:"email"`
        Password string `json:"password"`
    }
    var req LoginReq
    if err := context.Bind(&req); err != nil {
        context.String(http.StatusOK, "系统错误")
        return
    }
    err := c.svc.Login(context, req.Email, req.Password)
    if err != nil {
        context.String(http.StatusOK, "账户或密码错误")
        return
    }
    context.String(http.StatusOK, "登录成功")
    return
}

Cookie 和 Session

在登录成功后,当访问/users/profile时,面临如何确定用户是否已登录的问题。

HTTP 协议是无状态的,即连续发送两次请求时,HTTP 无法知道这两个请求都是同一个用户发出的,无法将上一次请求和这一次请求关联起来。

因此需要一种机制来记录状态,于是就有了 Cookie 和 Session。

Cookie 是浏览器存储在本地的一些数据,简单理解为存储在电脑上的键值对。由于 Cookie 存放在浏览器本地,所以很不安全。

在使用 Cookie 时要注意安全配置:

  • Domain:设定 Cookie 可用的域名,按照最小化原则设置。
  • Path:设定 Cookie 可用的路径,同样按照最小化原则设置。
  • Max-Age 和 Expires:设置 Cookie 的过期时间,只保留必要时间。
  • Http-Only:设置为 true 时,浏览器上的 JS 代码无法使用该 Cookie,应永远设置为 true。
  • Secure:仅用于 HTTPS 协议,在生产环境永远设置为 true。
  • SameSite:决定是否允许跨站发送 Cookie,尽量避免。

在面试初级工程师岗位时,能详细解释这些参数含义会赢得微小竞争优势。

由于 Cookie 具有不安全的特性,所以大部分时候只在 Cookie 中放一些不太关键的数据。关键数据希望放在后端存储,这个存储的东西叫做 Session。在登录场景中,可以通过 Session 来记录登录状态。


在使用 Session 进行登录时,关键在于服务器要给浏览器一个 sess_id(即 Session 的 ID)。后续每次请求都带上这个 Session ID,服务端就能通过这个 ID 知道发出请求的用户是谁。

Session 机制中后端服务器认 ID 不认人。这意味着如果攻击者拿到了用户的 Session ID,那么服务器就会把攻击者当成该用户。例如在图中,攻击者窃取到了 sess_id 后,就可以冒充用户。


由于 sess_id 是标识用户身份的东西,需要在每一次访问系统时携带。有以下几种方式:

  • 最佳方式:使用 Cookie,将 sess_id 放到 Cookie 里面。因为 sess_id 自身没有任何敏感信息,所以放在 Cookie 中是可行的。
  • 另一种方式:放在 Header 中,例如在 Header 里面带一个 sess_id,但这需要前端研发人员记得在 Header 中带上。
  • 还可以:放在查询参数中,即 ?sess_id=xxx
  • 理论上也可以放在请求体(body)中,但基本没人这么做。
  • 在一些禁用了 Cookie 功能的浏览器上,只能考虑后两种方式(放在 Header 中或查询参数中)。


在浏览器上,可以通过插件 cookieEditor 查看某个网站的 Cookie 信息。

可以使用 Gin 的 Session 插件来实现登录功能。一般遇到问题可以找插件,热门功能 Gin 通常有插件可用。这里提到的 Gin 的 Session 插件使用分为两部分:

  1. 在 middleware(中间件)里面接入,它会从 Cookie 中找到 sess_id,再根据 sess_id 找到对应的 Session。
  2. 拿到 Session 之后可以进行各种操作,例如在这里用来校验是否登录。其插件地址为 https://github.com/gin-contrib/sessions。
func initWebServer() *gin.Engine {
    engine := gin.Default()
    engine.Use(cors.New(cors.Config{
        //AllowOrigins:     []string{"http://localhost:3000"},
        AllowMethods: []string{"PUT", "PATCH", "GET", "POST", "OPTIONS"},
        AllowHeaders: []string{"Content-Type", "authorization"},
        //ExposeHeaders:    []string{"x-jwt-token"},
        AllowCredentials: true, //是否允许你带cookie之类的
        AllowOriginFunc: func(origin string) bool {
            if strings.HasPrefix(origin, "http://localhost") {
                return true // 开发环境
            }
            return strings.Contains(origin, "your.company.com")
        },
        MaxAge: 12 * time.Hour,
    }))

    store := cookie.NewStore([]byte("secret"))
    // cookie 的名字叫ssid
    engine.Use(sessions.Sessions("ssid", store))
    engine.Use(middleware.NewLoginMiddlewareBuilder().Build())

    return engine

}


//  middleware.go
type LoginMiddlewareBuilder struct {
}

func NewLoginMiddlewareBuilder() *LoginMiddlewareBuilder {
    return &LoginMiddlewareBuilder{}
}

func (l *LoginMiddlewareBuilder) Build() gin.HandlerFunc {
    return func(ctx *gin.Context) {
        if ctx.Request.URL.Path == "/users/login" || ctx.Request.URL.Path == "/users/signup" {
            return // 登录和注册不需要校验
        }
        sess := sessions.Default(ctx)

        if sess.Get("userId") == nil {
            // 没有登录
            ctx.AbortWithStatus(http.StatusUnauthorized)
            return
        }
    }
}

// web
func (c *UserHandler) Login(context *gin.Context) {
    type LoginReq struct {
        Email    string `json:"email"`
        Password string `json:"password"`
    }
    var req LoginReq
    if err := context.Bind(&req); err != nil {
        context.String(http.StatusOK, "系统错误")
        return
    }
    user, err := c.svc.Login(context, req.Email, req.Password)
    if err != nil {
        context.String(http.StatusOK, "账户或密码错误")
        return
    }
    sess := sessions.Default(context)
    sess.Set("userId", user.Id)
    sess.Save()
    context.String(http.StatusOK, "登录成功")
    return
}

面试题

一、什么是 Cookie,什么是 Session?

  • Cookie
    • Cookie 是浏览器存储在本地的一些数据,简单理解为存储在电脑上的键值对。例如,一些网站会将用户的偏好设置、登录状态等信息存储在 Cookie 中。
    • Cookie 可以在不同的请求中被发送到服务器,服务器可以根据 Cookie 中的信息识别用户。但是,由于 Cookie 存储在客户端,所以安全性较低,容易被篡改或窃取。
    • 在使用 Cookie 时,要注意安全配置,如按照最小化原则设置 Domain 和 Path、合理设置 Max-Age 和 Expires、将 Http-Only 设置为 true(防止浏览器上的 JS 代码使用 Cookie)、在生产环境将 Secure 设置为 true(仅用于 HTTPS 协议)、尽量避免使用 SameSite 允许跨站发送 Cookie。
  • Session
    • 由于 Cookie 本身不安全的特性,大部分时候我们只在 Cookie 里面放一些不太关键的数据。关键数据我们希望放在后端,这个存储关键数据的东西就叫做 Session。
    • 在登录场景中,可以通过 Session 来记录登录状态。服务器给浏览器一个 sess_id(Session 的 ID),后续每一次请求都带上这个 Session ID,服务端就能通过这个 ID 识别用户身份。

二、Cookie 和 Session 比起来有什么缺点?

  • 安全性低:Cookie 存储在客户端,容易被篡改或窃取。而 Session 数据存储在服务器端,相对安全。
  • 存储容量有限:Cookie 的存储容量通常较小,一般为 4KB 左右。而 Session 可以存储更多的数据。
  • 性能影响:每次请求都需要发送 Cookie 数据,会增加网络传输量,对性能有一定影响。而 Session ID 通常较小,对网络传输的影响较小。

三、Session ID 可以放在哪里?这个问题,你要记得提起 Cookie 禁用的问题。

Session ID 可以放在以下几个地方:

  • Cookie 中:这是最常见的方式。将 Session ID 放在 Cookie 中,每次请求时浏览器会自动发送 Cookie,服务器可以根据 Session ID 找到对应的 Session。但是,如果用户禁用了 Cookie,这种方式就不可行了。
  • Header 中:可以在请求的 Header 中添加一个自定义的字段,用来携带 Session ID。例如,可以在 Header 里面带一个 sess_id。这需要前端的研发人员记得在每次请求时在 Header 中带上 Session ID。
  • 查询参数中:可以将 Session ID 放在查询参数中,即 ?sess_id=xxx。但是这种方式不太安全,容易被用户看到和篡改。
  • 理论上也可以放在请求体(body)中,但基本没人这么做。

在一些禁用了 Cookie 功能的浏览器上,可以考虑使用 Header 或查询参数的方式来携带 Session ID。

四、用户密码加密算法选取有什么注意事项?你用的是什么?

用户密码加密算法选取的注意事项:

  • 难破解:选择加密算法的标准是难破解。要考虑相同的密码加密后的结果应该不同,防止很多用户使用简单密码(如 123456)时数据库存储值相同。同时,难以通过碰撞、彩虹表来破解。
  • 安全性高:常见加密算法安全性逐步提高的有 md5 之类的哈希算法,但 md5 相对不安全;在哈希算法基础上引入盐值或进行多次哈希可以提高安全性;PBKDF2BCrypt 这一类随机盐值的加密算法安全性更高,同样的文本加密后的结果都不同。

这里可以选择使用 BCrypt 加密算法,它有以下优点:

  • 不需要自己生成盐值。
  • 不需要额外存储盐值。
  • 可以通过控制 cost 来控制加密性能。
  • 同样的文本,加密后的结果不同。

如果要使用 BCrypt,需要使用 golang.org/x/crypto。由于 BCrypt 限制密码长度不能超过 72 字节,所以在校验时要校验这个长度,只需要修改一下正则表达式即可。

五、怎么做登录校验?核心是利用 Gin 的 middleware。

登录校验可以通过以下步骤实现:


登录接口实现

  • 在登录请求被发到 /users/login 上时,service 会调用 repository 查找邮箱所对应的用户。
  • 然后 service 会匹配输入的密码和数据库中保存的是否一致。如果用户没找到或者密码错误,都返回同一个 error。
  1. 利用 Gin 的 middleware 进行登录校验

Gin 的 Session 插件用起来分成两部分:

  • 一个是在 middleware(中间件)里面接入,它会帮你从 Cookie 里面找到 sess_id,再根据 sess_id 找到对应的 Session。
  • 另外一部分就是你拿到这个 Session 之后,就可以进行登录校验等操作。例如,可以在 Session 中存储用户的登录状态信息,当用户访问需要登录才能访问的资源(如 /users/profile)时,通过 middleware 检查 Session 中的登录状态信息,如果用户已登录,则允许访问,否则返回错误信息。





相关推荐

如何设计一个优秀的电子商务产品详情页

加入人人都是产品经理【起点学院】产品经理实战训练营,BAT产品总监手把手带你学产品电子商务网站的产品详情页面无疑是设计师和开发人员关注的最重要的网页之一。产品详情页面是客户作出“加入购物车”决定的页面...

怎么在JS中使用Ajax进行异步请求?

大家好,今天我来分享一项JavaScript的实战技巧,即如何在JS中使用Ajax进行异步请求,让你的网页速度瞬间提升。Ajax是一种在不刷新整个网页的情况下与服务器进行数据交互的技术,可以实现异步加...

中小企业如何组建,管理团队_中小企业应当如何开展组织结构设计变革

前言写了太多关于产品的东西觉得应该换换口味.从码农到架构师,从前端到平面再到UI、UE,最后走向了产品这条不归路,其实以前一直再给你们讲.产品经理跟项目经理区别没有特别大,两个岗位之间有很...

前端监控 SDK 开发分享_前端监控系统 开源

一、前言随着前端的发展和被重视,慢慢的行业内对于前端监控系统的重视程度也在增加。这里不对为什么需要监控再做解释。那我们先直接说说需求。对于中小型公司来说,可以直接使用三方的监控,比如自己搭建一套免费的...

Ajax 会被 fetch 取代吗?Axios 怎么办?

大家好,很高兴又见面了,我是"高级前端进阶",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发!今天给大家带来的主题是ajax、fetch...

前端面试题《AJAX》_前端面试ajax考点汇总

1.什么是ajax?ajax作用是什么?AJAX=异步JavaScript和XML。AJAX是一种用于创建快速动态网页的技术。通过在后台与服务器进行少量数据交换,AJAX可以使网页实...

Ajax 详细介绍_ajax

1、ajax是什么?asynchronousjavascriptandxml:异步的javascript和xml。ajax是用来改善用户体验的一种技术,其本质是利用浏览器内置的一个特殊的...

6款可替代dreamweaver的工具_替代powerdesigner的工具

dreamweaver对一个web前端工作者来说,再熟悉不过了,像我07年接触web前端开发就是用的dreamweaver,一直用到现在,身边的朋友有跟我推荐过各种更好用的可替代dreamweaver...

我敢保证,全网没有再比这更详细的Java知识点总结了,送你啊

接下来你看到的将是全网最详细的Java知识点总结,全文分为三大部分:Java基础、Java框架、Java+云数据小编将为大家仔细讲解每大部分里面的详细知识点,别眨眼,从小白到大佬、零基础到精通,你绝...

福斯《死侍》发布新剧照 "小贱贱"韦德被改造前造型曝光

时光网讯福斯出品的科幻片《死侍》今天发布新剧照,其中一张是较为罕见的死侍在被改造之前的剧照,其余两张剧照都是死侍在执行任务中的状态。据外媒推测,片方此时发布剧照,预计是为了给不久之后影片发布首款正式预...

2021年超详细的java学习路线总结—纯干货分享

本文整理了java开发的学习路线和相关的学习资源,非常适合零基础入门java的同学,希望大家在学习的时候,能够节省时间。纯干货,良心推荐!第一阶段:Java基础重点知识点:数据类型、核心语法、面向对象...

不用海淘,真黑五来到你身边:亚马逊15件热卖爆款推荐!

Fujifilm富士instaxMini8小黄人拍立得相机(黄色/蓝色)扫二维码进入购物页面黑五是入手一个轻巧可爱的拍立得相机的好时机,此款是mini8的小黄人特别版,除了颜色涂装成小黄人...

2025 年 Python 爬虫四大前沿技术:从异步到 AI

作为互联网大厂的后端Python爬虫开发,你是否也曾遇到过这些痛点:面对海量目标URL,单线程爬虫爬取一周还没完成任务;动态渲染的SPA页面,requests库返回的全是空白代码;好不容易...

最贱超级英雄《死侍》来了!_死侍超燃

死侍Deadpool(2016)导演:蒂姆·米勒编剧:略特·里斯/保罗·沃尼克主演:瑞恩·雷诺兹/莫蕾娜·巴卡林/吉娜·卡拉诺/艾德·斯克林/T·J·米勒类型:动作/...

停止javascript的ajax请求,取消axios请求,取消reactfetch请求

一、Ajax原生里可以通过XMLHttpRequest对象上的abort方法来中断ajax。注意abort方法不能阻止向服务器发送请求,只能停止当前ajax请求。停止javascript的ajax请求...