Gin 框架之Cookie与Session

news2024/11/18 14:29:50

文章目录

    • 一、Cookie和Session的由来
    • 二、Cookie简介
      • 1. 什么是Cookie
      • 2. Cookie规范
      • 3. 安全性
      • 4. Cookie 关键配置
    • 三、Session简介
      • 1. 什么是Session
      • 2. Session 安全性
      • 3. 如何让客户端携带 sess_id
    • 四、使用 Gin 的 Session 插件
      • 4.1 介绍
      • 4.2 基本使用
    • 五、 session与store
      • 5.1 会话(Session)
      • 5.2 存储(Store)
    • 六、小黄书实践

一、Cookie和Session的由来

我们知道HTTP协议无连接的, 也就是不保存用户的状态信息。

早期(十几年前)的网页是静态的, 数据都是写死的, 人们访问网页只是用来查看新闻的, 没有保存用户状态的需求。

而往后出现了像论坛、博客、网购这一类需要保存用户信息的网站, 如果网站不保存用户的状态信息, 意味着用户每次访问都需要重新输入用户名和密码, 这无疑对用户的体验是极其不好的。

于是, 就出现了会话跟踪技术, 我们可以把它理解为客户端与服务端之间的一次会晤, 一次会晤包含的多次请求与响应, 每次请求都带着请求参数, 比如请求登入的请求参数是用户名和密码, 服务端就会拿着请求参数与数据库去比对, 找到相应的用户信息。

如何实现会话跟踪 : 在HTTP协议中可以使用Cookie来完成, 在Web开发中可以使用Session来完成

  • Cookie是存在浏览器中的键值对, 每次发送请求都携带者参数, 但是容易被截获, 不安全
  • 于是就出现了Session, 它是存在于服务端的键值对, key为随机字符串, 安全性提高了, 但所有的数据都存在服务器中, 服务器的压力很大
  • 之后便产生了Token的概念, 服务端签发加密后的字符串给客户端浏览器保存, 客户端每次请求携带用户名和密码, 并加上由服务端签发的用户名和密码加密的字符串, 服务端收到请求后再对用户名密码加密, 与后面携带的密文对比, 由于它也是保存在客户端浏览器上的, 所以也叫Cookie

二、Cookie简介

1. 什么是Cookie

  • Cookie是服务器保存在客户端浏览器之上的key-value键值对 : username='jarvis';password="123"
  • 它是随着服务器的响应发送给客户端, 客户端将其保存, 下一次请求时会将Cookie放在其中, 服务器通过识别Cookie就能知道是哪个客户端浏览器

2. Cookie规范

  • Cookie大小上限为4KB
  • 一个服务器最多在客户端浏览器上保存20个Cookie
  • 一个浏览器最多保存300个Cookie

上面是HTTP中Cookie的规范, 现在浏览器的竞争, 有些Cookie大小能打到8KB, 最多可以保存500个Cookie

不同浏览器之间的Cookie是不共享的

3. 安全性

  • Cookie保存在浏览器本地, 意味着很容易被窃取和篡改

4. Cookie 关键配置

在使用 Cookie 的时候,要注意“安全使用”。

  • Domain: 也就是 Cookie 可以用在什么域名下,按照最小化原则来设定。
  • Path: Cookie 可以用在什么路径下,同样按照最小化原则来设定。
  • Max-AgeExpires: 过期时间,只保留必要时间。
  • Http-Only: 设置为 true 的话,那么浏览器上的 JS 代码将无法使用这个 Cookie。永远设置为 true。
  • Secure: 只能用于 HTTPS 协议,生产环境永远设置为 true。
  • SameSite: 是否允许跨站发送 Cookie,尽量避免。

在Go语言中,标准库net/http提供了用于处理HTTP请求和响应的功能,包括处理Cookie的相关功能。可以通过http.Cookie{}查看。

对应解释如下:

type Cookie struct {
	Name  string      // Cookie的名称,用于唯一标识一个Cookie
	Value string      // Cookie的值,存储在Cookie中的具体数据

	Path       string    // Cookie的路径,指定了哪些路径下的页面可以访问该Cookie
	Domain     string    // Cookie的域,指定了哪些域名下可以访问该Cookie
	Expires    time.Time // Cookie的过期时间,表示Cookie在何时之前有效
	RawExpires string    // 仅用于读取Cookie,保存未解析的过期时间信息

	// MaxAge=0 表示没有指定'Max-Age'属性
	// MaxAge<0 表示立即删除Cookie,相当于 'Max-Age: 0'
	// MaxAge>0 表示'Max-Age'属性存在,并以秒为单位给出
	MaxAge   int

	Secure   bool   // Secure属性,指定是否只在使用HTTPS协议时才发送Cookie
	HttpOnly bool   // HttpOnly属性,设置为true时,禁止通过JavaScript访问该Cookie

	SameSite SameSite // SameSite属性,控制是否允许跨站发送Cookie

	Raw      string   // 保存未解析的原始Cookie字符串
	Unparsed []string // 保存未解析的属性-值对文本
}

三、Session简介

1. 什么是Session

  • 存放在服务器上的键值对 : {'weqweq':{'username':'jarvis','password':'123'}}
  • Cookie可以保存状态, 但本身最大只能支持4069字节, 并且不安全, 于是就出现了Session
  • 它能支持更多字节, 并且保存在服务端上, 具有较高的安全性,Session基于Cookie, 本地存放服务器返回给浏览器的随机字符串
  • 客户端浏览器请求中携带随机字符串(session_id), 服务端收到后与数据库中存储的session做对比

ps : session存储的方式多种多样, 可以是数据库、缓存、硬盘等等

2. Session 安全性

Session 关键在于服务器要给浏览器一个 sess_id,也就是 Session 的 ID。

后续每一次请求都带上这个 Session ID,服务端就知道你是谁了。

但是后端服务器是“只认 ID 不认人”的。也就是说,如果攻击者拿到了你的 ID,那么服务器就会把攻击者当成你。在下图中,攻击者窃取到了 sess_id,就可以冒充是你了。

3. 如何让客户端携带 sess_id

因为 sess_id 是标识你身份的东西,所以你需要在每一次访问系统的时候都带上。

  • 最佳方式就是用 Cookie,也就是 sess_id 放到 Cookie 里面。sess_id 自身没有任何敏感信息,所以放 Cookie 也可以。
  • 也可以考虑放 Header,比如说在 Header 里面带一个 sess_id。这就需要前端的研发记得在 Header 里面带上。
  • 还可以考虑放查询参数,也就是 ?sess_id=xxx

理论上来说还可以放 body,但是基本没人这么干。在一些禁用了 Cookie 功能的浏览器上,只能考虑后两者。

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

四、使用 Gin 的 Session 插件

4.1 介绍

Gin框架本身并不内置对Session的支持,但你可以使用第三方的Session中间件来实现。其中比较常用的是 github.com/gin-contrib/sessions

4.2 基本使用

首先,确保你已经安装了Session中间件:

go get github.com/gin-contrib/sessions

然后在你的Gin应用程序中使用它。以下是一个基本的使用示例:

package main

import (
	"github.com/gin-contrib/sessions"
	"github.com/gin-contrib/sessions/cookie"
	"github.com/gin-gonic/gin"
	"net/http"
	"time"
)

func main() {
	// 创建Gin引擎
	r := gin.Default()

	// 使用Cookie存储Session
	store := cookie.NewStore([]byte("secret"))
	r.Use(sessions.Sessions("mysession", store))

	// 路由:设置Session
	r.GET("/set", func(c *gin.Context) {
		// 获取默认Session
		session := sessions.Default(c)
		
		// 设置Session中的键值对
		session.Set("user", "example@example.com")
		session.Set("expires", time.Now().Add(24*time.Hour).Unix())
		
		// 保存Session
		session.Save()

		// 返回JSON响应
		c.JSON(http.StatusOK, gin.H{"message": "Session set successfully"})
	})

	// 路由:获取Session
	r.GET("/get", func(c *gin.Context) {
		// 获取默认Session
		session := sessions.Default(c)
		
		// 从Session中获取键值对
		user := session.Get("user")
		expires := session.Get("expires")

		// 返回JSON响应
		c.JSON(http.StatusOK, gin.H{"user": user, "expires": expires})
	})

	// 启动Gin应用,监听端口8080
	r.Run(":8080")
}

在上述示例中,我们使用github.com/gin-contrib/sessions/cookie包创建了一个基于Cookie的Session存储。路由"/set"用于设置Session,路由"/get"用于获取Session。请注意,这里的Session数据是存储在客户端的Cookie中的,因此在实际应用中需要注意安全性。

五、 session与store

在Web应用中,会话(session)是一种用于在不同请求之间存储和共享用户信息的机制。通常,会话用于跟踪用户的身份验证状态、存储用户首选项和其他与用户相关的数据。在Gin框架中,会话的管理通常通过sessionstore两个概念来完成。

5.1 会话(Session)

  • 概念: 会话是在服务器端存储用户状态的一种机制。每个用户访问网站时,服务器都会为其创建一个唯一的会话标识符,该标识符存储在用户的浏览器中,通常通过Cookie来实现。服务器可以根据这个标识符来识别用户,并在多个请求之间共享用户的状态信息。
  • 作用: 主要用于存储用户的身份验证状态、用户的首选项、购物车内容等用户相关的信息,以便在用户访问不同页面或进行不同请求时能够保持一致的用户状态。

5.2 存储(Store)

  • 概念: 存储是用于实际存储和检索会话数据的地方。存储可以是内存、数据库、文件系统等,具体取决于应用程序的需求。存储负责维护会话数据的持久性和安全性。
  • 作用: 存储的主要作用是提供对会话数据的 CRUD(Create、Read、Update、Delete)操作。当用户发起请求时,存储会根据会话标识符检索相应的会话数据,服务器可以通过存储来实现会话的管理。

在Gin框架中,常用的Session中间件是 github.com/gin-contrib/sessions,而存储则可以选择不同的后端,例如使用Cookie、Redis、内存等。

在Gin应用中引入该中间件,通过创建Session对象和设定存储引擎,可以方便地进行Session的处理。以下是一个基本的使用示例:

package main

import (
	"github.com/gin-contrib/sessions"
	"github.com/gin-contrib/sessions/cookie"
	"github.com/gin-gonic/gin"
	"net/http"
)

func main() {
	r := gin.Default()

	// 使用Cookie存储Session
	store := cookie.NewStore([]byte("secret"))
	r.Use(sessions.Sessions("mysession", store))

	r.GET("/set", func(c *gin.Context) {
		// 设置Session
		session := sessions.Default(c)
		session.Set("user", "example@example.com")
		session.Save()

		c.JSON(http.StatusOK, gin.H{"message": "Session set successfully"})
	})

	r.GET("/get", func(c *gin.Context) {
		// 获取Session
		session := sessions.Default(c)
		user := session.Get("user")

		c.JSON(http.StatusOK, gin.H{"user": user})
	})

	r.Run(":8080")
}
  • cookie.NewStore([]byte("secret")) 在上述示例中,使用了Cookie存储。cookie.NewStore 创建了一个基于Cookie的存储引擎,使用了一个密钥来加密Cookie中的会话数据。
  • 其他存储引擎: 除了Cookie存储,github.com/gin-contrib/sessions 还支持其他存储引擎,例如Redis、文件系统等。可以根据实际需求选择合适的存储引擎。
// 使用Redis存储Session的例子
import (
	"github.com/gin-contrib/sessions"
	"github.com/gin-contrib/sessions/redis"
	"github.com/gin-gonic/gin"
)

func main() {
	r := gin.Default()

	// 使用Redis存储Session
	store, _ := redis.NewStore(10, "tcp", "localhost:6379", "", []byte("secret"))
	r.Use(sessions.Sessions("mysession", store))

	// ...
}

在实际项目中,选择存储引擎时要考虑性能、可扩展性和持久性等因素。 Cookie 存储适用于小型应用,而对于大型应用,可能需要选择支持分布式的存储引擎,如 Redis。

总体而言,使用Gin的Session中间件能够方便地管理会话,而不同的存储引擎则提供了灵活的选择,以适应不同的应用场景。

六、小黄书实践

目录结构

├── docker-compose.yaml         # Docker Compose配置文件,定义了整个应用的容器化部署
├── go.mod                      # Go模块文件,用于管理项目的依赖项
├── go.sum                      # Go模块文件,包含依赖项的版本信息
├── internal                    # 内部模块,包含应用程序的业务逻辑
│   ├── domain                  # 领域层,包含领域对象和领域逻辑
│   │   └── user.go            # 用户领域对象的定义
│   ├── repository              # 数据仓库层,用于与数据库交互
│   │   ├── dao                 # 数据访问对象,与数据库的交互接口
│   │   │   ├── init.go         # 数据库初始化
│   │   │   └── user.go         # 用户数据访问对象的定义
│   │   └── user.go             # 用户数据仓库接口的定义
│   ├── service                 # 服务层,包含业务逻辑的实现
│   │   └── user.go            # 用户服务的实现
│   └── web                     # Web层,处理HTTP请求和路由
│       ├── middleware          # 中间件,用于处理HTTP请求的中间逻辑
│       │   └── login.go       # 登录中间件的定义
│       ├── user.go             # 用户相关的HTTP处理函数和路由定义
│       └── user_test.go        # 用户相关的测试文件
├── main.go                     # 主程序入口,应用程序的启动文件
├── pkg                         # 外部可导出的包,供其他应用程序或模块使用
├── script                      # 脚本目录,包含各种初始化和辅助脚本
    └── mysql                   # MySQL初始化脚本目录
        └── init.sql            # 数据库初始化SQL脚本

internal/domain/user.go 文件代码:

package domain

import "time"

// User 领域对象,是DDD 中的 entity
type User struct {
	Id       int64
	Email    string
	Password string
	Ctime    time.Time
}

internal/repository/user.go

package repository

import (
	"context"
	"webook/internal/domain"
	"webook/internal/repository/dao"
)

var (
	ErrUserDuplicateEmail = dao.ErrUserDuplicateEmail
	ErrUserNotFound       = dao.ErrUserNotFound
)

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) 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{
		Id:       u.Id,
		Email:    u.Email,
		Password: u.Password,
	}, nil
}

internal/repository/dao/init.go

package dao

import "gorm.io/gorm"

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

internal/repository/dao/user.go

package dao

import (
	"context"
	"errors"
	"github.com/go-sql-driver/mysql"
	"gorm.io/gorm"
	"time"
)

var (
	ErrUserDuplicateEmail = errors.New("邮箱冲突")
	ErrUserNotFound       = gorm.ErrRecordNotFound
)

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().UnixNano()
	u.Ctime = now
	u.Utime = now
	err := dao.db.WithContext(ctx).Create(&u).Error
	// 类型断言,判断是否是mysql的唯一冲突错误
	if mysqlErr, ok := err.(*mysql.MySQLError); ok {
		const uniqueConflictsErrNo uint16 = 1062
		if mysqlErr.Number == uniqueConflictsErrNo {
			// 邮箱已存在
			return ErrUserDuplicateEmail
		}
	}
	return err
}
func (dao *UserDAO) FindByEmail(ctx context.Context, email string) (User, error) {
	var u User
	err := dao.db.WithContext(ctx).Where("email = ?", email).First(&u).Error
	return u, err
}

// User 直接对应数据库中的表
// 有些人叫做entity,有些人叫做model
type User struct {
	Id int64 `gorm:"primaryKey,autoIncrement"`
	// 全部用户唯一
	Email    string `gorm:"unique"`
	Password string

	// 创建时间,毫秒数,解决时区问题
	Ctime int64
	// 更新时间
	Utime int64
}

internal/service/user.go

package service

import (
	"context"
	"errors"
	"golang.org/x/crypto/bcrypt"
	"webook/internal/domain"
	"webook/internal/repository"
	"webook/internal/repository/dao"
)

var (
	ErrUserDuplicateEmail    = dao.ErrUserDuplicateEmail
	ErrInvalidUserOrPassword = errors.New("账号/邮箱或密码不对")
)

type UserService struct {
	repo *repository.UserRepository
}

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

func (svc *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 svc.repo.Create(ctx, u)
}

func (svc *UserService) Login(ctx context.Context, email, password string) (domain.User, error) {
	// 先找用户
	u, err := svc.repo.FindByEmail(ctx, email)
	if err == repository.ErrUserNotFound {
		return domain.User{}, ErrInvalidUserOrPassword
	}
	if err != nil {
		return domain.User{}, err
	}
	// 比较密码了
	err = bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(password))
	if err != nil {
		// DEBUG
		return domain.User{}, ErrInvalidUserOrPassword
	}
	return u, nil
}

internal/web/user.go

package web

import (
	regexp "github.com/dlclark/regexp2"
	"github.com/gin-contrib/sessions"
	"github.com/gin-gonic/gin"
	"net/http"
	"webook/internal/domain"
	"webook/internal/service"
)

type UserHandler struct {
	svc         *service.UserService
	emailExp    *regexp.Regexp
	passwordExp *regexp.Regexp
}

func NewUserHandler(svc *service.UserService) *UserHandler {
	const (
		emailRegexPattern    = "^\\w+([-+.]\\w+)*@\\w+([-.]\\w+)*\\.\\w+([-.]\\w+)*$"
		passwordRegexPattern = `^(?=.*[A-Za-z])(?=.*\d)(?=.*[$@$!%*#?&])[A-Za-z\d$@$!%*#?&]{8,}$`
	)
	emailExp := regexp.MustCompile(emailRegexPattern, regexp.None)
	passwordExp := regexp.MustCompile(passwordRegexPattern, regexp.None)
	return &UserHandler{
		svc:         svc,
		emailExp:    emailExp,
		passwordExp: passwordExp,
	}
}

func (u *UserHandler) RegisterRoutes(server *gin.Engine) {
	ug := server.Group("/user")   //ug is user group
	ug.GET("/profile", u.Profile) // 查询用户信息接口
	ug.POST("/signup", u.SignUp)  // 注册接口
	ug.POST("/login", u.Login)    // 登录接口
	ug.POST("/logout", u.Logout)  // 登出接口
	ug.POST("/edit", u.Edit)      // 修改用户信息接口

}

func (u *UserHandler) RegisterRoutesV1(ug *gin.RouterGroup) {
	ug.GET("/profile", u.Profile) // 查询用户信息接口
	ug.POST("/signup", u.SignUp)  // 注册接口
	ug.POST("/login", u.Login)    // 登录接口
	ug.POST("/logout", u.Logout)  // 登出接口
	ug.POST("/edit", u.Edit)      // 修改用户信息接口

}
func (u *UserHandler) Profile(ctx *gin.Context) {
}
func (u *UserHandler) SignUp(ctx *gin.Context) {
	type SignUpRequest struct {
		Email           string `json:"email"`
		Password        string `json:"password"`
		ConfirmPassword string `json:"confirmPassword"`
	}
	var request SignUpRequest
	// 如果 Bind 方法发现输入有问题,它就会直接返回一 个错误响应到前端。
	if err := ctx.Bind(&request); err != nil {
		return
	}

	ok, err := u.emailExp.MatchString(request.Email)
	if err != nil {
		ctx.String(http.StatusOK, "系统错误")
		return
	}
	if !ok {
		ctx.String(http.StatusOK, "邮箱格式错误")
		return
	}
	ok, err = u.passwordExp.MatchString(request.Password)
	if err != nil {
		ctx.String(http.StatusOK, "系统错误")
		return
	}
	if !ok {
		ctx.String(http.StatusOK, "密码必须包含至少一个数字、一个字母、一个特殊字符,并且长度至少为8位")
		return
	}
	if request.Password != request.ConfirmPassword {
		ctx.String(http.StatusOK, "两次密码不一致")
		return
	}
	// 调用以一下svc 的方法
	err = u.svc.SignUp(ctx, domain.User{
		Email:    request.Email,
		Password: request.Password,
	})
	if err == service.ErrUserDuplicateEmail {
		ctx.String(http.StatusOK, "邮箱冲突")
		return
	}
	if err != nil {
		ctx.String(http.StatusOK, "系统错误")
		return

	}
	ctx.String(http.StatusOK, "注册成功")
}
func (u *UserHandler) Login(ctx *gin.Context) {
	type LoginRequest struct {
		Email    string `json:"email"`
		Password string `json:"password"`
	}
	var request LoginRequest
	if err := ctx.Bind(&request); err != nil {
		return
	}
	// 调用以一下svc 的方法
	user, err := u.svc.Login(ctx, request.Email, request.Password)

	if err == service.ErrInvalidUserOrPassword {
		ctx.String(http.StatusOK, "账号或密码错误")
		return
	}
	if err != nil {
		ctx.String(http.StatusOK, "系统错误")
		return
	}
	// 登录成功设置session
	sess := sessions.Default(ctx)
	// 设置session的key和value
	sess.Set("userId", user.Id)
	sess.Save()
	ctx.String(http.StatusOK, "登录成功")
	return
}

func (u *UserHandler) Logout(ctx *gin.Context) {

}
func (u *UserHandler) Edit(ctx *gin.Context) {

}

main.go

package main

import (
	"github.com/gin-contrib/cors"
	"github.com/gin-contrib/sessions"
	"github.com/gin-contrib/sessions/cookie"
	"github.com/gin-gonic/gin"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	"strings"
	"time"
	"webook/internal/repository"
	"webook/internal/repository/dao"
	"webook/internal/service"
	"webook/internal/web"
	"webook/internal/web/middleware"
)

func main() {
	db := initDB()
	ser := initWebServer()
	u := initUser(db)
	u.RegisterRoutes(ser)
	ser.Run(":8080")

}

func initWebServer() *gin.Engine {
	ser := gin.Default()
	ser.Use(func(ctx *gin.Context) {
		println("这是一个中间件")
	})
	ser.Use(func(ctx *gin.Context) {
		println("这是二个中间件")
	})
	ser.Use(cors.New(cors.Config{
		//AllowOrigins: []string{"*"},
		//AllowMethods: []string{"POST", "GET"},
		AllowHeaders: []string{"Content-Type", "Authorization"},
		//ExposeHeaders: []string{"x-jwt-token"},
		// 是否允许你带 cookie 之类的东西
		AllowCredentials: true,
		AllowOriginFunc: func(origin string) bool {
			if strings.HasPrefix(origin, "http://localhost") {
				// 你的开发环境
				return true
			}
			return strings.Contains(origin, "yourcompany.com")
		},
		MaxAge: 12 * time.Hour,
	}))
	// 用 cookie 存储 session
	store := cookie.NewStore([]byte("secret"))
	// 使用 session 中间件
	ser.Use(sessions.Sessions("mysession", store))
	//ser.Use(middleware.NewLoginMiddlewareBuilder().Build())
	//ser.Use(middleware.NewLoginMiddlewareBuilder().
	//	IgnorePaths("/users/signup").
	//	IgnorePaths("/users/login").Build())
	// v1
	middleware.IgnorePaths = []string{"sss"}
	ser.Use(middleware.CheckLogin())

	// 不能忽略sss这条路径
	server1 := gin.Default()
	server1.Use(middleware.CheckLogin())
	return ser
}

func initUser(db *gorm.DB) *web.UserHandler {
	ud := dao.NewUserDAO(db)
	repo := repository.NewUserRepository(ud)
	svc := service.NewUserService(repo)
	u := web.NewUserHandler(svc)
	return u
}
func initDB() *gorm.DB {
	db, err := gorm.Open(mysql.Open("root:root@tcp(localhost:13316)/webook"))
	if err != nil {
		// panic 相当于 整个 goroutine 结束
		panic("failed to connect database")
	}
	err = dao.InitTable(db)
	if err != nil {
		panic("failed to init table")
	}
	return db
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1396386.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

第六回 花和尚倒拔垂杨柳 豹子头误入白虎堂-安装服务器管理面板AMH和cyberpanel

且说鲁智深踏入菜园之时&#xff0c;二三十个泼皮无赖正聚集于此&#xff0c;他们手持果盒酒礼&#xff0c;脸上嬉皮笑脸&#xff0c;口称前来庆贺。然而&#xff0c;当这群人走到粪窖边缘&#xff0c;打头阵的张三和李四竟妄想搬动鲁智深&#xff0c;结果却被他轻描淡写地一脚…

Microsoft365管理员创建共享邮箱

​​​​​​ 创建共享邮箱 项目2023/08/2110 个参与者 反馈 本文内容 创建共享邮箱并添加成员您应使用哪些权限&#xff1f;阻止登录共享邮箱帐户向 Outlook 添加共享邮箱 显示另外 3 个 备注 如果你的组织使用的是混合 Exchange 环境&#xff0c;则你应使用本地 Excha…

新能源汽车智慧充电桩方案:智能高效的充电桩管理模式及应用场景

一、行业背景 随着全球对环境保护的日益重视&#xff0c;新能源汽车成为了未来的发展趋势。而充电桩作为新能源汽车的核心基础设施&#xff0c;其智慧化的解决方案对于推动新能源汽车的普及和发展至关重要。通过智能化、高效化的充电服务&#xff0c;提高用户体验&#xff0c;…

智慧充电桩的市场前景未来

随着电动汽车的日益普及&#xff0c;充电问题成为广大车主的关注焦点。作为领先的科技企业&#xff0c;天津通捷创科为您带来了一站式解决方案——共享充电桩APP。 <h1>一、即刻定位&#xff0c;充电桩触手可及</h1> 在天津通捷创科的共享充电桩APP中&#xff0c…

洛谷P5731 【深基5.习6】蛇形方阵(C语言)

思路感觉还是比较好想的。 从 1 到 n 依次算。先往右&#xff0c;走到头往下&#xff0c;再走到头往左&#xff0c;以此类推。 #include<stdio.h>int main() {int n, i, j, k1,t0;scanf("%d", &n);int a[100][100];if (n % 2 0)t n / 2;elset n / 2 …

测温传感器表带式ATE200安装指导

ATE200 安装方法 ATE200 表带式无线测温传感器适用于断路器动触头、静触头、电缆接头、母排等处。 表带式无线温度传感器结构说明: ATE200 structure introduction: 1 —— 无线温度传感器主体&#xff0c;测温探头在背面 The core of wireless temperature sensor ATE200,…

2023总结,瞳孔滤镜

2022总结&#xff0c;强风吹拂 2021总结&#xff0c;欲望反光 2020总结&#xff0c;我与思阳 2019总结&#xff0c;乘风破浪 2018总结&#xff0c;蜗行牛步 2017总结&#xff0c;勿忘初心 得益于疫情的结束&#xff0c;出行自由&#xff0c;2023年09月24日下午 &#xf…

Qt/C++自定义界面大全/20套精美皮肤/26套精美UI界面/一键换肤/自定义颜色/各种导航界面

一、前言 这个系列对应自定义控件大全&#xff0c;一个专注于控件的编写&#xff0c;一个专注于UI界面的编写&#xff0c;程序员有两大软肋&#xff0c;一个是忌讳别人说自己的程序很烂很多bug&#xff0c;一个就是不擅长UI&#xff0c;基本上配色就直接rgb&#xff0c;对于第…

精品基于Uniapp+springboot校园学校趣事管理系统app

《[含文档PPT源码等]精品基于Uniappspringboot趣事管理系统app》该项目含有源码、文档、PPT、配套开发软件、软件安装教程、项目发布教程、包运行成功&#xff01; 软件开发环境及开发工具&#xff1a; 开发语言&#xff1a;Java 后台框架&#xff1a;springboot、ssm 安卓…

大模型学习与实践笔记(九)

一、LMDeply方式部署 使用 LMDeploy 以本地对话方式部署 InternLM-Chat-7B 模型&#xff0c;生成 300 字的小故事 2.api 方式部署 运行 结果&#xff1a; 显存占用&#xff1a; 二、报错与解决方案 在使用命令&#xff0c;对lmdeploy 进行源码安装是时&#xff0c;报错 1.源…

航空飞行器运维VR模拟互动教学更直观有趣

传统的二手车鉴定评估培训模式存在实践性不强、教学样本不足、与实际脱节等一些固有的不足。有了VR虚拟仿真技术的加持&#xff0c;二手车鉴定评估VR虚拟仿真实训系统逐渐进入实训领域&#xff0c;为院校及企业二手车检测培训提供了全新的解决方案。 高职院校汽车专业虚拟仿真实…

MySQL 基于创建时间进行RANGE分区

MySQL是一款广泛使用的关系型数据库。在MySQL中&#xff0c;大量数据场景提高查询效率是非常关键的&#xff0c;所以&#xff0c;对数据表进行分区是一个很好的选择。 在创建分区表之前&#xff0c;需要了解一下MySQL分区的基本概念。MySQL分区可以将一个大表分成多个小表&…

《后疫情时代薪酬管理和数字化趋势报告》

经历了疫情的严峻考验&#xff0c;企业迎来了工作模式和组织管理的一系列新变革&#xff0c;比如远程办公、线上协作、灵活用工等。为了助力企业在后疫情时代积极应对挑战&#xff0c;在2023年的新起点上抢夺先发优势&#xff0c;上海外服针对400余家企业开展专项调研&#xff…

Kubernetes(K8S)拉取本地镜像部署Pod 实现类似函数/微服务功能(可设置参数并实时调用)

以两数相加求和为例&#xff0c;在kubernetes集群拉取本地的镜像&#xff0c;实现如下效果&#xff1a; 1.实现两数相加求和 2.可以通过curl实时调用&#xff0c;参数以GET方式提供&#xff0c;并得到结果。&#xff08;类似调用函数&#xff09; 一、实现思路 需要准备如下的…

【学习记录24】vue3自定义指令

一、在单vue文件中直接使用 1、html部分 <template><divstyle"height: 100%;"v-loading"loading"><ul><li v-for"item in data">{{item}} - {{item * 2}}</li></ul></div> </template> 2、js…

golang面试题大全

go基础类 1、与其他语言相比&#xff0c;使用 Go 有什么好处&#xff1f; 与其他作为学术实验开始的语言不同&#xff0c; Go 代码的设计是务实的。每个功能和语法决策都旨在让程序员的生活更轻松。Golang 针对并发进行了优化&#xff0c;并且在规模上运行良好。由于单一的标…

Java进阶-Tomcat发布JavaWeb项目

对于云服务器&#xff0c;程序员一般不会陌生&#xff0c;如果项目需要发布到现网&#xff0c;那么服务器是必不可缺的一项硬性条件&#xff0c;那么如何在云服务器上部署一个项目&#xff0c;需要做哪些配置准备&#xff0c;下面就由本文档为大家讲解&#xff0c;本篇以Tomcat…

springcloud之链路追踪

写在前面 源码 。 本文一起来看下链路追踪的功能&#xff0c;链路追踪是一种找出病因的手段&#xff0c;可以类比医院的检查仪器&#xff0c;服务医生治病救人&#xff0c;而链路追踪技术是辅助开发人员查找线上问题的。 1&#xff1a;为什么微服务需要链路追踪 孔子同志月过…

python数字图像处理基础(十)——背景建模

目录 背景建模背景消除-帧差法混合高斯模型 背景建模 背景建模是计算机视觉和图像处理中的一项关键技术&#xff0c;用于提取视频中的前景对象。在视频监控、运动检测和行为分析等领域中&#xff0c;背景建模被广泛应用。其基本思想是通过对视频序列中的像素进行建模&#xff…

【信号与系统】【北京航空航天大学】实验四、幅频、相频响应和傅里叶变换

一、实验目的 1、 掌握利用MATLAB计算系统幅频、相频响应的方法&#xff1b; 2、 掌握使用MATLAB进行傅里叶变换的方法&#xff1b; 3、 掌握使用MATLAB验证傅里叶变换的性质的方法。 二、实验内容 1、 MATLAB代码&#xff1a; >> clear all; >> a [1 3 2]; …