LOGO
首页 网站广场 站长动态 活跃度榜 审核查询 逛逛好站 留言交流 提交申请 关于本站

站长动态

站长动态所展示的是已加入好站网成员站长文章
共同步 2387 篇博文
(每2小时更新一次)
Debug
入驻第1年
Restful API 设计指北
近期学习了 Go 语言,跟着七米在学习,学习过程中了解到了 API 的一个设计规范,也就是本文要讲的 Restful API 设计模式,现在互联网处在前后端分离的阶段,API 的书写及规范化是非常重要的,针对于 API 中 Restful API 中设计比较规范的是 Github API,可以直接访问他们的 https://api.github.com 直接查看 Github 针对与公共接口的链接及使用方法。 此篇文章也是针对于这几天学习 Restful API 做了一个笔记或小结,若有不足之处还望批评指正,谢谢。 使用 HTTPS 协议 这个协议使用本身与这个 API 设计标准没有什么直接联系,使用 HTTPS 协议主要目的是将用户客户端与 API 服务器连接过程中保证其数据的安全性。 注意:由于 API 接口使用 HTTPS 协议,不要让非 SSL 的链接访问重定向到 SSL 的链接。 API 地址和版本问题 为 API 使用专门子域名比较友好,例如使用如下链接使用: 1 https://api.debuginn.com 也可以将 API 放在主域名下,例如: 1 https://debuginn.com/api/ 当然,针对于 API 版本问题针对以上两种方法可以分别使用如下例子: 1 2 3 4 # 针对于 API 子域名方式 api.domain/v1/ https://api.debuginn.com/v1/ # 针对于 主域名目录方式 domain/api/v1/ https://debuginn.com/api/v1/ Schema 响应数据模式 现在前后端分离项目使用的数据响应模式大部分采用的是 JSON 格式数据,也有一些项目采用 XML 格式的数据。 针对于用户客户端请求,服务器响应尽量有 状态码 Status Code 及详细解释。 使用正确的 Method 使用正确的 Method 也就是使用正确的 HTTP 请求动词,即 HTTP 协议规定的常常使用的六种请求动词,并针对请求 SQL 语句辅助理解: 1 2 3 4 5 GET 请求 => SELECT 从服务端获取数据 POST 请求 => CREATE 从服务端创建数据 PUT 请求 => UPDATE 从服务端更新数据(将所有数据元素全部替换掉) PATCH 请求 => UPDATE 从服务端更新数据(将部分数据元素替换掉) DELETE请求 => DELETE 从服务端删除数据 还有两个不常使用的请求: 1 2 HEAD 获取资源的元数据。 OPTIONS 获取信息,关于资源的哪些属性是客户端可以改变的。 注意:更新和创建操作应该返回最新的资源,来通知用户资源的情况;删除资源一般不会返回内容。 过滤信息 针对用户端查询数据,需要服务端查询对应数据,包括了筛选、分页等操作。 筛选操作 1 2 3 4 5 api.domain/user/limit/10 指定返回记录的数量; api.domain/user/offset/10 指定返回记录的开始位置; api.domain/user/animal_type_id/1 指定筛选条件 api.domain/user/page/2/per_page/100 指定第几页,以及每页的记录数; api.domain/user/sortby/name/order/asc 指定返回结果按照哪个属性排序,以及排序顺序 分页操作 当返回某个资源的列表时,如果要返回的数目特别多,比如 github 的 /users,就需要使用分页分批次按照需要来返回特定数量的结果。 分页的实现会用到上面提到的 url query,通过两个参数来控制要返回的资源结果: per_page:每页返回多少资源,如果没提供会使用预设的默认值;这个数量也是有一个最大值,不然用户把它设置成一个非常大的值(比如99999999)也失去了设计的初衷。 page:要获取哪一页的资源,默认是第一页。 状态码 Status Code HTTP 应答中,需要带一个很重要的字段:status code。它说明了请求的大致情况,是否正常完成、需要进一步处理、出现了什么错误,对于客户端非常重要。状态码都是三位的整数,大概分成了几个区间: 2XX:请求正常处理并返回 3XX:重定向,请求的资源位置发生变化 4XX:客户端发送的请求有错误 5XX:服务器端错误 在 HTTP API 设计中,经常用到的状态码以及它们的意义如下表: 状态码 LABEL 解释 200 OK 请求成功接收并处理,一般响应中都会有 body 201 Created 请求已完成,并导致了一个或者多个资源被创建,最常用在 POST 创建资源的时候 202 Accepted 请求已经接收并开始处理,但是处理还没有完成。一般用在异步处理的情况,响应 body 中应该告诉客户端去哪里查看任务的状态 204 No Content 请求已经处理完成,但是没有信息要返回,经常用在 PUT 更新资源的时候(客户端提供资源的所有属性,因此不需要服务端返回)。如果有重要的 metadata,可以放到头部返回 301 Moved Permanently 请求的资源已经永久性地移动到另外一个地方,后续所有的请求都应该直接访问新地址。服务端会把新地址写在 Location 头部字段,方便客户端使用。允许客户端把 POST 请求修改为 GET。 304 Not Modified 请求的资源和之前的版本一样,没有发生改变。用来缓存资源,和条件性请求(conditional request)一起出现 307 Temporary Redirect 目标资源暂时性地移动到新的地址,客户端需要去新地址进行操作,但是不能修改请求的方法。 308 Permanent Redirect 和 301 类似,除了客户端不能修改原请求的方法 400 Bad Request 客户端发送的请求有错误(请求语法错误,body 数据格式有误,body 缺少必须的字段等),导致服务端无法处理 403 Forbidden 服务器端接收到并理解客户端的请求,但是客户端的权限不足。比如,普通用户想操作只有管理员才有权限的资源。 404 Not Found 客户端要访问的资源不存在,链接失效或者客户端伪造 URL 的时候会遇到这个情况 405 Method Not Allowed 服务端接收到了请求,而且要访问的资源也存在,但是不支持对应的方法。服务端必须返回Allow头部,告诉客户端哪些方法是允许的 415 Unsupported Media Type 服务端不支持客户端请求的资源格式,一般是因为客户端在Content-Type或者Content-Encoding中申明了希望的返回格式,但是服务端没有实现。比如,客户端希望收到xml返回,但是服务端支持Json 429 Too Many Requests 客户端在规定的时间里发送了太多请求,在进行限流的时候会用到 500 Internal Server Error 服务器内部错误,导致无法完成请求的内容 503 Service Unavailable 服务器因为负载过高或者维护,暂时无法提供服务。服务器端应该返回 Retry-After 头部,告诉客户端过一段时间再来重试 针对于状态码,请看此文章:HTTP常见状态码 错误处理 如果出错的话,在 response body 中通过 message 给出明确的信息。 比如客户端发送的请求有错误,一般会返回4XX Bad Request结果。这个结果很模糊,给出错误 message 的话,能更好地让客户端知道具体哪里有问题,进行快速修改。 如果请求的 JSON 数据无法解析,会返回Problems parsing JSON; 如果缺少必要的 filed,会返回422 Unprocessable Entity,除了 message 之外,还通过errors给出了哪些 field 缺少了,能够方便调用方快速排错。 基本的思路就是尽可能提供更准确的错误信息:比如数据不是正确的 json,缺少必要的字段,字段的值不符合规定…… 而不是直接说“请求错误”之类的信息。 验证及授权 一般来说,让任何人随意访问公开的 API 是不好的做法。验证和授权是两件事情: 验证(Authentication)是为了确定用户是其声明的身份,比如提供账户的密码。不然的话,任何人伪造成其他身份(比如其他用户或者管理员)是非常危险的 授权(Authorization)是为了保证用户有对请求资源特定操作的权限。比如用户的私人信息只能自己能访问,其他人无法看到;有些特殊的操作只能管理员可以操作,其他用户有只读的权限等等 如果没有通过验证(提供的用户名和密码不匹配,token 不正确等),需要返回401 Unauthorized状态码,并在 body 中说明具体的错误信息;而没有被授权访问的资源操作,需要返回403 Forbidden状态码,还有详细的错误信息。 注意:对某些用户未被授权访问的资源操作返回404 Not Found,目的是为了防止私有资源的泄露(比如黑客可以自动化试探用户的私有资源,返回 403 的话,就等于告诉黑客用户有这些私有的资源,无异于是给黑客提供了方向)。 Hypermedia API RESTful API 最好做到 Hypermedia,即返回结果中提供链接,连向其他API方法,使得用户不查文档,也知道下一步应该做什么。 比如访问 api.github.com,就可以看到 Github API 支持的资源操作。 易读的 API 接口文档 API 最终是给人使用的,不管是公司内部,还是公开的 API 都是一样。即使我们遵循了上面提到的所有规范,设计的 API 非常优雅,用户还是不知道怎么使用我们的 API。最后一步,但非常重要的一步是:为你的 API 编写优秀的文档。 注意:对每个请求以及返回的参数给出说明,最好给出一个详细而完整的示例。 参考资料 RESTful API 设计指南 - 阮一峰 跟着 Github 学习 Restful HTTP API 设计 REST API Tutorial Representational State Transfer (REST) - Roy Fielding 关注微信公众号,第一时间获取最新内容,让我们一起变得更强!Debug客栈:订阅本站· 文章归档· 我的项目· 友情链接· 我的使用· 飞湾计划· 摄影展集· 我的主页
Debug
入驻第1年
如何将豆瓣观影记录实时同步至博客中
事情的起因是这样的,前几日在看 idealclover 大佬的博客,不经意间看到了他的豆瓣观影记录,他博客中关于豆瓣观影记录是实时同步的,很好奇是如何实现的,经过查看,他是爬取的豆瓣观影界面来实现的,其实关于豆瓣观影记录,网上也有很多的教程,恰巧自己所学的 Go 语言也可以做简单的爬虫实现其效果,于是开始上手造轮子了,PS:了解到非法爬取网站信息是违法的,之前豆瓣 API 接口,关闭访问,在豆瓣上找了好久,终于在我的主页中找到了对于观影记录的官方提供 RSS 订阅,打开订阅,看到有自己所需要的字段,比较好获取,于是就开始了此项目。 分析 首先,需要获取豆瓣提供的 XML 文件,在我的主页右下角就可以看到 RSS 订阅链接: 找到了订阅地址,点击查看 XML 结构,可以看到豆瓣提供的结构还是挺理想的: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <rss xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" version="2.0"> <channel> <title>Meng小羽 的收藏</title> <link>https://www.douban.com/people/debuginn/</link> <description> <![CDATA[ Meng小羽 的收藏:想看、在看和看过的书和电影,想听、在听和听过的音乐 ]]> </description> <language>zh-cn</language> <copyright>© 2013, douban.com.</copyright> <pubDate>Sat, 30 May 2020 09:14:08 GMT</pubDate> <item> <title>看过黑衣人:全球追缉</title> <link>http://movie.douban.com/subject/19971676/</link> <description> <![CDATA[ <table><tr> <td width="80px"><a href="https://movie.douban.com/subject/19971676/" title="Men in Black International"> <img src="https://img1.doubanio.com/view/photo/s_ratio_poster/public/p2558701068.webp" alt="Men in Black International"></a></td> <td> <p>推荐: 力荐</p> </td></tr></table> ]]> </description> <dc:creator>Meng小羽</dc:creator> <pubDate>Sat, 30 May 2020 09:14:08 GMT</pubDate> <guid isPermaLink="false">https://www.douban.com/people/debuginn/interests/2402808825</guid> </item> ...... <channel> 其实,我们提取的主要就是 item 标签下对应的电影信息内容: 1 2 3 4 5 6 7 8 9 10 <item> <title>看过黑衣人:全球追缉</title> <link>http://movie.douban.com/subject/19971676/</link> <description> <![CDATA[ <table><tr> <td width="80px"><a href="https://movie.douban.com/subject/19971676/" title="Men in Black International"> <img src="https://img1.doubanio.com/view/photo/s_ratio_poster/public/p2558701068.webp" alt="Men in Black International"></a></td> <td> <p>推荐: 力荐</p> </td></tr></table> ]]> </description> <dc:creator>Meng小羽</dc:creator> <pubDate>Sat, 30 May 2020 09:14:08 GMT</pubDate> <guid isPermaLink="false">https://www.douban.com/people/debuginn/interests/2402808825</guid> </item> 设计 根据豆瓣官方提供的 XML 标签数据,可以利用 Go 语言中 encoding/xml 包来进行对数据的映射,可以设计成如下结构体: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 // 豆瓣 xml 描述结构体 type Attributes struct { XMLName xml.Name `xml:"rss"` Version string `xml:"version,attr"` Channel Channel `xml:"channel"` } // XML 主题结构拆分 type Channel struct { Title string `xml:"title"` Link string `xml:"link"` Description string `xml:"description"` Language string `xml:"language"` Copyright string `xml:"copyright"` Pubdate string `xml:"pubDate"` MovieItem []MovieItem `xml:"item"` } // 豆瓣 电影列表结构体 type MovieItem struct { Title string `xml:"title"` Link string `xml:"link"` Description string `xml:"description"` Pubdate string `xml:"pubDate"` } 可以和 XML 文件对应字段进行匹配,可以从上面的结构体中我们可以看到,最终我们想获取到的数据就是结构体 MovieItem 的数据。 由于是从网上链接获取数据的,在这里首先我们需要将网上豆瓣提供的 XML 文件转换成 []byte 类型的数据: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 // 获取 xml 文件数据 func getXMLData(url string) (data []byte, err error) { // 读取 xml 文件 client := &http.Client{} req, _ := http.NewRequest("GET", url, nil) // 自定义Header req.Header.Set("User-Agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)") resp, err := client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() // 关闭文件 // 读取所有文件内容保存至 []byte data, err = ioutil.ReadAll(resp.Body) if err != nil { return nil, err } return } 上面这个函数实现的就是将 XML 文件保存至 Go 语言的数据结构的操作,现在可以将 XML 文件成功读取出来,接下来就是要进行 XML 字段与上面作出的结构体之间的映射,其实映射至结构体的过程是比较简单的,首先声明 Attributes{} 类型的结构体,之后通过 xml.Unmarshal 来实现映射拷贝,就可以得到对应的结构体类型的数据,由于我们想要的数据是结构体数据中的一部分,即 MovieItem,在得到结构体数据后就可以将想要的这一部分的数据选择抽取出来: 1 2 3 4 5 6 7 v := Attributes{} unMarshalErr := xml.Unmarshal(data, &v) if unMarshalErr != nil { fmt.Printf("xml unmarshal failed, err:%v\n", err) } movieItem := v.Channel.MovieItem Map 转换 在这里我们可以得到结构体中嵌套的结构体,在结构体中有一些字段我们是不想要的,需要进行处理,对于 description 这个字段中,官方提供的是一段 HTML 描述串,其中电影的描述文件是我们所需要的,对于 HTML 字符串的拆分,我们可以借助strings.Split 函数来实现截取,使用 \" 符号截取,虽然可以获取到我们想要的数据了,但是由于这个是嵌套的结构体,我们需要做一个匹配的 map 来进行存储处理好的数据,可以看代码中我的设计: 1 2 3 4 5 6 7 8 9 10 11 MoviesMap := make(map[int]interface{}) for i := 0; i < len(movieItem); i++ { movie := make(map[string]string) description := strings.Split(movieItem[i].Description, "\"") movie["Title"] = string([]rune(movieItem[i].Title)[2:]) movie["Link"] = movieItem[i].Link movie["Img"] = description[7] movie["Pubdate"] = movieItem[i].Pubdate MoviesMap[i] = movie } 外层 map 是采用 map[int]interface{} 类型,在 interface{} 中存储这内层 map map[string]string 类型。 针对于 Img 地址的获取,是现根据特定符号拆分,之后获取制定位置的数据获取的。 1 2 3 0 map[Img:https://img1.doubanio.com/view/photo/s_ratio_poster/public/p2558701068.jpg Link:http://movie.douban.com/subject/19971676/ Pubdate:Sat, 30 May 2020 09:14:08 GMT Title:黑衣人:全球追缉] 1 map[Img:https://img1.doubanio.com/view/photo/s_ratio_poster/public/p2263408369.jpg Link:http://movie.douban.com/subject/1294371/ Pubdate:Thu, 28 May 2020 10:06:23 GMT Title:摩登时代] ...... 最后就是将这个 map 做一下序列化处理,这样就可以返回给前台数据了。 1 data, _ = json.Marshal(MoviesMap) 服务 处理好数据,做了对应的处理,怎么将数据作为服务端提供给前台,在这里需要使用 Web 服务,Go 中可以使用原生 Web,不过我在这里使用的是之前学过的 Gin 框架,来提供服务的: 1 2 3 4 5 r := gin.Default() r.GET("/doubanmovies", func(context *gin.Context) { context.JSON(http.StatusOK, MoviesMap) }) _ = r.Run(":8080") 启动服务,可以得到对应的 json 数据,你若以为现在就可以实现了,那么你错了,远远没有那么简单…… 前台 由于我知晓我的博客采用的前台 UI 技术是 MDUI, 我利用自身的卡片 UI 迅速设计了一个模块,因为后期需要放在我的博客页面上,前端读取数据采用的是 VUE 和 axios 技术: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <div class="mdui-container-fluid" id="app"> <div class="mdui-row"> <div v-for="item in info"> <div class="mdui-col-xs-6 mdui-col-sm-4 mdui-col-md-3 mdui-col-lg-3 mdui-m-b-1 mdui-m-t-1"> <a :href="item.Link" target="_blank"> <div class="mdui-card mdui-hoverable"> <div class="mdui-card-media"> <img :src="item.Img" style="height: 260px;" /> <div class="mdui-card-media-covered"> <div class="mdui-card-primary"> <div class="mdui-card-primary-subtitle">{{ item.Title }}</div> </div> </div> </div> </div> </a> </div> </div> </div> </div> 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <script src="./static/js/mdui.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.1.8/vue.min.js"></script> <script src="https://cdn.staticfile.org/axios/0.18.0/axios.min.js"></script> <script> new Vue({ el: '#app', data() { return { info: null } }, mounted() { axios .get('http://127.0.0.1:8080/doubanmovies') .then(response => (this.info = response.data)) .catch(function (error) { // 请求失败处理 console.log(error); }); } }) </script> 设计好了以后,访问页面,却加载不出来,emmmmmm CORS 看到了是 CORS 同源策略的原因,接下来就是要解决同源问题了,方法比较简单,就是将 Go 服务端加上 CORS 同源策略就可以了,方法如下: 1 2 3 4 5 6 7 r := gin.Default() r.Use(Cors()) r.GET("/doubanmovies", func(context *gin.Context) { context.JSON(http.StatusOK, MoviesMap) }) _ = r.Run(":8080") 在路由访问中添加 Cors() 函数: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 // 跨域 func Cors() gin.HandlerFunc { return func(c *gin.Context) { method := c.Request.Method //请求方法 origin := c.Request.Header.Get("Origin") //请求头部 var headerKeys []string // 声明请求头keys for k, _ := range c.Request.Header { headerKeys = append(headerKeys, k) } headerStr := strings.Join(headerKeys, ", ") if headerStr != "" { headerStr = fmt.Sprintf("access-control-allow-origin, access-control-allow-headers, %s", headerStr) } else { headerStr = "access-control-allow-origin, access-control-allow-headers" } if origin != "" { c.Writer.Header().Set("Access-Control-Allow-Origin", "*") c.Header("Access-Control-Allow-Origin", "*") // 这是允许访问所有域 c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE,UPDATE") //服务器支持的所有跨域请求的方法,为了避免浏览次请求的多次'预检'请求 // header的类型 c.Header("Access-Control-Allow-Headers", "Authorization, Content-Length, X-CSRF-Token, Token,session,X_Requested_With,Accept, Origin, Host, Connection, Accept-Encoding, Accept-Language,DNT, X-CustomHeader, Keep-Alive, User-Agent, X-Requested-With, If-Modified-Since, Cache-Control, Content-Type, Pragma") // 允许跨域设置 可以返回其他子段 c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers,Cache-Control,Content-Language,Content-Type,Expires,Last-Modified,Pragma,FooBar") // 跨域关键设置 让浏览器可以解析 c.Header("Access-Control-Max-Age", "172800") // 缓存请求信息 单位为秒 c.Header("Access-Control-Allow-Credentials", "false") // 跨域请求是否需要带cookie信息 默认设置为true c.Set("content-type", "application/json") // 设置返回格式是json } //放行所有OPTIONS方法 if method == "OPTIONS" { c.JSON(http.StatusOK, "Options Request!") } // 处理请求 c.Next() // 处理请求 } } 这样就可以看到结果了,如下图: 看到结果后,心中窃喜,感觉成功了,接下来就需要将 Go 服务部署到我的服务器中去了,部署步骤比较简单,就不过多解释了,最后访问服务器 IP 及对应单口可以呈现结果,最后将前台代码粘贴到新建的页面中,生成预览,emmmm,啥都没有,浏览器居然报 HTTPS 请求 HTTP 资源是不安全的,吐了一口血,解决吧,唉,经过查询资料,得出如下两个解决方案: Gin 框架服务本身使用 SSL 证书,实现 HTTPS 访问,不过需要配置域名; 使用 Nginx 服务做一下代理,将一个特定链接代理到本身服务中去。 作为学生党的我,没有太多的资金去申请过多的 SSL 证书(省着点用),于是我就在我的 debuginn.com 子域名下做了一个代理。 代理 Nginx 代理实现也是比较简单的,就是将前端访问某个接口代理至服务器中某个端口的服务中,表面上看是 Nginx 在做数据处理,实际上是 Nginx 只做了一个代理转发,由于我www.debuginn.com 子域名本身就是 https 的,所以设置好了代理之后,就可以使用固定的代理链接访问了,配置如下: 1 2 3 4 5 6 7 8 9 server{ ..... location /douban_movies { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host:80; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } } 这样就可以实现 https 资源访问了:https://debuginn.com/doubanmovies 效果 解决了 HTTPS 访问 HTTP 资源的问题,就解决了所有问题,实现了效果。 具体效果如下:https://debuginn.com/doubanmovies 开源 针对于此小项目,我已经开源至 Github 中,若是你感兴趣或者有什么建议,可以联系我,我们一起改进,同时希望你可以给我一个 Star,万分感谢! https://github.com/debuginn/douban-movies 关注微信公众号,第一时间获取最新内容,让我们一起变得更强!Debug客栈:订阅本站· 文章归档· 我的项目· 友情链接· 我的使用· 飞湾计划· 摄影展集· 我的主页
Debug
入驻第1年
2021 年度总结
今年,时间依旧飞快流逝,转眼间,自己已经毕业了小2年了,渐渐的自己开始习惯了北漂的生活,一个人的北京,自由与孤独同在。也体会到了离家远的遗憾,远的连奶奶最后一面也没有见到,第一次感受到了亲人阴阳两隔的无奈与悲哀。疫情 😷 在全球范围内还在持续,国内偶尔也零星的出现,但愿 22 年疫情结束,全世界人们都可以回归到疫情之前,和爱人、亲人、朋友去想去的地方,去探索多彩的世界。 网站数据 2021年统计数据共享链接 今年,写了一些在工作中使用到的 Golang 的一些技巧及思考,以及一些规范。 让我意外的是大家对 Go 语言入门学习有着很大的兴趣,下面这个文章是今年访问最多的文章,访问量:2785 https://blog.debuginn.com/p/go-dev-design/ 技术导航 经常游荡于各个大厂的技术博客之中,于是做了一个集合导航,后续计划将大佬们也收集到此,大家有好的技术分享网站也可以评论区留言分享一下。 关注微信公众号,第一时间获取最新内容,让我们一起变得更强!Debug客栈:订阅本站· 文章归档· 我的项目· 友情链接· 我的使用· 飞湾计划· 摄影展集· 我的主页
Debug
入驻第1年
我们是如何用 Prometheus 对网关进行监控的
近期,我们对 APP 网关 Gateway 做了升级,由于项目创建时间过早(6年前的项目),那时候还没有好的包管理工具,使用的是最原始的 Go Path 来进行项目的依赖管理,历史包袱比较重,项目中很多的第三方引用都是直接将代码拷贝到项目目录下,升级与维护起来特别麻烦,升级之后就是现在官方主推的是 Go module 包管理方式。 解决了上面的这个痛点,网关程序就可以集成一些业界主流的基础工具,升级与维护起来就简单多了。 言归正传,本文主要是讲的我们是如何用 Prometheus 对网关进行监控的,之前我们的网关程序也是集成了我们公司开源打点监控工具 Open falcon,并且使用 Grafana 进行绘图并查看,但是为啥我们不再继续使用了?之后我们为啥拥抱了 Prometheus 生态?还有一些打点、报警、绘图的思考,还有一些我们在使用的过程中出现的问题以及解决方案,一一讲解一下。 抛弃 Open falcon 拥抱 Prometheus 在决定使用 Prometheus 之前,我们的 Gateway 使用的是 Open falcon,但是一直存在着一个对于我们而言的痛点,就是作为网关程序,历史维护的路由太多了,接口可用性及接口报错无法聚合报警,也就是我们的监控体系存在着盲区,这个对我们而言来说是最为致命的,那个接口出现了问题会直接导致用户的使用,并且我们使用的那些上游服务出现问题我们也无法及时感知。 使用 Prometheus 最主要的是我们可以通过 PromQL 语法进行正则匹配,实现对某个或多个接口的聚合计算并报警,这样就可以解决我们无法聚合报警的一个痛点。 打点、绘图、报警 打点 全面、量小 作为业务使用,怎么设计点位,既可以满足报警使用,对每个接口进行各项指标的监控,同时要保证点位数据是可穷举的(避免出现 OOM)和产生数据量比较小。简而言之,就是“监控要全面、打点数据量要小”,因为数据量大的话在 Prometheus 拉取指标的时间及周期就不得不设置的过大,这样的后果就是造成图的绘制缓慢甚至超时,同时报警也失去了实效性。 我们网关使用的是 http 协议,可以充分利用 Go 的 net/http 特性,使用中间件设计,对请求与返回进行打点,于是我们是这样设计的: 对任意一个请求做一个 qps 的打点记录(无任何的业务参与其中); 对单个路由请求进行打点(区分业务状态码); 对单个路由请求进行耗时打点(区分业务状态码)。 请求路由 按照业界通用的设计:/version/model/action 以上的场景,仅仅使用指标类型中的两种 Counter(计数器) 和 Histogram(直方图)就可以满足我们打点需求。 绘图 清晰、快速 构建一栋房子所需的材料都准备好了,准备建造, building…… 点位指标收集到了,接下来就是对点位进行各个维度的拼装,来呈现我们想要的图,这里解答一下为什么我们要把业务状态码打到指标中去,以及我们是如何使用的:我们的系统设计采用业务封装错误码,只要是传输调用链路没有问题,所有的场景都走业务状态码,类似的返回解决如下: 1 2 3 4 5 6 7 { "code": 0, "desc": "success", "data":{ "result": "ok" } } code 为 0,代表当前请求是正常的,返回数据会封装在 data 中; code 不为 0,代表着当前请求存在业务上可捕获或者自定义的错误。 作为网关程序,与下游微服务采用相同的接口设计,对我们现在的打点设计也是非常友好的。 同样的,有的服务使用的是 Restful API 思想,使用的是 http 标准状态码,那就是 200 代表着成功,非 200 代表着业务或者系统存在错误,当然 5XX 错误可以单独拿出来做可用性或者细化的报警。 之所以打点记录业务状态码,好处如下: 对业务状态码打点,可以对某个业务上的特定错误进行捕捉,看图及报警都是非常便捷的; 不影响对接口可用性进行计算,可以多维度聚合计算可用性(根据业务定义而言)。 当然,打点指标设置的粒度越小,对应的点位的存储大小以及聚合运算的代价也是成倍的提高的。 铺垫了好久,说一下我们是怎么进行绘图的,在打点的时候讲到使用 Counter、Histogram 进行打点,绘图的时候我们主要从以下三点进行可视化: 接口的 qps 看图呈现; 接口可用性(Pxx)看图呈现; 接口请求PXX 耗时统计 看图呈现。 接口 qps 看图绘图 qps 的点位数据怎么打?就是充分利用中间件的设计,在一个请求 prepare 阶段就将该路由记录并获取进行打点。 使用 PromQL 语句就可以实现对对应信息看图的绘制。 1 2 3 4 5 6 // 过去1分钟 每秒请求 qps // sum 求和函数 // rate 计算范围向量中时间序列的每秒平均增长率 // api_request_alert_counter 指标名称 // service_name 和 subject 都是 label kv参数 sum(rate(api_request_alert_counter{service_name="gateway", subject="total"}[1m])) by (subject) 接口可用性看图绘图 接口可用性就是验证当前接口在单位时间内的处理正确的请求数目比上总体的请求数目,在打点的时候也讲到,我们业务代码 0 代表着正确返回,非 0 的代表着存在问题,这样就可以很简单的算出来接口的可用性。 1 2 3 4 5 6 7 8 9 10 11 12 // 过去1分钟 每秒接口可用性 // sum 求和函数 // rate 计算范围向量中时间序列的每秒平均增长率 // api_request_cost_status_count 指标名称 // service_name 和 code 都是 label kv参数 (sum(rate(api_request_cost_status_count{service_name="gateway", code="0"}[1m])) by (handler) / ( sum(rate(api_request_cost_status_count{service_name="gateway", code="0"}[1m])) by (handler) + sum(rate(api_request_cost_status_count{service_name="gateway", code!="0"}[1m])) by (handler)) ) * 100.0 接口 Pxx 耗时统计看图绘图 接口耗时统计打点依赖 prometheus api 中的 histogram 实现,在呈现打点耗时的时候有时候局部的某个耗时过长并不能进行直接反应整体的,我们只需要关注 SLO (服务级别目标)目标下是否达标即可。 1 2 3 4 // 过去1分钟 95% 请求最大耗时统计 // histogram_quantile 1000* histogram_quantile(0.95, sum(rate(api_request_cost_status_bucket{service_name="gateway",handler=~"v1.app.+"}[1m])) by (handler, le)) histogram_quantile(φ float, b instant-vector) 从 bucket 类型的向量 b 中计算 φ (0 ≤ φ ≤ 1) 分位数(百分位数的一般形式)的样本的最大值。(有关 φ 分位数的详细说明以及直方图指标类型的使用,请参阅直方图和摘要)。向量 b 中的样本是每个 bucket 的采样点数量。每个样本的 labels 中必须要有 le 这个 label 来表示每个 bucket 的上边界,没有 le 标签的样本会被忽略。直方图指标类型自动提供带有 _bucket 后缀和相应标签的时间序列。 上面是官方对于 histogram_quantile 函数的解释,关注的是 设置 φ 分位数 对应的 bucket 桶,但是实际中有 分位数计算误差的问题。 Prometheus 官方 histogram 设置的默认 buckets 如下: 1 DefBuckets = []float64{.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10} 这里可以看到我们的接口指标分界时间,每一个请求的耗时都会根据具体的设置的 bucket 的范围落到不同的区间内,这里设置的桶的范围直接影响到计算值的准确度(上面所提到的 分位数计算误差问题)。 报警 及时、准确 使用 Prometheus 的 Alert Manager 就可以对服务进行报警,但是如何及时又准确的报警,已经如何合理设置报警,我们就要引入 SLO 的概念,在实际的业务场景中,我们会发现某个接口某个时间段的耗时是一组离散的点: 我们可以看到大部分的请求可以在 1s 之内就可以快速的返回,只有个别的请求可能由于网络的抖动、应用短暂升级或者其他因素导致过慢,若是我们直接设置接口最大请求耗时超过2s(持续一个时间段),那我们就面临着疯狂的告警轰炸,同时告警也就失去了针对某个接口的异常活动做出提示供开发人员处理的意义。 服务级别目标(Service-level objective,SLO)是指服务提供者向客户作出的服务保证的量化指标。服务级别目标与服务级别协议有所不同。服务级别协议是指服务提供者向客户保证会提供什么样的服务,服务级别目标则是服务的量化说明。 Service-level objective 服务级别目标 比方说我们发现上面的 90% 请求都在 1s 内返回,我们就可以只需要对 90% 请求耗时做监控分析其调用链路并告警。 举个栗子,比方说我们一个首页的接口 /v1/home/page 99% 的请求可以在 500ms 内返回,只有个别的请求超过 2s+ 的时间,大多数情况下我们就不会关心这 1%的请求,那我们就可以定制一个 持续 1分钟首页 99% 请求耗时大于 1s的报警,这样当我们收到报警的时候,我们就可以第一时间知道首页出现了问题,我们就可以根据报警及时处理。 **业务的报警是与接口的实现与调用链路的复杂度是紧密结合在一起的,根据不同的业务场景,配置合理的报警才满足我们及时准确的要求。**反之就是配置过高不灵敏、往往线上已经出现了好久报警就是没有,配置过低,分分钟触发报警,对业务开发人员增加了排查问题的时间成本。 遇到的问题 收集指标过大拉取超时 由于我们是 gateway BFF 层做得指标,本身的路由的基数就比较大,热点路由就有好几百个,再算上对路由的打点、耗时、错误码等等的打点,导致我们每台机器的指标数量都比较庞大,最终指标汇总的时候下游的 prometheus 节点拉取经常出现耗时问题。 前期解决方案比较粗暴,就是修改 prometheus job 的拉取频率及其超时时间,这样可以解决超时问题,但是带来的结果就是最后通过 grafana 看板进行看图包括报警收集上来的点位数据延迟大,并且随着我们指标的设置越来越多的话必然会出现超时问题。 目前的解决方案就是做分布式,采用 prometheus 联邦集群的方式来解决指标收集过大的问题,采用了分布式,就可以将机器分组收集汇总,之后就可以成倍速的缩小 prometheus 拉取的压力。 动态收集机器指标 因为我们机器都是部署在集群上并且会随着活动大促动态调整机器的数量,联邦集群中配置文件最重要的就是配置各个收集节点指标的 IP:Port ,我们不可能每次都去手动维护这个配置,成本比较高,那么我们就需要将配置动态写入,针对此问题,在 leader 的建议下,使用运维服务树拿到该节点下的机器的 Ip,使用脚本程序动态维护起来就非常方便了,默认 Prometheus 是 20s 读取一次配置。 请求的耗时看图与报警不准确 这个问题是在我们的业务中,请求耗时最常见的是在 2s 之内返回,但是通过 Prometheus histogram 对应 1-2s 的请求会落在 le 为 2.5 桶中,导致报警误报,我们看日志中的请求在 1.* s 的都算在 2.5 的桶上,而报警的配置是 大于 2s, emmm 1 DefBuckets = []float64{.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10} 之后根据我们的业务场景调整了一下,使用了自己的 CustomBuckets: 1 CustomBuckets = []float64{.01, .025, .05, .1, .25, .5, 1, 1.5, 2, 3, 4, 8} References Prometheus 官方文档 Prometheus 翻译文档 wiki SLO 服务级别目标 wiki 累积直方图 关注微信公众号,第一时间获取最新内容,让我们一起变得更强!Debug客栈:订阅本站· 文章归档· 我的项目· 友情链接· 我的使用· 飞湾计划· 摄影展集· 我的主页
Debug
入驻第1年
来小米,一起玩 !!!
永远相信美好的事情即将发生 欢迎大家联系我,让我为你内推吧,小米众多岗位等你来选,不清楚岗位信息的可以联系我(关注微信公众号「Debug客栈」直接发送消息即可),我会给你发对应的内推部门及岗位,也可以联系我查询内推情况,感觉 OK,你就来吧! 关注微信公众号,第一时间获取最新内容,让我们一起变得更强!Debug客栈:订阅本站· 文章归档· 我的项目· 友情链接· 我的使用· 飞湾计划· 摄影展集· 我的主页
Debug
入驻第1年
Git 命令 reset 和 revert 的区别
前言 在团队开发中,使用 Git 作为版本开发工具,可以便捷地协同多人管理并行开发,但是由于自己或者其他人代码提交污染了远程分支,就需要对远程代码进行恢复操作,Git 提供了 reset 和 revert 两种命令来进行恢复操作,这两种操作效果是截然不同的,不太清楚这个原理的同学需要了解一下,以免在实际的开发过程中翻车,导致线上远程仓库不可逆转的操作。 首先从英文释义来讲,reset 是重置的意思,revert 是恢复、还原的意思,作为 Coder ,第一感觉 reset 的效果比 revert 更猛一些,实际情况也的确如此,让我们一起探讨一下吧。 背景 Git 的每一次提交都是一次 commit,上图可以看到在时间线上有三次提交,此时 HEAD 指向 main 分支,main 分支又指向最新的 Commit3。 HEAD 是指向当前分支的最新提交的指针,可以在任意分支进行切换; main (master)分支,是一个 git 代码仓库的主分支也是默认分支; commit 每一次提交代码都会产生一个 commit id 来标识工作区的变更与改动。 实践出真理 为了直接明白的了解其原理,我这里在 github 上创建一个空白的仓库,按照上图创建三次提交: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 commit b0ef8f9125226af8f06ff1aba7c1f1fc83adea9b (HEAD -> master, origin/master) Author: debuginn <debuginn@icloud.com> Date: Tue Sep 21 16:36:39 2021 +0800 feat add 3.go commit 338bf3e30983d34074f37a18b3ff80ea9bca75f0 Author: debuginn <debuginn@icloud.com> Date: Tue Sep 21 16:36:09 2021 +0800 feat add 2.go commit 6b166ed34962da08d944e2b1d3f36d9015dd8f35 Author: debuginn <debuginn@icloud.com> Date: Tue Sep 21 16:35:16 2021 +0800 feat add 1.go Git Reset git reset 的作用是将 HEAD 指向指定的版本上去: 1 使用 git log 查看提交记录: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 commit b0ef8f9125226af8f06ff1aba7c1f1fc83adea9b (HEAD -> master, origin/master) Author: debuginn <debuginn@icloud.com> Date: Tue Sep 21 16:36:39 2021 +0800 feat add 3.go commit 338bf3e30983d34074f37a18b3ff80ea9bca75f0 Author: debuginn <debuginn@icloud.com> Date: Tue Sep 21 16:36:09 2021 +0800 feat add 2.go commit 6b166ed34962da08d944e2b1d3f36d9015dd8f35 Author: debuginn <debuginn@icloud.com> Date: Tue Sep 21 16:35:16 2021 +0800 feat add 1.go 这里可以看到我们提交了三次记录,我们现在想恢复到第一次 commit 提交的时候。 2 使用 git reset –hard 命令操作: 1 2 ➜ demo git:(master) git reset --hard 6b166ed34962da08d944e2b1d3f36d9015dd8f35 HEAD 现在位于 6b166ed feat add 1.go 再次查看 git log : 1 2 3 4 5 commit 6b166ed34962da08d944e2b1d3f36d9015dd8f35 (HEAD -> master) Author: debuginn <debuginn@icloud.com> Date: Tue Sep 21 16:35:16 2021 +0800 feat add 1.go 此时我们可以看到已经恢复到了第一次提交代码的时候,目前我们是使用 git reset --hard 的方式,其实这里存在着三种方式,TODO 下一篇 git 操作讲一下。 这时候我们只是讲本地的 HEAD 指向了 main 分支的 commit 1,但是远程并没有变更,此时需要强行推一下就可以了。 3 使用git push -f 强行推送到远程: 1 2 3 4 ➜ demo git:(master) git push -f 总共 0(差异 0),复用 0(差异 0),包复用 0 To github.com:debuginn/demo.git + b98f95e...6b166ed master -> master (forced update) 此时我们可以看到远程也没有了我们之前提交的三次记录而是只有第一次的提交记录。 在团队合作的共同操作一个仓库的时候, git reset 命令一定要慎重使用,在使用的时候一定要再三确认其他同学的代码是否会被重置操作而导致代码丢失,导致一些提交记录的丢失,这些都是不可逆的,一定要慎重。 Git revert git revert 是用来重做某一个 commit 提交的内容,在我们原始的提交之中,我们会发现分支上面有创建了一个新的 commit 提交,而此时我们对于想重做的某个 commit 提交的内容都不存在了: 1 使用git log查看提交记录: 1 2 3 4 Author: debuginn <debuginn@icloud.com> Date: Tue Sep 21 16:36:39 2021 +0800 feat add 3.go 2 使用git revert命令重做操作: 1 2 3 4 5 ➜ demo git:(master) git revert 338bf3e30983d34074f37a18b3ff80ea9bca75f0 删除 2.go [master ef822b7] Revert "feat add 2.go" 1 file changed, 9 deletions(-) delete mode 100644 2.go 再次查看 git log : 1 2 3 4 5 6 7 8 9 10 11 12 13 commit ef822b71c33a2dbbdaa350fddcfa14e8fc55e543 (HEAD -> master, origin/master) Author: debuginn <debuginn@icloud.com> Date: Tue Sep 21 17:12:00 2021 +0800 Revert "feat add 2.go" This reverts commit 338bf3e30983d34074f37a18b3ff80ea9bca75f0. commit b0ef8f9125226af8f06ff1aba7c1f1fc83adea9b Author: debuginn <debuginn@icloud.com> Date: Tue Sep 21 17:05:39 2021 +0800 feat add 3.go 可以看到当前已经重做了一下 commit 2 的提交,已经讲 2.go 删除掉了。 可以看到 github 上面有了四次提交记录。 总结 git reset和git revert都是属于重新恢复工作区以及远程提交的方式,但这两种操作有着截然不同的结果: git reset是将之前的提交记录全部抹去,将 HEAD 指向自己重置的提交记录,对应的提交记录都不复存在; git revert 操作是将选择的某一次提交记录 重做,若之后又有提交,提交记录还存在,只是将指定提交的代码给清除掉。 选择合适的方式回滚自己的代码在团队合作中很重要,但是要慎重操作,不要丢失代码哦。 关注微信公众号,第一时间获取最新内容,让我们一起变得更强!Debug客栈:订阅本站· 文章归档· 我的项目· 友情链接· 我的使用· 飞湾计划· 摄影展集· 我的主页
Debug
入驻第1年
使用数字人民币兑换建党100周年纪念币
今天使用数字人民币兑换了建党100周年纪念币,过程比较坎坷,不过最终还是兑换成功了。 预约纪念币成功后,今天中秋假期,正好去兑换纪念币,小雨转中雨 ☁️,作为多年没有使用纸质人民币的我实在是没有钱来兑换纪念币,之后搜索了一下附近可以兑换人民币的营业厅,都在千米之外,算了…… 突然想到前两天美团有一个数字人民币的活动,下载了数字人民币 APP,研究了一下,发现有工商银行支持数字人民币,之后搜寻了一下网点,发现北京地区都是支持数字人民币的了,之后我就去申请了工商银行电子钱包,往里面转了 200 元钱,之后去银行网点ing。 到了之后工作人员引领到专门兑换纪念币柜台,我问了一下是否可以使用数字人民币兑换,好家伙,社死瞬间,一下来了 6 个工作人员看我操作,柜台小姐姐说没有操作过数字人民币付款,之后那我当一下小白鼠 ? ? 操作出来数字人民币支付二维码页面,之后扫描发现不能使用 emmmmm,尴尬,看了提示,原来是让我下载工行的 APP,之后使用上面的数字人民币进行支付,一通下载注册之后,再次去柜台兑换,扫码 => 支付,等了 5s 左右,最终成功兑换了纪念币,现在想想,我应该是第一个使用数字人民币兑换纪念币的第一人了吧。 数字人民币未来由国家导向大力推广,会使人民的支付更加便捷,不过个人建议纸质币保留下来,照顾不会使用手机的老年群体,总之,技术的进步,未来看来我们都是为了一串数字而奋斗喽。 关注微信公众号,第一时间获取最新内容,让我们一起变得更强!Debug客栈:订阅本站· 文章归档· 我的项目· 友情链接· 我的使用· 飞湾计划· 摄影展集· 我的主页
小十
入驻第1年
使用群晖nas搭建独立局域网ip的docker版centos
群晖系统里面的docker套件非常好用,但是群晖默认情况下,没有桥接到局域网的网络配置,因此要想建立的docker拥有独立的局域网ip,而不是通过端口转发,或者占用nas的端口的方式,就需要对网络进行一定的配置。 本篇就记录了从新建docker网络配置到最后建立独立局域网ip的docker版centos系统的过程。 一、创建桥接网卡 1、启用open switch 控制面板-网络-网络界面-管理-open switch设置,勾选启用,确定。 (如果安装了VMM套件,此功能默认开启) 2、Docker创建桥接网卡 这一步不可少,群晖Docker默认是没有桥接网卡的,需要手动创建。 首先控制面板-终端机和SNMP,启用SSH功能。 通过ssh软件连入群晖后台 sudo -i 回车输入密码 输入命令 ip addr 返回的值中,找到对应你的群晖IP地址那条,上面就是物理网卡名称,网卡名称是ovs_eth0 输入命令创建桥接网卡 docker network create -d macvlan --subnet=192.168.31.0/24 --gateway=192.168.31.1 -o parent=ovs_eth0 bridge-host 命令说明:192.168.31.0为你的内网地址段,192.168.31.1为你的网关地址,ovs_eth0是你的物理网卡名称 创建好之后,docker的网络里会多出一个bridge-host网卡 二、创建centos7系统 1、下载镜像 在注册表中下载centos的镜像,我选择了centos7.6版本。 或者使用命令获取:docker pull centos 在ssh下,输入docker images查看镜像列表。 2、启动镜像 通过镜像启动docker实例,并在启动实例的过程中,配置好相关参数。 命令如下: docker run --privileged -itd --name centos7 --network bridge-host --ip=192.168.31.111 f1cb7c7d58b7 /usr/sbin/init —-privileged 启动后让docker容器具备超级特权。 -itd 交互式、终端、后台运行 –name 给启动的容器命名,方便后续操作 –network 配置docker使用的网卡 –ip 设置dokcer的ip f1cb7c7d58b7 就是IMAGE ID 注:–privileged 和/usr/sbin/init是必须的,否则会报错。 Failed to get D-Bus connection: Operation not permitted
Debug
入驻第1年
订阅本站
Feed 订阅 欢迎订阅下面的 Feed,您可以及时跟踪我的更新: 1 https://blog.debuginn.com/index.xml 本博客已经有 订阅者。 公众号订阅 当然,您不习惯上方方式订阅,您可以选择你经常阅读的社区进行订阅,目前仅支持 知乎专栏、腾讯云+社区(Sync)。 视频专区 我还不时制作视频上传至 哔哩哔哩 和 Youtube ,欢迎关注。 关注微信公众号,第一时间获取最新内容,让我们一起变得更强!Debug客栈:订阅本站· 文章归档· 我的项目· 友情链接· 我的使用· 飞湾计划· 摄影展集· 我的主页
小十
入驻第1年
更换了博客的主程序
从2011年2月2日第一次申请了顶级com域名后,这个博客就开始使用了,最开始就使用了wordpress程序,在第一次使用顶级域名之前,还使用过zblog程序等等,那时候的网站备份随着机械移动硬盘的损坏,再也没有办法恢复了。 最近工作上挺忙,因为赶上了安全学习和郑州7·20灾害,在工作之余,偶尔看了下网站的运行状态,发现wordpress更新非常频繁,使用的主题也已经无法满足部分兼容需求。新版本的博客程序也越来越臃肿,实在没有办法,就想到重新装一下服务器的操作系统,重新配置下服务器环境。 将旧网站(chyfh.com,i.xiaoten.com,demo.xiaoten.com,work.xiaoten.com,backup.xiaoten.com,hua.xiaoten.com,note.xiaoten.com,jiaohuiming.cn……)完全转移到自己的本地环境,而真正留下外网服务器运行的就是,博客、论坛和相册这三个了。 今天先把博客更换为typecho程序,因为这程序非常简单,更新也不频繁,也刚好有一个比较易用的主题,可以使用,主题本身功能很强大,但没有认真去研究。 重点还是先把内容恢复了。 不知道用这个程序能不能用的惯,已经遇到很多问题,慢慢解决。。
Debug
入驻第1年
[译] 方法是否应该在 T 或 *T 上声明
译文原地址:Should methods be declared on T or *T - David 在 Go 中,对于任何的类型 T,都存在一个类型 *T,他是一个表达式的结果,该表达式接收的是类型 T ,例如: 1 2 3 type T struct { a int; b bool } var t T // t's type is T var p = &t // p's type is *T 这两种类型,T 和 *T 是不同的,但 *T 不能代替 T。 你可以在你拥有的任意类型上声明一个方法;也就是说,在您的包中的函数声明的类型。因此,您可以在声明的类型 T 和对应的派生指针类型 *T 上声明方法。另一种说法是,类型上的方法被声明为接收器接收者值的副本,或一个指向其接收者值的指针。所以问题就存在了,究竟是哪种形式最合适? 显然,如果你的方法改变了他的接收者,他应该在 *T 上声明。但是,如果方法不改变他的接收者,在 T 上声明它是安全的么? 事实证明,这样做的话安全的情况非常有限(简单理解就是不安全的)。例如,众所周知,你不应该复制一个 sync.Mutex 的值,因为它打破了互斥量的不变量。由于互斥锁控制对变量(共享资源)的访问,他们经常被包装在一个结构体中,包含他们的控制的值(共享资源): 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package counter type Val struct { mu sync.Mutex val int } func (v *Val) Get() int { v.mu.Lock() defer v.mu.Unlock() return v.val } func (v *Val) Add(n int) { v.mu.Lock() defer v.mu.Unlock() v.val += n } 大部分 Gopher 都知道,忘记在指针接收器 *Val 上是声明 Get 或 Add 方法是错误的。然而,任何嵌入 Val 来利用其 0 值的类型,也必须仅在其指针接收器上声明方法,否者可能会无意复制其嵌入类型值的内容: 1 2 3 4 5 6 7 type Stats struct { a, b, c counter.Val } func (s Stats) Sum() int { return s.a.Get() + s.b.Get() + s.c.Get() // whoops(哎呀) } 维护值切片的类型可能会出现类似的陷阱,当然也有可能发生意外的数据竞争。 简而言之,我认为您更应该喜欢在 *T 上声明方法,除非您有非常充分的理由不该这样做。 我们说 T 但这只是您声明的类型的占位符; 此规则是递归的,取 *T 类型的变量的地址返回的是 **T 类型的结果; 这就是为什么没有人可以在像 int 这样的基础类型上声明方法; Go 中的方法只是将接受者作为第一个形式参数传递的函数的语法糖; 如果方法不改变它的接收者,它是否需要是一个方法吗? 相关文章: What is the zero value, and why is it useful? Ice cream makers and data races Slices from the ground up The empty struct 最后,此篇文章我是第一次尝试翻译英文文章,尽管英文水平不太好,一些单词不认识,但是相信自己翻译一篇文章可以学习英语与理解 Go 设计获取 double 的乐趣。 关注微信公众号,第一时间获取最新内容,让我们一起变得更强!Debug客栈:订阅本站· 文章归档· 我的项目· 友情链接· 我的使用· 飞湾计划· 摄影展集· 我的主页
Debug
入驻第1年
我的阅读
我的阅读清单 2025 状态 书籍 类别 笔记 推荐指数 ✅ 《高效能人士的七个习惯》 效率 对于职场提高效率非常有帮助,第二遍看 🌟🌟🌟🌟🌟 ✅ 《纳瓦尔宝典》 财经与幸福 资本的原始积累与幸福密集的分享,还是非常有帮助的 🌟🌟🌟🌟🌟 ✅ 《挪威的森林》 小说 一个关于人性的深度思考,也可以作为一种生活方式 🌟🌟🌟🌟🌟 ✅ 《鹿鼎记》 小说 读起来很有代入感,逻辑闭环很好,简直了爽文 🌟🌟🌟🌟🌟 ✅ 《奈飞文化手册》 管理 对奈飞的文化进行了详细的介绍,也可以作为一种管理方式 🌟🌟🌟🌟 ✅ 《人类简史》 历史 一个关于人类历史的深度思考 🌟🌟🌟🌟 ✅ 《左耳听风》 技术 写的很真挚也很有深度,作者 RIP. 🌟🌟🌟🌟🌟 ✅ 《自洽的程序员》 职场 一个关于程序员的深度思考,怎么说呢,建议阅读~ 🌟🌟🌟🌟🌟 2024 状态 书籍 类别 笔记 推荐指数 ✅ 《长安的荔枝》 小说 感谢同事分享的马伯庸老师的《太白金星有点烦》和《长安的荔枝》,我理解他的作品为神话历史类讽刺小说? 🌟🌟🌟🌟🌟 ✅ 《我在北京送快递》 白话文 跟着作者体验最真实的凡人歌。 🌟🌟🌟🌟 2023 状态 书籍 类别 笔记 推荐指数 ✅ 《富爸爸穷爸爸》 理财 怎么去积累财富,摆脱穷人思维,跳出“老鼠赛跑”游戏 🌟🌟🌟🌟🌟 ⏳ 《毛泽东选集》 文论 看了新民主主义革命至抗日战争的相关章节,不得不佩服领袖的远见与决策,推荐阅读 🌟🌟🌟🌟🌟 ✅ 《Apache Dubbo 微服务从入门到精通》 技术 Java 协议相关书籍 🌟🌟🌟🌟🌟 ✅ 《埃隆·马斯克传》 传记 遵循“第一性原理”,创造奇迹的人,推荐阅读 🌟🌟🌟🌟🌟 ✅ 《太白金星有点烦》 小说 如何规划西天取经,看太白金星与观音菩萨的运作 🌟🌟🌟🌟🌟 2022 状态 书籍 类别 笔记 推荐指数 ✅ 《Effective Go 中英双语版》 技术 🌟🌟🌟🌟 ✅ 《一往无前》 公司 🌟🌟🌟🌟🌟 ✅ 《黄金时代》 小说 🌟🌟🌟🌟 ✅ 《乖,摸摸头》 散文 🌟🌟🌟🌟🌟 ✅ 《图解 HTTP》 技术 🌟🌟🌟🌟 ✅ 《分布式缓存:原理、架构及Go语言实现》 技术 🌟🌟🌟🌟 ✅ 《深入理解Java虚拟机JVM高级特性与最佳实践》 技术 🌟🌟🌟🌟🌟 ✅ 《深入设计模式》 技术 🌟🌟🌟🌟 ⏳ 《保重》 散文 🌟🌟🌟🌟🌟 2021 状态 书籍 类别 笔记 推荐指数 ✅ 《MySQL 实战 45 讲》 技术 🌟🌟🌟🌟🌟 ✅ 《Redis 设计与实现》 技术 🌟🌟🌟🌟🌟 关注微信公众号,第一时间获取最新内容,让我们一起变得更强!Debug客栈:订阅本站· 文章归档· 我的项目· 友情链接· 我的使用· 飞湾计划· 摄影展集· 我的主页
Debug
入驻第1年
emmm 这是一篇碎碎念
距离最后一篇博文 《Go 语言开发设计指北》发布已经过去一个多月的时间了,在这一段的时间里,在看了大量的书籍?,在工作上安排的工作都比较得心应手,时间还算比较充裕,但是懒惰心里没有丝毫退去 ?,这样是不行的,很容易让自己的思维和学习能力下降。 先来谈谈近期阅读的一些文章吧,《高性能 MySQL》这一本书相信是计算机从业人员必读的一本书 ? 了吧,这本书虽然看起来比较厚,但是里面的知识面和富有情趣的讲解还是很不错的,给作者点个赞,现在是第一遍读这本书,本着:“先把书读薄”的原则来读,已经攻读了3章多了,学到了很多实际中业务开发的宝贵经验,但是在实际设计中还是会落入坑中,学到了主键、索引设计技巧,以及具体是怎么去使用的,自己所书写的每一条 SQL 语句是在 MySQL中是怎样运行的,执行效率如何,是否可以优化,以及怎么去衡量自己优化的效率可以达到多少,在这本书中都有所讲解。 第二本就是在极客时间上追更鸟窝大神的《Go语言并发实战》,学习了学多的Go语言并发设计所使用到的并发原语及处理方法,包括 Mutex、RWMutex、WaitGroup、Pool、Once、Context等等操作及内部实现,emmm 目前看老师已经更新完了 自己还没有追更完,惭愧呀,Flag 要树立起来了,哈哈。 第三本是《GC的认识》,在开发过程中,自己在业务代码的设计中无需考虑变量声明后销毁的流程,因为在Go语言中已经实现了对堆栈资源的销毁与清理,但是GC是怎么操作的自己之前都是模糊的了解有三色标记,从根出发标记,很笼统的概念,近期看的这本书,严格意义上一笔记,就讲解了GC的执行过程,怎么去观察GC操作以及怎么去对GC优化等操作,详细的就不展开了,大家感兴趣的可以去看一下。 还有就是一些小的细节点的学习了,还有对自己项目组中的项目及框架了解了一下,这里就不分享啦,实际上是不知道是否存在蟹蜜危机。 毕竟上一篇文章发布的时候就战战兢兢 。 结尾呼应标题,这是一篇水文,主要是想告诉大家 Meng小羽并没有跑路 Debug客栈 还在,另外希望大家有好的分享资料的分享的话可以和我互动或者加入我的群聊,毕竟 1+1 > 2 的,对吧。好了不多说了,跑步去了 ?。 关注微信公众号,第一时间获取最新内容,让我们一起变得更强!Debug客栈:订阅本站· 文章归档· 我的项目· 友情链接· 我的使用· 飞湾计划· 摄影展集· 我的主页
Debug
入驻第1年
Go 语言开发设计指北
Go 语言是一种强类型、编译型的语言,在开发过程中,代码规范是尤为重要的,一个小小的失误可能会带来严重的事故,拥有一个良好的 Go 语言开发习惯是尤为重要的,遵守开发规范便于维护、便于阅读理解和增加系统的健壮性。 以下是我们项目组开发规范加上自己开发遇到的问题及补充,希望对你有所帮助: 注:我们将以下约束分为三个等级,分别是:【强制】、【推荐】、【参考】。 Go 编码相关 【强制】代码风格规范遵循 go 官方标准:CodeReviewComments,请使用官方 golint lint 进行风格静态分析; 【强制】代码格式规范依照 gofmt,请安装相关 IDE 插件,在保存代码或者编译时,自动将源码通过 gofmt 做格式化处理,保证团队代码格式一致(比如空格,递进等) 【强制】业务处理代码中不能开 goroutine,此举会导致 goroutine 数量不可控,容易引起系统雪崩,如果需要启用 goroutine 做异步处理,请在初始化时启用固定数量 goroutine,通过 channel 和业务处理代码交互,初始化 goroutine 的函数,原则上应该从 main 函数入口处明确的调用: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 func crond() { defer func() { if err := recover(); err != nil { // dump stack & log } }() // do something } func main() { // init system go crond() go crond2() // handlers } 【强制】异步开启 goroutine 的地方(如各种 cronder ),需要在最顶层增加 recover(),捕捉 panic,避免个别 cronder 出错导致整体退出: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func globalCrond() { for _ := ticker.C { projectCrond() itemCrond() userCrond() } } func projectCrond() { defer func() { if err := recover(); err != nil { // 打日志,并预警 } } // do } 【强制】当有并发读写 map 的操作,必须加上读写锁 RWMutex,否则 go runtime 会因为并发读写报 panic,或者使用 sync.Map 替代; 【强制】对于提供给外部使用的 package,返回函数里必须带上 err 返回,并且保证在 err == nil 情况下,返回结果不为 nil,比如: 1 2 resp, err := package1.GetUserInfo(xxxxx) // 在err == nil 情况下,resp不能为nil或者空值 【强制】当操作有多个层级的结构体时,基于防御性编程的原则,需要对每个层级做空指针或者空数据判别,特别是在处理复杂的页面结构时,如: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 type Section struct { Item *SectionItem Height int64 Width int64 } type SectionItem struct { Tag string Icon string ImageURL string ImageList []string Action *SectionAction } type SectionAction struct { Type string Path string Extra string } func getSectionActionPath(section *Section) (path string, img string, err error) { if section.Item == nil || section.Item.Action == nil { // 要做好足够防御,避免因为空指针导致的panic err = fmt.Errorf("section item is invalid") return } path = section.Item.Action.Path img = section.Item.ImageURL // 对取数组的内容,也一定加上防御性判断 if len(section.Item.ImageList) > 0 { img = section.Item.ImageList[0] } return } 【推荐】生命期在函数内的资源对象,如果函数逻辑较为复杂,建议使用 defer 进行回收: 1 2 3 4 5 6 7 8 func MakeProject() { conn := pool.Get() defer pool.Put(conn) // 业务逻辑 ... return } 对于生命期在函数内的对象,定义在函数内,将使用栈空间,减少 gc 压力: 1 2 3 4 5 6 7 func MakeProject() (project *Project){ project := &Project{} // 使用堆空间 var tempProject Project // 使用栈空间 return } 【强制】不能在循环里加 defer,特别是 defer 执行回收资源操作时。因为 defer 是函数结束时才能执行,并非循环结束时执行,某些情况下会导致资源(如连接资源)被大量占用而程序异常: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 // 反例: for { row, err := db.Query("SELECT ...") if err != nil { ... } defer row.Close() // 这个操作会导致循环里积攒许多临时资源无法释放 ... } // 正确的处理,可以在循环结束时直接close资源,如果处理逻辑较复杂,可以打包成函数: for { func () { row, err := db.Query("SELECT ...") if err != nil { ... } defer row.Close() ... }() } 【推荐】对于可预见容量的 slice 或者 map,在 make 初始化时,指定cap大小,可以大大降低内存损耗,如: 1 2 3 4 5 6 7 8 9 10 11 headList := make([]home.Sections, 0, len(srcHomeSection)/2) tailList := make([]home.Sections, 0, len(srcHomeSection)/2) dstHomeSection = make([]*home.Sections, 0, len(srcHomeSection)) …. if appendToHead { headList = append(headList, info) } else { tailList = append(tailList, info) } …. dstHomeSection = append(dstHomeSection, headList…) dstHomeSection = append(dstHomeSection, tailList…) 【推荐】逻辑操作中涉及到频繁拼接字符串的代码,请使用 bytes.Buffer 替代。使用 string 进行拼接会导致每次拼接都新增 string 对象,增加 GC 负担: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 // 正例: var buf bytes.Buffer for _, name := range userList { buf.WriteString(name) buf.WriteString(",") } return buf.String() // 反例: var result string for _, name := range userList { result += name + "," } return result 【强制】对于固定的正则表达式,可以在全局变量初始化时完成预编译,可以有效加快匹配速度,不需要在每次函数请求中预编译: 1 2 3 4 var wordReg = regexp.MustCompile("[\\w]+") func matchWord(word string) bool { return wordReg.MatchString(word) } 【推荐】JSON 解析时,遇到不确定是什么结构的字段,建议使用 json.RawMessage 而不要用 interface,这样可以根据业务场景,做二次 unmarshal 而且性能比 interface 快很多; 【强制】锁使用的粒度需要根据实际情况进行把控,如果变量只读,则无需加锁;读写,则使用读写锁 sync.RWMutex; 【强制】使用随机数时(math/rand),必须要做随机初始化(rand.Seed),否则产生出的随机数是可预期的,在某些场合下会带来安全问题。一般情况下,使用math/rand可以满足业务需求,如果开发的是安全模块,建议使用crypto/rand,安全性更好; 【推荐】对性能要求很高的服务,或者对程序响应时间要求高的服务,应该避免开启大量 gouroutine; 说明:官方虽然号称 goroutine 是廉价的,可以大量开启 goroutine,但是由于 goroutine 的调度并没有实现优先级控制,使得一些关键性的 goroutine(如网络/磁盘IO,控制全局资源的goroutine)没有及时得到调度而拖慢了整体服务的响应时间,因而在系统设计时,如果对性能要求很高,应避免开启大量goroutine。 打点规范 【强制】打点使用.来做分隔符,打点名称需要包含业务名,模块,函数,函数处理分支等,参考如下: 1 2 // 业务名.服务名.模块.功能.方法 service.gateway.module.action.func 【强制】打点使用场景是监控系统的实时状态,不适合存储任何业务数据; 【强制】在打点个数太多时,展示时速度会变慢。建议单个服务打点的key不超过10000个,key中单个维度不同值不超过 1000个(千万不要用 user_id 来打点); 【推荐】如果展示的时候需要拿成百上千个key的数据通过 Graphite 的聚合函数做聚合,最后得到一个或几个 key。这种情况下可以在打点的时候就把这个要聚合的点聚合好,这样展示的时候只要拿这几个 key,对展示速度是巨大的提升。 日志相关 【强制】日志信息需带上下文,其中 logid 必须带上,同一个请求打的日志都需带上 logid,这样可以根据 logid 查找该次请求相关的信息; 【强制】对debug/notice/info 级别的日志输出,必须使用条件输出或者使用占位符方式,避免使用字符拼接方式: 1 log.Debug("get home page failed %s, id %d", err, id) 【强制】如果是解析 json 出错的日志,需要将报错 err 及原内容一并输出,以方便核查原因; 【推荐】对debug/notice/info级别的日志,在打印日志时,默认不显示调用位置(如/path/to/code.go:335) 说明:go 获取调用栈信息是比较耗时的操作(runtime.Caller),对于性能要求很高的服务,特别是大量调用的地方,应尽量避免开发人员在使用该功能时,需知悉这个调用带来的代价。 Redis 相关 【推荐】统一使用:作为前缀后缀分隔符,这里可以根据 Redis 中间件 key proxy 怎么解析分析 Key 进行自定义,便于基础服务的数据可视化及问题排查; 【强制】避免使用 HMGET/HGETALL/HVALS/HKEYS/SMEMBERS 阻塞命令这类命令在 value 较大时,对 Redis 的 CPU/带宽消耗较高,容易导致响应过慢引发系统雪崩; 【强制】不可把 Redis 当成存储,如有统计相关的需求,可以考虑异步同步到数据库进行统计,Redis 应该回归缓存的本质; 【推荐】避免使用大 key,按经验超过 10k 的 value,可以压缩(gzip/snappy等算法)后存入内存,可以减少内存使用,其次降低网络消耗,提高响应速度: 1 2 3 value, err := c.RedisCache.GetGzip(key) …. c.RedisCache.SetExGzip(content, 60) 【推荐】Redis 的分布式锁,可以使用: 1 2 lock: redis.Do("SET", lockKey, randint, "EX", expire, "NX") unlock: redis.GetAndDel(lockKey, randint) // redis暂不支持,可以用lua脚本 【推荐】尽量避免在逻辑循环代码中调用 Redis,会产生流量放大效应,请求量较大时需采用其他方法优化(比如静态配置文件); 【推荐】key 尽量离散读写,通过uid/imei/xid等跟用户/请求相关的后缀分摊到不同分片,避免分片负载不均衡; 【参考】当缓存量大,请求量较高,可能超出 Redis 承受范围时,可充分利用本地缓存(localcache)+redis缓存的组合方案来缓解压力,削减峰值: 使用这个方法需要具备这几个条件: cache 内容与用户无关,key 状态不多,属于公共信息; 该cache内容时效性较高,但是访问量较大,有峰值流量。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 key := "demoid:3344" value := localcacche.Get(key) if value == "" { value = rediscache.Get(key) if value != "" { // 随机缓存 1~5s,各个机器间错开峰值,只要比 redis缓存短即可 localcache.SetEx(key, value, rand.Int63n(5)+1) } } if value == "" { .... // 从其他系统或者数据库获取数据 appoint.GetValue() // 同时设置到redis及localcache中 rediscache.SetEx(key, content, 60) localcache.SetEx(key, content, rand.Int63n(5)+1) } 【参考】对于请求量高,实时性也高的内容,如果纯粹使用缓存,当缓存失效瞬间,会导致大量请求穿透到后端服务,导致后端服务有雪崩危险: 如何兼顾扛峰值,保护后端系统,同时也能保持实时性呢?在这种场景下,可以采用随机更新法更新数据,方法如下: 正常请求从缓存中读取,缓存失效则从后端服务获取; 在请求中根据随机概率 1%(或者根据实际业务场景设置比率)会跳过读取缓存操作,直接从后端服务获取数据,并更新缓存。 这种做法能保证最低时效性,并且当访问量越大,更新概率越高,使得内容实时性也越高。 如果结合上一条 localcache+rediscache 做一二级缓存,则可以达到扛峰值同时保持实时性。 数据库相关 【强制】操作数据库 sql 必须使用 stmt 格式,使用占位符替代参数,禁止拼接 sql; 【强制】SQL语句查询时,不得使用 SELECT * (即形如 SELECT * FROM tbl WHERE),必须明确的给出要查询的列名,避免表新增字段后报错; 【强制】对于线上业务 SQL,需保证命中索引,索引设计基于业务需求及字段区分度,一般可区分状态不高的字段(如 status 等只有几个状态),不建议加到索引中; 【强制】在成熟的语言中,有实体类,数据访问层(repository / dao)和业务逻辑层( service );在我们的规范中存储实体 struct 放置于 entities 包下; 【强制】对于联合索引,需将区分度较大的字段放前面,区分度小放后面,查找时可以减少被检索数据量; 1 2 3 4 5 6 7 8 -- 字段区分度 item_id > project_id alter table xxx add index idx_item_project ( item_id , project_id ) 【强制】所有数据库表必须有主键 id; 【强制】主键索引名为 pk字段名; 唯一索引名为 uk字段名; 普通索引名则为 idx_字段名; 【强制】防止因字段类型不同造成的隐式转换,导致索引失效,造成全表扫描问题; 【强制】业务上有唯一特性的字段,即使是多字段的组合,也必须建成唯一索引; 【强制】一般事务标准操作流程: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 func TestTXExample(t *testing.T) { // 打开事务 tx, err := db.Beginx() if err != nil { log.Fatal("%v", err) return } // defer异常 needRollback := true defer func() { if r := recover(); r != nil { // 处理recover,避免因为panic,资源无法释放 log.Fatal("%v", r) needRollback = true } if needRollback { xlog.Cause("test.example.transaction.rollback").Fatal() tx.Rollback() } }() // 事务的逻辑 err = InsertLog(tx, GenTestData(1)[0]) if err != nil { log.Fatal("%v", err) return } // 提交事务 err = tx.Commit() if err != nil { log.Fatal("%v", err) return } needRollback = false return } 【强制】执行事务操作时,请确保SELECT ... FOR UPDATE条件命中索引,使用行锁,避免一个事务锁全表的情况; 【强制】禁止超过三个表的 join,需要 join 的字段,数据类型必须一致,多表关联查询时,保证被关联的字段有索引; 【强制】数据库 max_open 连接数不可设置过高,会导致代理连接数打满导致不可用状况; 关注微信公众号,第一时间获取最新内容,让我们一起变得更强!Debug客栈:订阅本站· 文章归档· 我的项目· 友情链接· 我的使用· 飞湾计划· 摄影展集· 我的主页
阿川
入驻第1年
大年初一,徒步西湖
阿川
入驻第1年
PHP 笔记
阿川
入驻第1年
Ubuntu Server部署日记
Debug
入驻第1年
如何提升自己的技术博文水平
2021 年的一月份马上就过去了,在这一个月中,并没有新鲜出炉的博文,恰恰相反的是我这一个月以来,在思考,自己的博客怎么输出高质量有水平的文章,正如一首优美的旋律,怎么听都可以让人回味无穷,每一遍都有自己的收获。 关于提升自己博客文章的水平,这个月思考了很多的方向,也阅读了不少人对于博客的看法和理解,最终对自己博客总结了几点不足之处: 看到及听到一种新技术或者新的事物,总是以为看了几篇的相关的文章介绍及简短的理解文章,就认为自己了解了某一个事物或者技术,之后输出自己的想法,写出一篇总结的文章,理所应当的写不出有深度有营养的文章; 写一篇博文访问量的关注与文章输出的内容对比更倾向于前者,突然感觉到自己很肤浅,本末倒置,好的文章输出最不用关心的就是访问量的问题; 博文不是自己的 OKR,而是自己技术的自留地,入职大厂之后,感觉自己的技术文章输出应该高出一个层次,一些浅显易懂的或者容易学到的点就不需要总结成文章了,眼高手低,没有认清自己的技术与输出的水平; 自己的技术储备不足(看的书及教程少),博客中一些文章还停留在了解的层次,并没有真正的去 Get 到每个的深层次的水平中去; 懒惰的心理,短视频及游戏 ? VS Debug客栈,大部分时间选择前者,但是前者看了再多,玩的再6,也提升不了自己,只能当算法喂养下的白痴。 毫不夸张的说,若是把博客水平比做成人的一生,我的博客水平依旧处在了18岁之前,没有自己的思考,更多的是在教程、分享的模版下输出,虽有输出,但今天的我发现并没有太多的营养。 正如上方的分割线,我希望自己未来的输出在这里是一个分水岭,接下来自己可以学到更多,变得更强,让自己输出的文章有深度,自己的编程技术更加有水平。 回归主题,“如何提升自己的技术博文水平”,其实映射出是自己的技术水平的不足导致的,那如何提升自己的技术水平,自己总结了一下接下来要做的努力: 阅读技术书籍,技术不能停留在会使用的阶段,要知道自己的每一步操作,在计算机内部发生了什么,原理及使用的技术是什么,现在的我谈不上如何去改进某个技术,但是要会灵活的使用现有技术提升自己的编程水平,提高自己代码的稳定性及让自己的代码写出来如诗一般优雅; 在本职工作中,多看项目组及同事的代码,不仅仅是看代码,思考为什么这样设计,这样设计带来的好处是什么,会有哪些不足,如何改进及优化; 在流行的技术及 Go 语言包中,加入到开源的项目中去,多去看大佬们的代码及设计哲学,了解业界技术的更迭及主流用法,可以的话贡献自己的代码; 多去交流技术,不能认为自己代码很 Low,没有了解很多就对研讨会或者分享会望而却步,恰恰相反,自己在这些分享会中会发现自己的水平处在什么阶段及自己如何去提升自己; 多去整理学过了、了解到的技术的笔记,学会提炼及吸收成自己的知识体系: 这是自己搭建的笔记平台:https://notes.debuginn.com 自己的学习笔记都会同步至此平台,更多的是自己的学习笔记及重要的知识点,这就是我的小本本。 最后,感谢自己导师的教诲与提醒,自己要更加的努力,提升自己,2021,定不负大家的厚望,努力成为项目组中的中坚力量,加油!!!撰写出更多有营养的文章,当然,有些理解片面或者不足的文章还请大家批评指出,谢谢 关注微信公众号,第一时间获取最新内容,让我们一起变得更强!Debug客栈:订阅本站· 文章归档· 我的项目· 友情链接· 我的使用· 飞湾计划· 摄影展集· 我的主页
小生
入驻第1年
李会生同志简介
李会生,男,汉族,中共党员,吉林长春人。长期以来他始终以“帮助别人,快乐自己”的精神甘愿付出,助人为乐,默默无闻从小事做起帮助身边的人,多年里,他积极承担社会责任,无私奉献。用自己的实际行动赢得...
Debug
入驻第1年
2020 年度总结
今年,最大的感受就是时间过的太快了,一切都是那么的来不及 …… 2020 年,疫情、毕业、工作,学生时代的 END,社会人时代的 START …… 2021 年,希望一切都在慢慢变好,新的开始、新的未来!!! 每一次总结都是新的开始的起点,那么,接下来就开始吧~ 网站数据 今年是小站运行的第 4 年,感谢大家的支持与访问,这是我分享的天地、同时也是见证我成长的地方,加油~ 一往无前 !!! 最受欢迎的文章 🔥🔥🔥 Restful API 设计指北 🔥🔥 吊打百度,多吉搜索引擎 🔥 程序猿的 Chrome 浏览器插件推荐 搭建流媒体服务器 PingOS 平台搭建 怎么优雅的选择 MySQL 存储引擎 More 页面 2020 看见的我不止一面,这里记录了我的 MORE https://debuginn.com/about 关注微信公众号,第一时间获取最新内容,让我们一起变得更强!Debug客栈:订阅本站· 文章归档· 我的项目· 友情链接· 我的使用· 飞湾计划· 摄影展集· 我的主页

© 2026 好站网HaoZhan.wang 1.5 版权所有

苏ICP备19065220号-4    萌ICP备20269980号    茶ICP备2026050346号
本站数据    2026年报    版本历史    关于本站