G0第25章:Gin框架进阶项目实战

news2024/12/23 14:03:15

1 Gin框架源码解析

通过阅读gin框架的源码来探究gin框架路由与中间件的秘密。

1.1 Gin框架路由详解

gin框架使用的是定制版本的httprouter,其路由的原理是大量使用公共前缀的树结构,它基本上是一个紧凑的Trie tree 或者只是(Radix Tree)。具有公共前缀的节点也共享一个公共父节点。

Radix Tree

基数树,由成为PAT位树,是一种更节省空间的前缀树。对于基数树的每个节点,如果该节点是唯一的子树的话,就和父节点合并。
请添加图片描述
Radix Tree 可以被认为是一颗简洁版的前缀树。我们注册路由的过程就是构造前缀树的过程,具有公共前缀的节点也共享一个公共父节点。假设我们现在注册有以下路由信息:

r := gin.Default()

r.GET("/", func1)
r.GET("/search/", func2)
r.GET("/support/", func3)
r.GET("/blog/", func4)
r.GET("/blog/:post/", func5)
r.GET("/about-us/", func6)
r.GET("/about-us/team/", func7)
r.GET("/contact/", func8)

那么我们会得到一个GET方法对应的路由树,具体结构如下:

Priority   Path             Handle
9          \                *<1>
3          ├s               nil
2          |├earch\         *<2>
1          |└upport\        *<3>
2          ├blog\           *<4>
1          |    └:post      nil
1          |\     *<5>
2          ├about-us\       *<6>
1          |        └team\  *<7>
1          └contact\        *<8>

上面最右边那一列每个*<数字>表示Handle处理函数的内存地址(一个指针)。从根节点遍历到叶子节点我们就能得到完整的路由表。
例如:blog/:post其中:post只是实际文章名称的占位符(参数)。与hash-maps不同,这种树结构还允许我们使用像:post参数这种动态部分,因为我们实际上是根据路由模式进行匹配,而不仅仅是比较哈希值。

由于URL路径具有层次结构,并且只使用优先的一组字符(字节值),所以很可能有许多常见的前缀。这使得我们可以很容易的将路由简化为更小的问题。此外, 路由器为每种请求方法管理一棵单独的树。 一方面,它比在每个节点中都保存一个method-> handle map 更加节省空间,它还使我们甚至可以在开始在前缀树中查找之前大大减少路由问题。

为了获得更好的伸缩性,每个树级别上的子节点都按 Priority (优先级)排序,其中优先级(最左列)就是在子节点(子节点、子子节点等等)注册的句柄的数量。这样做有两个好处:

  • 1、首先优先匹配被大多数路由路径包含的节点。这样可以让尽可能多的路由快速被定位;
  • 2、类似于成本补偿。最长的路径可以被优先匹配,补偿体现在最长的路径需要花费更长的时间来定位,如果最长路径的节点能被优先匹配(即每次拿子节点都命中),那么路由匹配所花的时间不一定比短路径的路由长。
    节点匹配路径:从左到右、从上到下。(每个-可以看做一个节点)
├------------
   ├---------
   ├-----
   ├----
   ├--
   ├--
   └-

路由树节点

路由树是由一个个节点构成的,gin框架路由树的节点由 node 结构体表示,它有以下字段:

// tree.go

type node struct {
   // 节点路径,比如上面的s,earch,和upport
	path      string
	// 和children字段对应, 保存的是分裂的分支的第一个字符
	// 例如search和support, 那么s节点的indices对应的"eu"
	// 代表有两个分支, 分支的首字母分别是e和u
	indices   string
	// 儿子节点
	children  []*node
	// 处理函数链条(切片)
	handlers  HandlersChain
	// 优先级,子节点、子子节点等注册的handler数量
	priority  uint32
	// 节点类型,包括static, root, param, catchAll
	// static: 静态节点(默认),比如上面的s,earch等节点
	// root: 树的根节点
	// catchAll: 有*匹配的节点
	// param: 参数节点
	nType     nodeType
	// 路径上最大参数个数
	maxParams uint8
	// 节点是否是参数节点,比如上面的:post
	wildChild bool
	// 完整路径
	fullPath  string
}

请求方法树

在gin的路由中,每一个 HTTP Method (GET、POST、PUT、DELETE…)都对应了一颗 radix tree , 我们注册路由的时候会调用下面的 addRoute 函数

// gin.go
func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
   // liwenzhou.com...
   
   // 获取请求方法对应的树
	root := engine.trees.get(method)
	if root == nil {
	
	   // 如果没有就创建一个
		root = new(node)
		root.fullPath = "/"
		engine.trees = append(engine.trees, methodTree{method: method, root: root})
	}
	root.addRoute(path, handlers)
}

从上面的代码中我们可以看到在注册路由的时候都是先根据请求方法获取对应的树,也就是gin框架会为每一个请求方法创建一棵对应的树。只不过需要注意到一个细节是gin框架中保存请求方法对应树关系并不是使用的map而是使用的切片,engine.trees的类型是methodTrees,其定义如下:

type methodTree struct {
	method string
	root   *node
}

type methodTrees []methodTree  // slice

而获取请求方法对应树的get方法定义如下:

func (trees methodTrees) get(method string) *node {
	for _, tree := range trees {
		if tree.method == method {
			return tree.root
		}
	}
	return nil
}
为什么使用切片而不是map来存储 请求方法->树 的结构呢?

GPT解答:

1、性能考虑:在某些情况下,遍历小型切片的性能可能优于在映射中查找键。尤其是在请求方法数量相对较少的情况下(比如HTTP有GET, POST, PUT, DELETE等方法),线性搜索切片可能比在哈希表(也就是map)中查找更快;
2、内存占用:相较于map,切片通常会使用更少的内存。如果我们只需要存储少量的元素,那么切片可能会是更经济的选择;
3、简单性:使用切片可能会让代码更加简洁易读。在这个函数中,我们可以直接使用一个for循环来查找匹配的元素。如果我们使用map,我们就需要处理map返回的第二个值,这个值用来表示键是否存在于map中;
4、保持顺序:切片保持了元素的插入顺序,这在某些应用中可能是必需的。与此相反,map在Go语言中是无序的。

顺着这个思路,我们可以看一下gin框架中engine的初始化方法中,确实对tress字段做了一次内存申请:

func New() *Engine {
	debugPrintWARNINGNew()
	engine := &Engine{
		RouterGroup: RouterGroup{
			Handlers: nil,
			basePath: "/",
			root:     true,
		},
		// liwenzhou.com ...
		// 初始化容量为9的切片(HTTP1.1请求方法共9种)
		trees:                  make(methodTrees, 0, 9),
		// liwenzhou.com...
	}
	engine.RouterGroup.engine = engine
	engine.pool.New = func() interface{} {
		return engine.allocateContext()
	}
	return engine
}

注册路由

注册路由的逻辑主要有addRoute函数和insertChild方法。

addRoute
// tree.go

// addRoute 将具有给定句柄的节点添加到路径中。
// 不是并发安全的
func (n *node) addRoute(path string, handlers HandlersChain) {
	fullPath := path
	n.priority++
	numParams := countParams(path)  // 数一下参数个数

	// 空树就直接插入当前节点
	if len(n.path) == 0 && len(n.children) == 0 {
		n.insertChild(numParams, path, fullPath, handlers)
		n.nType = root
		return
	}

	parentFullPathIndex := 0

walk:
	for {
		// 更新当前节点的最大参数个数
		if numParams > n.maxParams {
			n.maxParams = numParams
		}

		// 找到最长的通用前缀
		// 这也意味着公共前缀不包含“:”"或“*” /
		// 因为现有键不能包含这些字符。
		i := longestCommonPrefix(path, n.path)

		// 分裂边缘(此处分裂的是当前树节点)
		// 例如一开始path是search,新加入support,s是他们通用的最长前缀部分
		// 那么会将s拿出来作为parent节点,增加earch和upport作为child节点
		if i < len(n.path) {
			child := node{
				path:      n.path[i:],  // 公共前缀后的部分作为子节点
				wildChild: n.wildChild,
				indices:   n.indices,
				children:  n.children,
				handlers:  n.handlers,
				priority:  n.priority - 1, //子节点优先级-1
				fullPath:  n.fullPath,
			}

			// Update maxParams (max of all children)
			for _, v := range child.children {
				if v.maxParams > child.maxParams {
					child.maxParams = v.maxParams
				}
			}

			n.children = []*node{&child}
			// []byte for proper unicode char conversion, see #65
			n.indices = string([]byte{n.path[i]})
			n.path = path[:i]
			n.handlers = nil
			n.wildChild = false
			n.fullPath = fullPath[:parentFullPathIndex+i]
		}

		// 将新来的节点插入新的parent节点作为子节点
		if i < len(path) {
			path = path[i:]

			if n.wildChild {  // 如果是参数节点
				parentFullPathIndex += len(n.path)
				n = n.children[0]
				n.priority++

				// Update maxParams of the child node
				if numParams > n.maxParams {
					n.maxParams = numParams
				}
				numParams--

				// 检查通配符是否匹配
				if len(path) >= len(n.path) && n.path == path[:len(n.path)] {
					// 检查更长的通配符, 例如 :name and :names
					if len(n.path) >= len(path) || path[len(n.path)] == '/' {
						continue walk
					}
				}

				pathSeg := path
				if n.nType != catchAll {
					pathSeg = strings.SplitN(path, "/", 2)[0]
				}
				prefix := fullPath[:strings.Index(fullPath, pathSeg)] + n.path
				panic("'" + pathSeg +
					"' in new path '" + fullPath +
					"' conflicts with existing wildcard '" + n.path +
					"' in existing prefix '" + prefix +
					"'")
			}
			// 取path首字母,用来与indices做比较
			c := path[0]

			// 处理参数后加斜线情况
			if n.nType == param && c == '/' && len(n.children) == 1 {
				parentFullPathIndex += len(n.path)
				n = n.children[0]
				n.priority++
				continue walk
			}

			// 检查路path下一个字节的子节点是否存在
			// 比如s的子节点现在是earch和upport,indices为eu
			// 如果新加一个路由为super,那么就是和upport有匹配的部分u,将继续分列现在的upport节点
			for i, max := 0, len(n.indices); i < max; i++ {
				if c == n.indices[i] {
					parentFullPathIndex += len(n.path)
					i = n.incrementChildPrio(i)
					n = n.children[i]
					continue walk
				}
			}

			// 否则就插入
			if c != ':' && c != '*' {
				// []byte for proper unicode char conversion, see #65
				// 注意这里是直接拼接第一个字符到n.indices
				n.indices += string([]byte{c})
				child := &node{
					maxParams: numParams,
					fullPath:  fullPath,
				}
				// 追加子节点
				n.children = append(n.children, child)
				n.incrementChildPrio(len(n.indices) - 1)
				n = child
			}
			n.insertChild(numParams, path, fullPath, handlers)
			return
		}

		// 已经注册过的节点
		if n.handlers != nil {
			panic("handlers are already registered for path '" + fullPath + "'")
		}
		n.handlers = handlers
		return
	}
}

其实上面的代码很好理解,大家可以参照动画尝试将以下情形代入上面的代码逻辑,体味整个路由树构造的详细过程:

第一次注册路由,例如注册search
继续注册一条没有公共前缀的路由,例如blog
注册一条与先前注册的路由有公共前缀的路由,例如support

请添加图片描述

insertChild

// tree.go
func (n *node) insertChild(numParams uint8, path string, fullPath string, handlers HandlersChain) {
  // 找到所有的参数
	for numParams > 0 {
		// 查找前缀直到第一个通配符
		wildcard, i, valid := findWildcard(path)
		if i < 0 { // 没有发现通配符
			break
		}

		// 通配符的名称必须包含':' 和 '*'
		if !valid {
			panic("only one wildcard per path segment is allowed, has: '" +
				wildcard + "' in path '" + fullPath + "'")
		}

		// 检查通配符是否有名称
		if len(wildcard) < 2 {
			panic("wildcards must be named with a non-empty name in path '" + fullPath + "'")
		}

		// 检查这个节点是否有已经存在的子节点
		// 如果我们在这里插入通配符,这些子节点将无法访问
		if len(n.children) > 0 {
			panic("wildcard segment '" + wildcard +
				"' conflicts with existing children in path '" + fullPath + "'")
		}

		if wildcard[0] == ':' { // param
			if i > 0 {
				// 在当前通配符之前插入前缀
				n.path = path[:i]
				path = path[i:]
			}

			n.wildChild = true
			child := &node{
				nType:     param,
				path:      wildcard,
				maxParams: numParams,
				fullPath:  fullPath,
			}
			n.children = []*node{child}
			n = child
			n.priority++
			numParams--

			// 如果路径没有以通配符结束
			// 那么将有另一个以'/'开始的非通配符子路径。
			if len(wildcard) < len(path) {
				path = path[len(wildcard):]

				child := &node{
					maxParams: numParams,
					priority:  1,
					fullPath:  fullPath,
				}
				n.children = []*node{child}
				n = child  // 继续下一轮循环
				continue
			}

			// 否则我们就完成了。将处理函数插入新叶子中
			n.handlers = handlers
			return
		}

		// catchAll
		if i+len(wildcard) != len(path) || numParams > 1 {
			panic("catch-all routes are only allowed at the end of the path in path '" + fullPath + "'")
		}

		if len(n.path) > 0 && n.path[len(n.path)-1] == '/' {
			panic("catch-all conflicts with existing handle for the path segment root in path '" + fullPath + "'")
		}

		// currently fixed width 1 for '/'
		i--
		if path[i] != '/' {
			panic("no / before catch-all in path '" + fullPath + "'")
		}

		n.path = path[:i]
		
		// 第一个节点:路径为空的catchAll节点
		child := &node{
			wildChild: true,
			nType:     catchAll,
			maxParams: 1,
			fullPath:  fullPath,
		}
		// 更新父节点的maxParams
		if n.maxParams < 1 {
			n.maxParams = 1
		}
		n.children = []*node{child}
		n.indices = string('/')
		n = child
		n.priority++

		// 第二个节点:保存变量的节点
		child = &node{
			path:      path[i:],
			nType:     catchAll,
			maxParams: 1,
			handlers:  handlers,
			priority:  1,
			fullPath:  fullPath,
		}
		n.children = []*node{child}

		return
	}

	// 如果没有找到通配符,只需插入路径和句柄
	n.path = path
	n.handlers = handlers
	n.fullPath = fullPath
}

insertChild 函数是根据path本身进行分割,将/分开的部分分别作为节点保存,形成一棵树结构。参数匹配中的:和*的区别是,前者是匹配一个字段而后者是匹配后面所有的路径。

路由匹配

我们先来看gin框架处理请求的入口函数ServeHTTP:

// gin.go
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
  // 这里使用了对象池
	c := engine.pool.Get().(*Context)
  // 这里有一个细节就是Get对象后做初始化
	c.writermem.reset(w)
	c.Request = req
	c.reset()

	engine.handleHTTPRequest(c)  // 我们要找的处理HTTP请求的函数

	engine.pool.Put(c)  // 处理完请求后将对象放回池子
}

函数很长,这里省略了部分代码,只保留相关逻辑代码:

// gin.go
func (engine *Engine) handleHTTPRequest(c *Context) {
	// liwenzhou.com...

	// 根据请求方法找到对应的路由树
	t := engine.trees
	for i, tl := 0, len(t); i < tl; i++ {
		if t[i].method != httpMethod {
			continue
		}
		root := t[i].root
		// 在路由树中根据path查找
		value := root.getValue(rPath, c.Params, unescape)
		if value.handlers != nil {
			c.handlers = value.handlers
			c.Params = value.params
			c.fullPath = value.fullPath
			c.Next()  // 执行函数链条
			c.writermem.WriteHeaderNow()
			return
		}
	
	// liwenzhou.com...
	c.handlers = engine.allNoRoute
	serveError(c, http.StatusNotFound, default404Body)
}

路由匹配是由节点的 getValue 方法实现的。getValue 根据给定的路径(键)返回 nodeValue 值,保存注册的处理函数和匹配到的路径参数数据。如果找不到任何处理函数,则会尝试TS(尾随斜杠重定向)。代码虽然很长,但还算比较工整。
大家可以借助注释看一下路由查找及参数匹配的逻辑。

// tree.go

type nodeValue struct {
	handlers HandlersChain
	params   Params  // []Param
	tsr      bool
	fullPath string
}

// liwenzhou.com...

func (n *node) getValue(path string, po Params, unescape bool) (value nodeValue) {
	value.params = po
walk: // Outer loop for walking the tree
	for {
		prefix := n.path
		if path == prefix {
			// 我们应该已经到达包含处理函数的节点。
			// 检查该节点是否注册有处理函数
			if value.handlers = n.handlers; value.handlers != nil {
				value.fullPath = n.fullPath
				return
			}

			if path == "/" && n.wildChild && n.nType != root {
				value.tsr = true
				return
			}

			// 没有找到处理函数 检查这个路径末尾+/ 是否存在注册函数
			indices := n.indices
			for i, max := 0, len(indices); i < max; i++ {
				if indices[i] == '/' {
					n = n.children[i]
					value.tsr = (len(n.path) == 1 && n.handlers != nil) ||
						(n.nType == catchAll && n.children[0].handlers != nil)
					return
				}
			}

			return
		}

		if len(path) > len(prefix) && path[:len(prefix)] == prefix {
			path = path[len(prefix):]
			// 如果该节点没有通配符(param或catchAll)子节点
			// 我们可以继续查找下一个子节点
			if !n.wildChild {
				c := path[0]
				indices := n.indices
				for i, max := 0, len(indices); i < max; i++ {
					if c == indices[i] {
						n = n.children[i] // 遍历树
						continue walk
					}
				}

				// 没找到
				// 如果存在一个相同的URL但没有末尾/的叶子节点
				// 我们可以建议重定向到那里
				value.tsr = path == "/" && n.handlers != nil
				return
			}

			// 根据节点类型处理通配符子节点
			n = n.children[0]
			switch n.nType {
			case param:
				// find param end (either '/' or path end)
				end := 0
				for end < len(path) && path[end] != '/' {
					end++
				}

				// 保存通配符的值
				if cap(value.params) < int(n.maxParams) {
					value.params = make(Params, 0, n.maxParams)
				}
				i := len(value.params)
				value.params = value.params[:i+1] // 在预先分配的容量内扩展slice
				value.params[i].Key = n.path[1:]
				val := path[:end]
				if unescape {
					var err error
					if value.params[i].Value, err = url.QueryUnescape(val); err != nil {
						value.params[i].Value = val // fallback, in case of error
					}
				} else {
					value.params[i].Value = val
				}

				// 继续向下查询
				if end < len(path) {
					if len(n.children) > 0 {
						path = path[end:]
						n = n.children[0]
						continue walk
					}

					// ... but we can't
					value.tsr = len(path) == end+1
					return
				}

				if value.handlers = n.handlers; value.handlers != nil {
					value.fullPath = n.fullPath
					return
				}
				if len(n.children) == 1 {
					// 没有找到处理函数. 检查此路径末尾加/的路由是否存在注册函数
					// 用于 TSR 推荐
					n = n.children[0]
					value.tsr = n.path == "/" && n.handlers != nil
				}
				return

			case catchAll:
				// 保存通配符的值
				if cap(value.params) < int(n.maxParams) {
					value.params = make(Params, 0, n.maxParams)
				}
				i := len(value.params)
				value.params = value.params[:i+1] // 在预先分配的容量内扩展slice
				value.params[i].Key = n.path[2:]
				if unescape {
					var err error
					if value.params[i].Value, err = url.QueryUnescape(path); err != nil {
						value.params[i].Value = path // fallback, in case of error
					}
				} else {
					value.params[i].Value = path
				}

				value.handlers = n.handlers
				value.fullPath = n.fullPath
				return

			default:
				panic("invalid node type")
			}
		}

		// 找不到,如果存在一个在当前路径最后添加/的路由
		// 我们会建议重定向到那里
		value.tsr = (path == "/") ||
			(len(prefix) == len(path)+1 && prefix[len(path)] == '/' &&
				path == prefix[:len(prefix)-1] && n.handlers != nil)
		return
	}
}

1.2 Gin框架中间件详解

gin框架涉及中间件相关有4个常用的方法,它们分别是c.Next()、c.Abort()、c.Set()、c.Get()。

中间件的注册

gin框架中的中间件设计很巧妙,我们可以首先从我们最常用的r := gin.Default()的Default函数开始看,它内部构造一个新的engine之后就通过Use()函数注册了Logger中间件和Recovery中间件:


func Default() *Engine {
	debugPrintWARNINGDefault()
	engine := New()
	engine.Use(Logger(), Recovery())  // 默认注册的两个中间件
	return engine
}

继续往下查看一下Use()函数的代码:

func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
	engine.RouterGroup.Use(middleware...)  // 实际上还是调用的RouterGroup的Use函数
	engine.rebuild404Handlers()
	engine.rebuild405Handlers()
	return engine
}

从下方的代码可以看出,注册中间件其实就是将中间件函数追加到group.Handlers中:

func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
	group.Handlers = append(group.Handlers, middleware...)
	return group.returnObj()
}

而我们注册路由时会将对应路由的函数和之前的中间件函数结合到一起:

func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
	absolutePath := group.calculateAbsolutePath(relativePath)
	handlers = group.combineHandlers(handlers)  // 将处理请求的函数与中间件函数结合
	group.engine.addRoute(httpMethod, absolutePath, handlers)
	return group.returnObj()
}

其中结合操作的函数内容如下,注意观察这里是如何实现拼接两个切片得到一个新切片的。

const abortIndex int8 = math.MaxInt8 / 2

func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {
	finalSize := len(group.Handlers) + len(handlers)
	if finalSize >= int(abortIndex) {  // 这里有一个最大限制
		panic("too many handlers")
	}
	mergedHandlers := make(HandlersChain, finalSize)
	copy(mergedHandlers, group.Handlers)
	copy(mergedHandlers[len(group.Handlers):], handlers)
	return mergedHandlers
}

也就是说,我们会将一个路由的中间件函数和处理函数结合到一起组成一条处理函数链条HandlersChain,而它本质上就是一个由HandlerFunc组成的切片:

type HandlersChain []HandlerFunc

请添加图片描述
请添加图片描述

中间件的执行

我们在上面路由匹配的时候见过如下逻辑:

value := root.getValue(rPath, c.Params, unescape)
if value.handlers != nil {
  c.handlers = value.handlers
  c.Params = value.params
  c.fullPath = value.fullPath
  c.Next()  // 执行函数链条
  c.writermem.WriteHeaderNow()
  return
}

其中c.Next()就是很关键的一步,它的代码很简单:

func (c *Context) Next() {
	c.index++
	for c.index < int8(len(c.handlers)) {
		c.handlers[c.index](c)
		c.index++
	}
}

从上面的代码可以看到,这里通过索引遍历HandlersChain链条,从而实现依次调用该路由的每一个函数(中间件或处理请求的函数)。

请添加图片描述
我们可以在中间件函数中通过再次调用c。Next()实现嵌套调用(funcl中调用func2;func2中调用func3),

请添加图片描述
或者通过调用c.Abort()中断整个调用链条,从当前函数返回。

func (c *Context) Abort() {
	c.index = abortIndex  // 直接将索引置为最大限制值,从而退出循环
}

c.Set()/c.Get()

c.Set()和c.Get()这两个方法多用于在多个函数之间通过c传递数据的,比如我们可以在认证中间件中获取当前请求的相关信息(userID等)通过c.Set()存入c,然后在后续处理业务逻辑的函数中通过c.Get()来获取当前请求的用户。c就像是一根绳子,将该次请求相关的所有的函数都串起来了
请添加图片描述

总结

  • 1、gin框架路由使用前缀树,路由注册的过程是构造前缀树的过程,路由匹配的过程就是查找前缀树的过程
  • 2、gin框架的中间件函数和处理函数是以切片形式的调用链条存在的,我们可以顺序调用也可以借助c.Next()方法实现嵌套调用
  • 3、借助c.Set()和c.Get()方法我们能够在不同的中间件函数中传递数据。

2 Go连接MySQL/Redis

Go语言中的 database/sql 包提供了保证SQL或类SQL数据库的泛用接口,并不提供具体的数据库驱动。使用 database/sql 包时必须注入至少一个数据库驱动。

我们常用的数据库基本上都有完整的第三方实现:例如:MySQL驱动https://github.com/go-sql-driver/mysql

2.1 database/sql及sqx使用

下载依赖

go get -u github.com/go-sql-driver/mysql

使用MySQL驱动

func Open(driverName, dataSourceName string)(*DB, error){}

Open打开一个dirverName指定的数据库,dataSourceName指定数据源,一般至少包括数据库文件名和其它连接必要的信息。

import (
	"database/sql"

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

func main() {
   // DSN:Data Source Name
	dsn := "user:password@tcp(127.0.0.1:3306)/dbname"
	db, err := sql.Open("mysql", dsn)
	if err != nil {
		panic(err)
	}
	defer db.Close()  // 注意这行代码要写在上面err判断的下面
}
思考题: 为什么上面代码中的defer db.Close()语句不应该写在if err != nil的前面呢?
假如defer db.Close()在其前边,那么假如输入的dsn是错误的,db将会直接返回一个nil,而对于defer db.Close()将会panic,就不会执行到错误处理。

初始化连接

Open函数可能只是验证其参数格式是否正确,实际上并不创建与数据库的连接。如果要检查数据源的名称是否真实有效,应该调用panic方法。
返回的DB对象可以安全地被多个goroutine并发使用,并且维护其自己的空闲连接池。因此,open函数应该仅仅被调用一次,很少需要关闭这个DB对象。
接下来,我们定义一个全局的变量 db,用来保存数据库连接对象。将上面的示例代码拆分出一个独立的initDB函数,只需要在程序启动时调用一次该函数完成全局变量db的初始化,其他函数就可以直接使用全局变量db 了。

// 定义一个全局对象db
var db *sql.DB

// 定义一个初始化数据库的函数
func initDB() (err error) {
	// DSN:Data Source Name
	dsn := "user:password@tcp(127.0.0.1:3306)/sql_test?charset=utf8mb4&parseTime=True"
	// 不会校验账号密码是否正确
	// 注意!!!这里不要使用:=,我们是给全局变量赋值,然后在main函数中使用全局变量db
	db, err = sql.Open("mysql", dsn)
	if err != nil {
		return err
	}
	// 尝试与数据库建立连接(校验dsn是否正确)
	err = db.Ping()
	if err != nil {
		return err
	}
	return nil
}

func main() {
	err := initDB() // 调用输出化数据库的函数
	if err != nil {
		fmt.Printf("init db failed,err:%v\n", err)
		return
	}
}

其中sql.DB是表示连接的数据库对象(结构体实例),它保存了连接数据库相关的所有信息。内部维护着一个具有零到多个底层连接的连接池,它可以安全地被多个goroutine同时使用。

SetMaxOpenConns

func (db *DB) SetMaxOpenConns(n int)
设置与数据库建立连接的最大数目。如果n大于0且小于最大闲置连接数,会将最大闲置连接数减小到匹配最大开启连接数的限制。如果n<=0,不会限制最大开启连接数,默认为0(无限制)。

SetMaxIdleConns

func(db *DB)SetMaxIdleConns(n int)
设置连接池中的最大限制连接数。如果n大于最大开启连接数,则新的最大闲置连接数会减小到匹配最大开启连接数的限制。如果n<=0,不会保留闲置连接。

请添加图片描述


2.2 go-redis库使用


3 搭建Go Web开发脚手架

3.1 zap日志库使用

3.2 Viperi配置管理库使用

3.3 优雅关机与平滑重启

3.4 CLD代码分层


4 bluebell项目实战(仿Reddit论坛项目)

4.1 分布式1D生成

4.2 JWT认证

4.3 基于MySQL实现主业务

4.4 基于Redis实现投票业务

4.5 基于Docker搭建开发环境

4.6 代码发布与项目部署

4.7 多到写不下的实战经验与技巧…

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

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

相关文章

Linux Tomcat服务 虚拟主机 多实例部署

Tomcat 服务 Tomcat 是 Java 语言开发的&#xff0c;Tomcat 服务器是一个免费的开放源代码的 Web 应用服务器。Tomcat 属于轻量级应用服务器&#xff0c;在中小型系统和并发访问用户不是很多的场合下被普遍使用&#xff0c;是开发和调试 java开发的JSP 动态页面程序的首选。一般…

Linux教程——常见Linux发行版本有哪些?

新手往往会被 Linux 众多的发行版本搞得一头雾水&#xff0c;我们首先来解释一下这个问题。 从技术上来说&#xff0c;李纳斯•托瓦兹开发的 Linux 只是一个内核。内核指的是一个提供设备驱动、文件系统、进程管理、网络通信等功能的系统软件&#xff0c;内核并不是一套完整的…

DDD领域模型

一、分层介绍 controller层&#xff1a;处理页面或者api请求的输入输出&#xff0c;定义VO(REQ,RES)&#xff0c;没有业务逻辑&#xff0c;只做请求处理和下层逻辑接application层&#xff1a;处理跨领域domain的复杂逻辑&#xff0c;定义DTOdomain层&#xff1a;领域核心逻辑…

深入理解Qt多线程编程:QThread、QTimer与QAudioOutput的内在联系__QObject的主线程的事件循环

深入理解Qt多线程编程&#xff1a;QThread、QTimer与QAudioOutput的内在联系__QObject的主线程的事件循环 1. Qt多线程编程的基础1.1 QObject和线程&#xff08;QObject and Threads&#xff09;1.2 QThread的使用和理解&#xff08;Understanding and Using QThread&#xff0…

C#,码海拾贝(35)——求“实对称矩阵““特征值与特征向量“的“雅可比法“之C#源代码

using System; namespace Zhou.CSharp.Algorithm { /// <summary> /// 矩阵类 /// 作者&#xff1a;周长发 /// 改进&#xff1a;深度混淆 /// https://blog.csdn.net/beijinghorn /// </summary> public partial class Matrix {…

编码器 | 基于 Transformers 的编码器-解码器模型

基于 transformer 的编码器-解码器模型是 表征学习 和 模型架构 这两个领域多年研究成果的结晶。本文简要介绍了神经编码器-解码器模型的历史&#xff0c;更多背景知识&#xff0c;建议读者阅读由 Sebastion Ruder 撰写的这篇精彩 博文。此外&#xff0c;建议读者对 自注意力 (…

【AUTOSAR】Bootloader说明(一)---- 时序流程

电机控制器选用TI TMS28xx DSP&#xff0c;包括boot-loader与应用软件两个部分。其中boot-loader包括下列内容&#xff1a; RAM自检应用程序有效性检查UDS命令处理FLASH操作 下面分别说明DSP上电后整个软件运行流程及程序刷新过程。 DSP软件执行流程 DSP复位后&#xff0c;将…

【Mysql基础】-关于常用的函数简单案例

目录 一、系统函数 二、日期函数 三、字符串函数数 说明&#xff1a;以下所有的操作在8.0的mysql数据库操作系统上操作 一、系统函数 1 显示连接列表&#xff1a;show PROCESSLIST; 2 MD5加密&#xff1a;select MD5("root") 二、日期函数 1、 推算一周之后的…

QMI8658 - 姿态传感的零偏(常值零偏)标定

1. 零偏 理论上在静止状态下三轴输出为0,0,0&#xff0c;但实际上输出有一个小的偏置&#xff0c;这是零偏的静态分量&#xff08;也称固定零偏&#xff09;。 陀螺生产出来后就一直固定不变的零偏值。对于传统的高性能惯性器件来说&#xff0c;该误差在出厂标定时往往就被补偿…

《水经注地图服务》用户如何登录?

《水经注地图服务》&#xff08;WeServer&#xff09;是一款可快速发布全国乃至全球海量卫星影像的地图发布服务产品&#xff0c;该产品完全遵循OGC相关协议标准&#xff0c;是一个基于若干项目成功经验总结的产品。它可以轻松发布100TB级海量卫星影像&#xff0c;从而使“在内…

如何使用 Raycast 一键打开预设工作环境

工作中&#xff0c;你一定遇到过这样的场景&#xff1a;你正在认真写代码&#xff0c;线上突然出现报警。看到报警信息之后&#xff0c;你不得不打开浏览器&#xff0c;点开收藏夹&#xff0c;打开监控页面、告警页面、trace 页面、日志搜索平台……有时&#xff0c;还需要打开…

chatgpt赋能python:Python取值:了解基础知识和应用方法

Python取值&#xff1a;了解基础知识和应用方法 什么是Python取值&#xff1f; Python取值是指从一个对象中获取信息或者值。对象可以包括列表、字典、元组、变量等。Python提供了多种方法来取值&#xff0c;包括基础的索引和切片操作&#xff0c;以及高级的列表推导式、字典…

MySQL JDBC详解

文章目录 简介JDBC APIJDBC Driver ManagerJDBC 驱动 JDBC 开发步骤一&#xff0c;导入 JDBC 驱动包&#xff0c;并加载驱动类二&#xff0c;建立数据库连接三&#xff0c;发送 SQL 语句&#xff0c;并获取执行结果Statement 对象PreparedStatement 对象 四&#xff0c;处理返回…

ADAS方案的简单比较

ADAS方案的简单比较 1 概述2 厂商Tesla硬件布局网络基础结构&#xff1a;HydraNet多头网络 NVIDIA百度&#xff08;Apollo&#xff09;版本历史硬件布局软件框架各版本框架 WaymoVolvo-Uber 3 芯片4 其他from [最全自动驾驶技术架构和综述](https://blog.csdn.net/buptgshengod…

项目质量管理

质量与项目质量 质量的定义&#xff1a;一组固有特征满足要求的程序。 质量是反应实体主题明确和隐含需求的能力的特性总和 质量与等级的关系&#xff1a; 一个低等级&#xff08;功能有限&#xff09;&#xff0c;高质量&#xff08;无明显缺陷&#xff0c;用户手册易读&am…

《Datawhale南瓜书》出第二版啦!

Datawhale干货 作者&#xff1a;Datawhale开源项目团队 作为机器学习的入门经典教材&#xff0c;周志华老师的《机器学习》&#xff0c;自2016年1月底出版以来&#xff0c;首印5000册一周售罄&#xff0c;并在8个月内重印9次。先后登上了亚马逊&#xff0c;京东&#xff0c;当…

【运维知识进阶篇】iptables防火墙详解

这篇文章给大家介绍下iptables防火墙&#xff0c;防火墙大致分三种&#xff0c;分别是硬件、软件和云防火墙。硬件的话部署在企业网络的入口&#xff0c;有三层路由的H3C、华为、Cisco&#xff08;思科&#xff09;&#xff0c;还有深信服等等&#xff1b;软件的话一般是开源软…

【服务器】iPad远程服务器进行开发

文章目录 前言1. 本地环境配置2. 内网穿透2.1 安装cpolar内网穿透(支持一键自动安装脚本)2.2 创建HTTP隧道 3. 测试远程访问4. 配置固定二级子域名4.1 保留二级子域名4.2 配置二级子域名 5. 测试使用固定二级子域名远程访问6. iPad通过软件远程vscode6.1 创建TCP隧道 7. ipad远…

人工智能 AI | ChatGPT 时代,程序员的生存之道

ChatGPT 近期炙手可热&#xff0c;仿佛没有什么问题是它不能解决的。出于对 ChatGPT 的好奇&#xff0c;我们决定探索下它对于前端开发人员来讲&#xff0c;是作为辅助工具多一些&#xff0c;还是主力工具更多一些&#xff1f; 2D 能力测试 我们就挑选一个著名的递归回溯问题—…

代码随想录算法训练营第三十九天|62.不同路径|63. 不同路径 II

LeetCode62.不同路径 动态规划五部曲&#xff1a; 1&#xff0c;确定dp数组&#xff08;dp table&#xff09;以及下标的含义&#xff1a;dp[i][j] &#xff1a;表示从&#xff08;0 &#xff0c;0&#xff09;出发&#xff0c;到(i, j) 有dp[i][j]条不同的路径。 2&#xff0c…