-
Notifications
You must be signed in to change notification settings - Fork 0
/
content.json
1 lines (1 loc) · 380 KB
/
content.json
1
{"meta":{"title":"Deng的博客","subtitle":"","description":"","author":"Evan Deng","url":"http://coderedeng.github.io","root":"/"},"pages":[{"title":"404 Not Found:该页无法显示","date":"2024-05-22T13:52:09.838Z","updated":"2024-01-22T12:58:59.000Z","comments":false,"path":"/404.html","permalink":"http://coderedeng.github.io/404.html","excerpt":"","text":""},{"title":"关于","date":"2024-05-22T13:52:09.838Z","updated":"2024-01-22T12:58:59.000Z","comments":false,"path":"about/index.html","permalink":"http://coderedeng.github.io/about/index.html","excerpt":"","text":""},{"title":"书单","date":"2024-05-26T03:15:39.159Z","updated":"2024-05-26T03:15:39.159Z","comments":false,"path":"books/index.html","permalink":"http://coderedeng.github.io/books/index.html","excerpt":"","text":"这是我的私人书单,列举于此的主要目的是巩固记忆,也方便与诸位分享。 Go语言相关书籍 Go语言设计与实现 Go语言精进之路 深入理解Go语言 Go语言编程 Go语言高级编程 Go语言编程之旅 Go语言核心编程 Go语言底层原理剖析 商业相关书籍 埃隆·马斯克传 芯片战争 芒格之道 深度学习革命 理财相关书籍 富爸爸,穷爸爸 小狗钱钱 牛奶可乐经济学 聪明的投资者 财富自由之路"},{"title":"分类","date":"2024-05-22T13:52:09.838Z","updated":"2024-01-22T12:58:59.000Z","comments":false,"path":"categories/index.html","permalink":"http://coderedeng.github.io/categories/index.html","excerpt":"","text":""},{"title":"Repositories","date":"2024-05-22T13:52:09.916Z","updated":"2024-01-22T12:58:59.000Z","comments":false,"path":"repository/index.html","permalink":"http://coderedeng.github.io/repository/index.html","excerpt":"","text":""},{"title":"标签","date":"2024-05-22T13:52:09.916Z","updated":"2024-01-22T12:58:59.000Z","comments":false,"path":"tags/index.html","permalink":"http://coderedeng.github.io/tags/index.html","excerpt":"","text":""},{"title":"博客","date":"2024-05-26T03:15:39.159Z","updated":"2024-05-26T03:15:39.159Z","comments":false,"path":"links/index.html","permalink":"http://coderedeng.github.io/links/index.html","excerpt":"","text":""}],"posts":[{"title":"使用 net/http 实现并发爬取多个 url 标题","slug":"Go爬虫 - 手动实现并发爬取多个url标题","date":"2024-04-30T13:26:38.000Z","updated":"2024-05-26T03:15:39.159Z","comments":true,"path":"2024/04/30/Go爬虫 - 手动实现并发爬取多个url标题/","link":"","permalink":"http://coderedeng.github.io/2024/04/30/Go%E7%88%AC%E8%99%AB%20-%20%E6%89%8B%E5%8A%A8%E5%AE%9E%E7%8E%B0%E5%B9%B6%E5%8F%91%E7%88%AC%E5%8F%96%E5%A4%9A%E4%B8%AAurl%E6%A0%87%E9%A2%98/","excerpt":"","text":"1. net/http 包相关方法1.1 http.NewRequestWithContextreq, err := http.NewRequestWithContext(ctx, \"GET\", url, nil) 这个方法用于创建一个新的 HTTP 请求。 它接受一个 context.Context 对象,可以用来设置请求的超时、取消等操作。 第一个参数是 HTTP 方法,这里是 “GET”。 第二个参数是要请求的 URL。 第三个参数是请求体,这里传入 nil 表示没有请求体。 返回一个 *http.Request 对象和错误对象。 1.2 Request 结构体类型type Request struct { Method string // 指定HTTP方法(GET,POST,PUT等)。 URL *url.URL ...... } 1.3 http.DefaultClient.Doresp, err := http.DefaultClient.Do(req) http.DefaultClient 是一个全局的 *http.Client 对象,它提供了默认的 HTTP 客户端实现。 Do 方法用于发送 HTTP 请求并返回响应。 它接受一个 *http.Request 对象作为参数,表示要发送的请求。 返回一个 *http.Response 对象和一个错误对象。 1.4 Response 结构体类型type Response struct { Status string // e.g. \"200 OK\" StatusCode int // e.g. 200 Proto string // e.g. \"HTTP/1.0\" ProtoMajor int // e.g. 1 ProtoMinor int // e.g. 0 ...... } 1.5 http.Response http.Response 结构表示 HTTP 响应。 它包含响应状态码、响应头和响应体等信息。 在代码中,我们使用 resp.StatusCode 来检查响应的状态码是否为200,以确定请求是否成功。 1.6 http.Response.Bodydefer resp.Body.Close() Body 字段是一个 io.ReadCloser 接口,代表响应体。 在读取完响应体后,我们应该关闭响应体以释放资源。通常使用 defer 关键字来确保在函数退出时关闭响应体。 2. golang.org/x/net/html 包相关方法2.1 html.Parse func Parse(r io.Reader) (*Node, error) 此函数接受一个实现了 io.Reader 接口的对象作为参数,通常是一个 http.Response.Body 或文件等。 返回一个 *html.Node 对象和一个 error,表示解析的根节点以及可能发生的错误。 2.2 html.Render func Render(w io.Writer, n *Node) error 此函数接受一个实现了 io.Writer 接口的对象以及一个 *html.Node 对象作为参数,将HTML节点 n 以HTML格式写入 w。 返回一个 error,表示可能发生的写入错误。 2.3 html.ParseFragment func ParseFragment(r io.Reader, context *Node) ([]*Node, error) 此函数接受一个实现了 io.Reader 接口的对象以及一个上下文节点 *html.Node 对象作为参数。 返回解析的HTML片段中的节点切片和一个 error。 2.4 html.EscapeString func EscapeString(s string) string 此函数接受一个HTML字符串作为参数,返回其在HTML中的转义形式。 2.5 html.UnescapeString func UnescapeString(s string) string 此函数接受一个转义过的HTML字符串作为参数,返回其原始形式。 2.6 html.Node HTML文档中的节点表示。 每个节点都有一个类型、一系列属性和子节点。 可以通过 Type 字段来判断节点的类型,如 ElementNode、TextNode 等。 可以通过 Data 字段获取节点的数据,如元素节点的标签名或文本节点的内容。 可以通过 Attr 字段获取节点的属性。 3. 具体实现代码package main import ( \"context\" \"fmt\" \"net/http\" \"os\" \"sync\" \"time\" \"golang.org/x/net/html\" ) // fetchTitle 使用给定的URL获取网站的标题。 func fetchTitle(ctx context.Context, url string) (string, error) { startTime := time.Now() req, err := http.NewRequestWithContext(ctx, \"GET\", url, nil) if err != nil { return \"\", err } resp, err := http.DefaultClient.Do(req) if err != nil { return \"\", err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return \"\", fmt.Errorf(\"bad status: %s\", resp.Status) } doc, err := html.Parse(resp.Body) if err != nil { return \"\", err } var title string var f func(*html.Node) f = func(n *html.Node) { if n.Type == html.ElementNode && n.Data == \"title\" && n.FirstChild != nil { title = n.FirstChild.Data } for c := n.FirstChild; c != nil; c = c.NextSibling { f(c) } } f(doc) elapsedTime := time.Since(startTime).Round(time.Millisecond) return title + \" (\" + elapsedTime.String() + \")\", nil } // crawlURLs 并发地爬取一系列URL的标题。 func crawlURLs(ctx context.Context, urls []string) ([]string, error) { // 使用WaitGroup等待所有的goroutine完成 var wg sync.WaitGroup wg.Add(len(urls)) // 使用通道来收集结果(防止结果竟态) titles := make(chan string, len(urls)) for _, url := range urls { // 每一个url创建一个goroutine go func(url string) { defer wg.Done() title, err := fetchTitle(ctx, url) if err != nil { fmt.Printf(\"Error fetching %s: %v\\n\", url, err) titles <- \"Error: \" + err.Error() return } // 将标题发送到通道 titles <- title }(url) } // 等待所有的goroutine完成 wg.Wait() close(titles) // 将通道的结果收集到数组中 resultTitles := make([]string, 0, len(urls)) for title := range titles { resultTitles = append(resultTitles, title) } return resultTitles, nil } func main() { urls := []string{ \"https://www.baidu.com\", \"https://www.36kr.com\", \"https://www.sina.com.cn\", \"https://www.jd.com\", \"https://www.taobao.com\", \"https://www.pinduoduo.com\", \"https://www.tmall.com\", \"https://www.zhihu.com\", \"http://www.juejin.cn\", \"https://www.aliyun.com\", } // 创建一个上下文,例如,用于设置超时 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() titles, err := crawlURLs(ctx, urls) if err != nil { fmt.Println(\"Error:\", err) os.Exit(1) } for i, title := range titles { fmt.Printf(\"%d: %s\\n\", i+1, title) } }","categories":[{"name":"Go爬虫","slug":"Go爬虫","permalink":"http://coderedeng.github.io/categories/Go%E7%88%AC%E8%99%AB/"}],"tags":[{"name":"Go爬虫","slug":"Go爬虫","permalink":"http://coderedeng.github.io/tags/Go%E7%88%AC%E8%99%AB/"}]},{"title":"colly","slug":"Go爬虫 - colly","date":"2024-04-30T12:12:11.000Z","updated":"2024-05-21T12:49:51.595Z","comments":true,"path":"2024/04/30/Go爬虫 - colly/","link":"","permalink":"http://coderedeng.github.io/2024/04/30/Go%E7%88%AC%E8%99%AB%20-%20colly/","excerpt":"","text":"Colly 是 Go 语言中一个功能强大的爬虫库,它被设计用于简化 Web 页面的抓取和数据提取过程。下面是关于 Colly 的一些主要特点和用法: 简单易用:Colly 提供了一个简洁的 API,使得编写爬虫变得非常容易。你可以很容易地定义需要爬取的网站的规则,并提取感兴趣的数据。 灵活的规则定义:你可以定义多个规则来匹配不同类型的网页,并在每个规则中指定需要采取的操作,例如提取数据或者跟踪链接。 并发支持:Colly 内置了对并发的支持,可以同时爬取多个页面,从而提高爬取效率。 中间件:Colly 提供了中间件机制,允许你在请求发送、响应接收等各个阶段添加自定义逻辑,从而灵活地扩展爬虫的功能。 内置的数据提取工具:Colly 提供了一些方便的工具函数,用于从 HTML 页面中提取数据,例如使用 CSS 选择器或者 XPath。 可扩展性:Colly 的设计非常灵活,你可以根据自己的需求轻松地扩展和定制功能。 以下是一个爬取微博热搜的示例代码: package main import ( \"fmt\" \"log\" \"strings\" \"github.com/gocolly/colly/v2\" \"github.com/gocolly/colly/v2/extensions\" ) // 获取微博热搜榜colly func main() { c := colly.NewCollector() extensions.RandomUserAgent(c) c.OnRequest(func(r *colly.Request) { // 向请求头添加 cookie 防止进入访客模式 r.Headers.Add(\"cookie\", \"SUB=_2AkMRY4uTf8NxqwFRmfsdxWLna410ygHEieKnP3pIJRMxHRl-yT9kqkAvtRB6OuOlexrgRHkeY_A2VqgX2CcV_p0455qS; SUBP=0033WrSXqPxfM72-Ws9jqgMF55529P9D9WhOnHVoAxngUau7LDxX9KO_; _s_tentry=passport.weibo.com; Apache=6010347679488.02.1715406001552; SINAGLOBAL=6010347679488.02.1715406001552; ULV=17154060015601116010347679488.02.1715406001552\") }) // 解析热搜页面HTML结构,获取相应热搜内容 c.OnHTML(\".data table tbody\", func(e *colly.HTMLElement) { startTime := time.Now() e.ForEach(\"tr\", func(i int, tr *colly.HTMLElement) { if i != 0 { // 去除第一个官方宣传内容 str := tr.Text str1 := strings.ReplaceAll(str, \" \", \"\") str2 := strings.ReplaceAll(str1, \"\\n\", \" \") if !strings.Contains(str2, \"•\") { // 根据•标记去除相应的广告 fmt.Printf(\"%+v\\n\", str2) } } }) timeConsum := time.Since(startTime) // 计算时间耗时 fmt.Println(\"总耗时:\", timeConsum) }) err := c.Visit(\"https://s.weibo.com/top/summary/\") if err != nil { log.Fatalln(err) return } }","categories":[{"name":"Go爬虫","slug":"Go爬虫","permalink":"http://coderedeng.github.io/categories/Go%E7%88%AC%E8%99%AB/"}],"tags":[{"name":"Go爬虫","slug":"Go爬虫","permalink":"http://coderedeng.github.io/tags/Go%E7%88%AC%E8%99%AB/"}]},{"title":"Go语言历史版本演进和新特性[持续更新]","slug":"Go语言历史版本演进和新特性[持续更新]","date":"2024-04-10T13:34:11.000Z","updated":"2024-05-21T13:58:14.408Z","comments":true,"path":"2024/04/10/Go语言历史版本演进和新特性[持续更新]/","link":"","permalink":"http://coderedeng.github.io/2024/04/10/Go%E8%AF%AD%E8%A8%80%E5%8E%86%E5%8F%B2%E7%89%88%E6%9C%AC%E6%BC%94%E8%BF%9B%E5%92%8C%E6%96%B0%E7%89%B9%E6%80%A7[%E6%8C%81%E7%BB%AD%E6%9B%B4%E6%96%B0]/","excerpt":"","text":"发布总览:Release History - The Go Programming Language GO 1.22 新特性发布时间:2024-02-06 官方说明:Go 1.22 Release Notes - The Go Programming Language 循环变量改进:Go 1.22解决了for循环中循环变量在迭代之间意外共享的问题。在新的版本中,for循环中的循环变量(如for range语句中的变量)将不再在整个循环中共享,而是在每次迭代中都有自己的变量。这意味着在goroutine中使用循环变量时,每个goroutine将捕获其迭代的变量,而不是共享同一个变量。这一变化可能会对现有代码的行为产生影响,因此Go团队提供了一个工具来检测代码中可能因此特性变化而产生问题的地方。 range支持整型表达式:在Go 1.22中,for range循环的range表达式除了支持传统的数组、切片、map、channel等类型外,还支持整型表达式。这意味着你可以在for range循环中使用整型值,循环将基于该整型值进行迭代。 性能提升:Go 1.22在运行时进行了内存优化,提高了CPU性能(约1-3%),并减少了大多数Go程序的内存开销(约1%)。此外,Go 1.21引入的profile-guided optimization(PGO)功能在1.22版本中得到了进一步改进,包括改进的devirtualization,允许更多的接口方法调用进行静态调度,从而提高了程序性能。 标准库新增内容: 引入了一个新的math/rand/v2包,提供更清晰、更一致的API,并使用更高质量、更快的伪随机生成算法。 net/http.ServeMux的 patterns 现在接受方法和通配符,例如可以匹配仅限GET请求的GET /task/{id}/模式。 database/sql包中新增了一个Null[T]类型,用于扫描可为空的列。 在slices包中添加了一个Concat函数,用于连接任意类型的多个切片。 工具链: 在Go工具链改善方面,首当其冲的要数go module相关工具了。 在Go 1.22中,go work增加了一个与go mod一致的特性:支持vendor。通过go work vendor,可以将workspace中的依赖放到vendor目录下,同时在构建时,如果module root下有vendor目录,那么默认的构建是go build -mod=vendor,即基于vendor的构建。 go mod init在Go 1.22中将不再考虑GOPATH时代的包依赖工具的配置文件了,比如Gopkg.lock。在Go 1.22版本之前,如果go module之前使用的是类似dep这样的工具来管理包依赖,go mod init会尝试读取dep配置文件来生成go.mod。 go vet工具取消了对loop变量引用的警告,增加了对空append的行为的警告(比如:slice = append(slice))、增加了deferring time.Since的警告以及在log/slog包的方法调用时key-value pair不匹配的警告。 GO 1.21 新特性发布时间:2023.08.08 官方说明:Go 1.21 Release Notes - The Go Programming Language 特性: go1.21.1(发布于 2023 年 9 月 6 日)包括对cmd/go、crypto/tls和html/template包的四个安全修复,以及对编译器、go命令、链接器、运行时和context、crypto/tls、encoding/gob、encoding/xml、go/types、net/http、os和 的错误修复path/filepath包。 go1.21.2(2023 年 10 月 5 日发布)包括对包的一项安全修复cmd/go,以及对编译器、go命令、链接器、运行时和包的错误修复runtime/metrics。 go1.21.3(2023 年 10 月 10 日发布)包含对该net/http软件包的安全修复。 GO 1.20 新特性发布时间:2023.02.01 官方说明:Go 1.20 Release Notes - The Go Programming Language 特性: 支持将slice直接转为数组 Comparable类型可比较 unsafe包添加Slice,SliceData,String,StringData 4个函数 可移植性:Go 1.20将会成为支持macOS 10.13 High Sierra和10.14 Mojave的最后一个版本。 Go 1.20增加了对于RISC-V架构在FreeBSD操作系统的实验性支持 PGO引入 标准库加强 新增了几个 时间转换格式常量 新包 crypto/ecdh 支持通过 NIST 曲线和 Curve25519 椭圆曲线 Diffie-Hellman 密钥交换 新类型 http.ResponseController 访问 http.ResponseWriter 接口未处理的扩展请求 httputil.ReverseProxy 包含一个新的 Rewrite 钩子函数,取代了之前的 Director 钩子 新方法 context.WithCancelCause 提供了一种方法来取消具有给定错误的上下文 os/exec.Cmd 结构体中的新字段 Cancel 和 WaitDelay, 指定 Cmd 在其关联的 Context 被取消或其进程退出时的回调 工具链 cover 工具可以收集整个程序的覆盖率,不仅仅是单元测试 go build、go install 和其他与构建相关的命令可以接收一个 -pgo 标志,启用配置文件引导优化,以及一个 -cover 标志,用于整个程序覆盖率分析 go test -json 的实现已得到改进,可以处理复杂多样的 Stdout 输出 vet 在并行运行的测试中可能会发生更多循环变量引用错误 在没有 C 工具链 的系统上默认禁用 CGO 性能提升 编译器和 GC 的优化减少了内存开销,并将 CPU 性能整体提高了 2% 针对编译时间进行了优化,提升了 10%。使得构建速度与 Go 1.17 保持一致 (恢复到了泛型之前的速度) Go 发行版瘦身,新版本起,Go 的 $GOROOT/pkg 目录将不再存储标准库的预编译包存档,Go 发行版的将迎来一轮瘦身 GO 1.19 新特性时间:2022.05 官方说明:Go 1.19 Release Notes - The Go Programming Language 主要特性: 泛型问题fix 修订Go memory model:对Go memory model做了更正式的整体描述,增加了对multiword竞态、runtime.SetFinalizer、更多sync类型、atomic操作以及编译器优化方面的描述。 修订go doc comment格式:增加了对超链、列表、标题、标准库API引用等格式支持 新增runtime.SetMemoryLimit和GOMEMLIMIT环境变量:避免Go程序因分配heap过多,超出系统内存资源限制而被kill,默认memory limit是math.MaxInt64,limit限制的是go runtime掌控的内存总量,对于开发者自行从os申请的内存(比如通过mmap)则不予考虑。 启动时将默认提高打开文件的限值:对于导入os包的go程序,Go将在1.19中默认提高这些限制值到hard limit。 race detector将升级到v3版thread sanitizer:race detector性能相对于上一版将提升1.5倍-2倍,内存开销减半,并且没有对goroutine的数量的上限限制 增加”unix” build tag://go:build unix 标准库net包使用EDNS 标准库flag包增加TextVar函数 正式支持64位龙芯cpu架构 (GOOS=linux, GOARCH=loong64) 当Go程序空闲时,Go GC进入到周期性的GC循环的情况下(2分钟一次),Go运行时现在会在idle的操作系统线程上安排更少的GC worker goroutine,减少空闲时Go应用对os资源的占用。 Go行时将根据goroutine的历史平均栈使用率来分配初始goroutine栈,避免了一些goroutine的最多2倍的goroutine栈空间浪费。 sync/atomic包增加了新的高级原子类型Bool, Int32, Int64, Uint32, Uint64, Uintptr和Pointer,提升了使用体验。 Go编译器使用jump table重新实现了针对大整型数和string类型的switch语句,平均性能提升20%左右。 等 Go 1.18 新特性时间:2022.03 官方说明:Go 1.18 Release Notes - The Go Programming Language 主要特性: 泛型支持 Workspaces 工作区 Go编译器与Go module变化:修正的语法bug,在AMD64平台上引入architectural level,为ARM64架构带来高达 20% 的 CPU 性能改进:但由于编译器中与支持泛型有关的变化,Go 1.18 的编译速度可能比Go 1.17的编译速度大约慢15%。编译后的代码的执行时间不受影响。打算在Go 1.19中提高编译器的速度。Go 1.18明确了能修改go.mod、go.sum的命令只有三个:go get、go mod tidy和go mod download。 go fuzzing test:将fuzz testing纳入了go test工具链,与单元测试、性能基准测试等一起成为了Go原生测试工具链中的重要成员,单元测试函数名样式:FuzzXxx go get 不再执行编译和安装工作 gofmt支持并发 内置函数Append对切片的扩容算法发生变化:和Go 1.17以1024作为大小分界不同,Go 1.18使用256作为threshold 新增net/netip包 tls client默认将使用TLS 1.2版本 crypto/x509包默认将拒绝使用SHA-1哈希函数签名的证书(自签发的除外) strings包和bytes包新增Cut函数 runtime/pprof精确性提升 sync包新增Mutex.TryLock、RWMutex.TryLock和RWMutex.TryRLock 等 Go 1.17 新特性时间:2021.08 官方说明:Go 1.17 Release Notes - The Go Programming Language 主要特性: 从切片到数组指针的转换: []T 类型的表达式 s 现在可以转换为数组指针类型 *[N]T go modules 支持“修剪模块图”(Pruned module graphs):go mod tidy -go=1.17 编译器带来了额外的改进:即一种传递函数参数和结果的新方法,程序性能提高了约 5%,amd64 平台的二进制大小减少了约 2%。 unsafe包新增了unsafe.Add和unsafe.Slice go.mod 中添加 // Deprecated: 注释来弃用模块 net包: url参数解析增加对“;”的支持变化(原先 example?a=1;b=2&c=3 会解析成 map[a:[1] b:[2] c:[3]], 现在解析成map[c:[3]]) 增加 IP.IsPrivate 判断私有 IP a.b.c.d 格式的 ip v4 地址不允许每段有前缀 0(因为某些系统会认为前缀 0 表示 8进制) 等 Go 1.16 新特性时间:2021.02 官方说明:Go 1.16 Release Notes - The Go Programming Language 主要特性: GO111MODULE 默认为 on 支持编译阶段将静态资源文件打包进编译好的程序中,并提供访问这些文件的能力://go:embed Go 1.15 新特性时间:2020.08 官方说明:Go 1.15 Release Notes - The Go Programming Language 主要特性: 改进了对高核心数的小对象的分配 编译器/汇编器/链接器的优化:二进制大小减少了约 5%,减少了链接器资源的使用(时间和内存)并提高了代码的稳健性/可维护性。 内置了time/tzdata包:允许将时区数据库嵌入到程序中 等 Go 1.14 新特性时间:2020.02 官方说明:Go 1.14 Release Notes - The Go Programming Language 主要特性: Go Module已可用于生产使用 嵌入具有重叠方法集的接口 改进了defer的性能 goroutines 异步可抢占 页面分配器更高效 内部定时器更高效 等 Go 1.13 新特性时间:2019.09 官方说明:Go 1.13 Release Notes - The Go Programming Language 主要特性: 优化sync.Pool sync 包的 Pool 组件得到改进,得其中的资源不会在垃圾回收时被清除(通过新机制里引入的缓存,两次垃圾回收之间没有被使用过的实例才会被清除) 重了逃逸分析逻辑,使得 Go 程序减少了堆上的分配次数 go 命令默认使用 Go module mirror and Go checksum database下载和验证模块 对数字文字的改进 错误换行 默认开启 TLS 1.3 等 Go 1.12 新特性时间:2019.02 官方说明:Go 1.12 Release Notes - The Go Programming Language 主要特性: 改进了Go modules 在analysis包基础上重写了 go vet 命令 等 Go 1.11 新特性时间:2018.08 官方说明:Go 1.11 Release Notes - The Go Programming Language 主要特性: Go modules Go 1.10 新特性时间:2018.02 官方说明:Go 1.10 Release Notes - The Go Programming Language 主要特性: go test with cache:go test命令可以缓存测试结果 go build 命令会缓存最近构建过的包,从而加快了构建过程 明确预声明类型(predeclared type)是defined type还是alias type 移除spec中对method expression: T.m中T的类型的限制 默认的GOROOT 增加GOTMPDIR变量 通过cache实现增量构建,提高go tools性能 go tool pprof做了一个较大的改变:增加了Web UI 标准库新增strings.Builder 标准库bytes包的几个方法Fields, FieldsFunc, Split和SplitAfter在底层实现上有变化,使得外部展现的行为有所变化 等 Go 1.9 新特性时间:2017.08 官方说明:Go 1.9 Release Notes - The Go Programming Language 主要特性: 提升了垃圾收集器和编译器 增加了类型别名 新增了sync.Map time包更加安全 testing包新增helper方法 支持渐进式代码重构 引入了类型别名并提升了运行时和工具支持 Go 1.8 新特性时间:2017.02 官方说明:Go 1.8 Release Notes - The Go Programming Language 主要特性: 优化编译 CPU 时间在 32 位 ARM 系统上减少了 20-30%, 还针对 64 位 x86 系统进行了一些适度的性能改进。编译器和链接器变得更快。 编译时间应该比 Go 1.7 改进了大约 15% Go 1.7中进入标准库的context,提供了取消和超时机制。 Go 1.8 让标准库中更多package使用(支持)context,包括 database/sql,net 包, net/http 包中的 Server.Shutdown等 对垃圾回收器改进,使两次垃圾回收的暂停时间减小到了毫秒级 同时识别了剩余仍未解决的暂停模式,并在下一个版本中得到修复。修复后,通常情况下暂停时间能控制在 100 微秒左右,甚至能低至 10 微秒。 改进了 defer 函数 部分标准库使用context包来改造 sort 包中新添加的 Slice 函数,对切片进行排序变得比之前简单得多 Go 1.7 新特性时间:2016.08 官方说明:Go 1.7 Release Notes - The Go Programming Language 主要特性: context包转正 编译时间显着加快:二进制文件大小减少了 20-30%, CPU 时间减少了 5-35% 垃圾收集器的加速和标准库的优化 go tool trace改进 Go 1.6 新特性时间:2016.02 官方说明:Go 1.6 Release Notes - The Go Programming Language 主要特性: 增加对于 HTTP/2 协议的默认支持 再一次降低了垃圾回收器的延迟 runtime改变了打印程序结束恐慌的方式。现在只打印发生panic的 goroutine 的堆栈,而不是所有现有的 goroutine 默认启用vendor目录 sort.Sort 内部的算法进行了改进,运行速度提高了约 10% Go 1.5 新特性时间:2015.08 官方说明:Go 1.5 Release Notes - The Go Programming Language 主要特性: 垃圾回收器被完全重新设计实现: 基于并发的回收期,GC延迟显著降低,来自Twitter生产案例从300ms下降到30ms 调度程序的相关改进允许将默认的 GOMAXPROCS 值(并发执行的 goroutine 的数量)从 1 更改为逻辑 CPU 的数量。在以前的版本中,默认值为 1 go tool trace:可以在运行时可视化跟踪程序,追踪信息可在测试或运行期间生成,展示在浏览器窗口中 map语法的更改:由于疏忽,允许从slice literals中省略元素类型的规则未应用于map。在1.5版本得到了修正,以下两种定义map的方式从1.5及之后都可以(即可以省略Point的类型) Go 1.4 新特性时间:2014.02 官方说明:Go 1.4 Release Notes - The Go Programming Language 主要特性: For-range loops支持新语法 1234567891011121314151617 package mainimport “fmt”func main() { sli := []string{“shandong”, “zhejiang”, “guangdong”, “jiangsu”} for k, v := range sli { fmt.Println(“k-v:”, k, v) //go 1.3及之前的For-range loops } for range sli { fmt.Println(“从1.4开始这种写法是可以通过编译的”) }} Android 的官方支持包golang.org/x/mobile随该版本一同发布,使开发者可以仅用 Go 代码编写简单的 Android 应用。 之前用 C 和汇编语言编写的大多数运行时已转换为用 Go 语言实现 && 使用了更精准的垃圾收集器,堆栈大小减少了 10~30% 发布 go generate 命令,此命令会扫描//go:generate 指令提供的信息生成代码,简化了代码生成的方式。 Generating code 引入了Internal包 Go 的项目代码管理工具从 Mercurial 切换为 Git,与此同时,项目也从 Google Code 迁移到了 Github 上 Go 1.3 新特性时间:2014.06 官方说明:Go 1.3 Release Notes - The Go Programming Language 主要特性: 堆栈管理得到了重要改善 发布了 sync 包的 Pool 组件 改进了channel的实现,提升了性能 Go 1.2 新特性时间:2013.12 官方说明:Go 1.2 Release Notes - The Go Programming Language 主要特性: Three-index slices go test 命令支持代码覆盖率报告,并提供新的 go tool cover 命令输出代码测试覆盖率的统计信息. The cover story Go 1.1 新特性时间:2013.05 官方说明:Go 1.1 Release Notes - The Go Programming Language 主要特性: 增强语言特性(编译器、垃圾回收机制、映射、goroutine 调度器)与性能。 Go 1.0 新特性时间:2012.03 官方说明:Go 1 Release Notes - The Go Programming Language 主要特性: 承诺兼容性,确保向后兼容 Go 1 and the Future of Go Programs - The Go Programming Language 本文转自 https://blog.csdn.net/mdpets/article/details/127663206,如有侵权,请联系删除。","categories":[{"name":"Go基础","slug":"Go基础","permalink":"http://coderedeng.github.io/categories/Go%E5%9F%BA%E7%A1%80/"}],"tags":[{"name":"Go基础","slug":"Go基础","permalink":"http://coderedeng.github.io/tags/Go%E5%9F%BA%E7%A1%80/"}]},{"title":"并发","slug":"Go进阶 - 并发","date":"2023-10-20T12:50:13.000Z","updated":"2024-07-18T14:21:45.410Z","comments":true,"path":"2023/10/20/Go进阶 - 并发/","link":"","permalink":"http://coderedeng.github.io/2023/10/20/Go%E8%BF%9B%E9%98%B6%20-%20%E5%B9%B6%E5%8F%91/","excerpt":"","text":"1. 并发1.1 并发和并行的区别并发和并行是两个不同的概念: 并行意味着程序在任意时刻都是同时运行的; 并发意味着程序在单位时间内是同时运行的 1.1.1 并行并行就是在任一粒度时间内都具备同时执行的能力:简单来说并行就是多机或多台机器并行处理; SMP(SMP 是对称多处理器(Symmetric MultiProcessing)的简称。在这样的系统中包含多个处理器,同时,处理器间共享了内存和 I/O 总线。”对称”是指所有的处理器在功能和位置上地位相同,不存在主处理器或者被处理器较多的 “主机”) 表面上看是并行的,但由于是共享内存,以及线程间的同步等,不可能完全做到并行。 1.1.2 并发并发是在规定的时间内多个请求都得到执行和处理,强调的是给外界的感觉,实际上内部可能是分时操作的。并发重在避免阻塞,使程序不会因为一个阻塞而停止处理。并发典型的应用场景:分时操作系统就是一种并发设计(忽略多核 CPU)。 1.2 goroutinegoroutine是 Go 语言中处理并发执行的一个主要工具,是 Go 运行时层面的轻量级线程,与 OS 线程相比,它的开销更小。操作系统可以进行线程和进程的调度,本身具备并发处理能力,但进程切换代价还是过高,当操作系统在系统进程之间切换时,它需要保存当前正在运行进程的状态,以便在再次切换回该进程时恢复执行。这通常涉及保存进程的 “上下文”,即使该进程能够从中断点继续执行的所有信息(处理器的寄存器、内存管理信息、进程状态、输入和输出状态、资源使用情况等)。如果应用可以在用户态进行调度,应该可以更大限度地提升程序运行效率,goroutine就是基于这个思想实现的。 goroutine 示例,代码如下: var wg sync.WaitGroup // 第一步:定义一个计数器 func routine1() { for i := 0; i < 10; i++ { fmt.Println(\"routine1 你好golang-\", i) // routine1 你好golang-0, ...9 time.Sleep(time.Millisecond * 100) } wg.Done() //协程计数器-1 // 第三步:协程执行完毕,计数器-1 } func routine2() { for i := 0; i < 2; i++ { fmt.Println(\"routine2 你好golang-\", i) // routine2 你好golang-0, routine2 你好golang-1 time.Sleep(time.Millisecond * 100) } wg.Done() //协程计数器-1 } func main() { wg.Add(1) //协程计数器+1 第二步:开启一个协程计数器+1 go routine1() //表示开启一个协程 wg.Add(1) //协程计数器+1 go routine2() //表示开启一个协程 wg.Wait() //等待协程执行完毕... 第四步:计数器为0时推出 fmt.Println(\"主线程退出...\") } goroutine 有如下特性: go 的执行是非阻塞的,不会等待; go 后面函数的返回值会被忽略; 调度器不能保证多个 goroutine 的执行次序; 没有父子 goroutine 的概念,所有 goroutine 是平等地被调度和执行的; go 程序运行时会在 main 函数先创建一个 goroutine,其他 go 关键字创建的 goroutine 会另外创建; go 没有暴露 goroutine id 给用户,所以不能在一个 goroutine 里面显式地操作另一个 goroutine ,不过 runtime 包提供了一些函数访问和设置 goroutine 的相关信息; 1.2.1 GOMAXPROCSGOMAXPROCS( n int ) 用来设置或查询可以并发执行的 goroutine 数目,n 大于 1 表示设置 GOMAXPROCS 值,否则表示查询当前 GOMAXPROCS 的值。 1.2.2 GoexitGoexit() 是结束当前 goroutine 的运行, Goexit 在结束当前 goroutine 运行之前会调用当前 goroutine 已经注册的 defer 。 Goexit 并不会产生 panic ,所以该 goroutine defer 里面的 recover 调用都返回 nil 。 1.2.3 GoschedGosched() 是放弃当前调度执行机会,将当前 goroutine 放到队列中等待下次被调度。只有 goroutine 还是不够的,多个 goroutine 之间还需要通信、同步、协同等。 1.3 Chanchan 是 Go 语言里面的一个关键宇,是 channel 的简写,翻译为中文就是通道。 goroutine 是 Go 语言里面的并发执行体,通道是 goroutine 之间通信和同步的重要组件。 Go 的哲学是“不要通过共享内存来通信,而是通过通信来共享内存”(CSP(Communicating Sequential Processes)是一种用于设计并发系统的模型,它强调通过在独立的并发实体或“进程”之间传递消息来进行通信),通道是 Go 通过通信来共享内存的载体。例如: //创建一个无缓冲的通道,通道存放元素的类型为 datatype make(chan datatype ) //创建一个有 10 个缓冲的通道,通道存放元素的类型为 datatype make(chan datatype, 10) 通道分为无缓冲的通道和有缓冲的通道, Go 提供内置函数 len 和 cap ,无缓冲的通道的 len 和 cap 都是 0,有缓冲的通道的 len 代表没有被读取的元素数, cap 代表整个通道的容量。无缓冲的通道既可以用于通信,也可以用于两个 goroutine 的同步,有缓冲的通道主要用于通信。有缓冲通道示例: var m sync.Mutex func main(){ m.Lock() // 互斥锁 c := make(chan int ,100) go func() { defer m.Unlock() // 解锁 for i := 0; i < 100; i++{ c <- i // 向 c 通道传递数据 } close(c) }() m.Lock() // 等到互斥锁解锁,然后再次锁定用来阻塞主程序。 for v := range c { // 向已关闭的通道遍历读取数据 fmt.Println(v) } } 写到缓冲通道中的数据不会消失,它还可以缓冲和适配两个 goroutine 处理速率不一致的情况,缓冲通道和消息队列类似,有削峰和增大吞吐量的功能。 操作不同状态的 chan 会引发三种行为: panic 向已经关闭的通道写数据会导致 panic ;最佳实践是由写入者关闭通道,能最大程度地避免向已经关闭的通道写数据而导致的 panic; 重复关闭的通道会导致 panic; 阻塞 向未初始化的通道写数据或读数据都会导致当前 goroutine 的永久阻塞; 向缓冲区己满的通道写入数据会导致 goroutine 阻塞; 通道中没有数据,读取该通道会导致 goroutine 阻塞; 非阻塞 读取己经关闭的通道不会引发阻塞,而是立即返回通道元素类型的零值,可以使用 comrna , ok 语法判断通道是否己经关闭; 向有缓冲且没有满的通道读/写不会引发阻塞; 1.4 WaitGroupgoroutine 和 chan 一个用于并发,另一个用于通信。没有缓冲的通道具有同步的功能,除此之外, sync 包也提供了多个 goroutine 同步的机制,主要是通过 WaitGroup 实现的。 主要数据结构和操作如下: type WaitGroup struct { // contains filtered or unexported fields } // 添加等待信号 func (wg *WaitGroup) Add(delta int) // 释放等待信号 func (wg *WaitGroup) Done() // 等待 func (wg *WaitGroup) Wait() WaitGroup 用来等待多个 goroutine 完成, main goroutine 调用 Add 设置需要等待 goroutine 的数目,每一个 goroutine 结束时调用 Done(), Wait() 被 main 用来等待所有的 goroutine 完成。 1.5 selectselect 是类 UNIX 系统提供的一个多路复用系统 API, Go 语言借用多路复用的概念,提供了 select 关键字,用于多路监听多个通道。当监听的通道没有状态是可读或可写的, select 是阻塞的;只要监听的通道中有一个状态是可读或可写的,则 select 就不会阻塞,而是进入处理就绪通道的分支流程。如果监听的通道有多个可读或可写的状态, 则 select 随机选取一个处理。 func main() { ch : = make(chan int , 1) go func(chan int) { for { select { //0 或 1 的写入是随机的 case ch < - 0 : case ch <- 1 : } } } (ch) for i : = 0; i < 10;i++ { println(<-ch) } } // 运行结果 0 0 1 0 0 1 0 1 1 0 1.6 扇入( Fan in )和扇出( Fan out )编程中经常遇到 “扇入和扇出” 两个概念,所谓的扇入是指将多路通道聚合到一条通道中处理,Go 语言最简单的扇入就是使用 select 聚合多条通道服务;所谓的扇出是指将一条通道发散到多条通道中处理,在 Go 语言里面具体实现就是使用 go 关键字启动多个 goroutine 并发处理。 中国有句经典的哲学名句叫 “分久必合,合久必分” 软件的设计和开发也遵循同样的哲学思想,扇入就是合,扇出就是分。当生产者的速度很慢时,需要使用扇入技术聚合多个生产者满足消费者, 比如很耗时的加密/解密服务;当消费者的速度很慢时,需要使用扇出技术,比如Web 服务器并发请求处理。扇入和扇出是 Go 并发编程中常用的技术。 1.6.1 扇入(Fan-In):func fanIn(input1, input2 <-chan string) <-chan string { c := make(chan string) go func() { for { select { case s := <-input1: c <- s case s := <-input2: c <- s } } }() return c } 扇入指的是将多个输入 channel 合并到一个 channel 中,扇出是将一个输入 channel 分散给多个 worker 进行处理。 1.6.2 扇出(Fan-Out):func fanOut(input <-chan string, workerCount int) []<-chan string { var outputs []<-chan string for i := 0; i < workerCount; i++ { outputs = append(outputs, createWorker(input)) } return outputs } func createWorker(input <-chan string) <-chan string { c := make(chan string) go func() { for n := range input { c <- doWork(n) } close(c) }() return c } func doWork(n string) string { //...执行一些操作... return n } 在以上扇出的例子中,input是输入channel,在fanOut函数中,我们根据workerCount创建相同数量的Worker来处理输入信息。每个Worker处理的任务是从输入channel读取信息,然后进行一些工作(在doWork函数中定义),然后将信息写入自己的输出channel中。Workers的输出channel会被添加到outputs切片中,并从fanOut函数返回。 1.6.3 扇入扇出分别对应的应用场景扇入和扇出的概念常用在处理并发和流处理系统中,它们各自有一些常见的应用场景: (1)扇入(Fan-In)扇入是将来自多个源的数据聚合到一个通道中,这种方式常用于多个并行或异步任务完成时集中处理结果,如: 对来自多个源的日志或状态更新聚合到一个处理者,以实现统一的日志记录、分析或监控。 在分布式计算的上下文中,多个节点可能正在并行处理任务,并在完成时将结果发送回中央节点以进行聚合和处理。 (2)扇出(Fan-Out)扇出是将数据从一个源分发到多个接收者的过程,每个接收者都会得到完整的数据拷贝,扇出可以提高处理或任务的吞吐量。具体应用可能包括: 在负载均衡的上下文中,扇出通常用作一种将任务分发到多个工作节点的手段以提高整体处理速度,每个节点处理部分工作负载。 在自然语言处理或图像处理等领域,可以使用扇出来并行训练或运行多个模型,然后比较各自的输出以确定最优解。 扇出模式还可以用于数据备份和冗余存储的场景。比如,我们可以将一个流量的数据同时发送到多个存储节点,以此达到数据的备份和冗余保障。 1.7 通知退出机制读取己经关闭的通道不会引起阻塞,也不会导致 panic ,而是立即返回该通道存储类型的零值。关闭 select 监听的某个通道能使 select 立即感知这种通知,然后进行相应的处理,这就是所谓的退出通知机制(close channel to broadcast )。下面通过一个随机数生成器的示例演示退出通知机制,下游的消费者不需要随机数时,显式地通知生产者停止生产。 // GenerateintA 是一个随机数生成器 func GenerateintA(done chan struct{}) chan int { ch := make(chan int) go func() { Label: for { select { case ch <- rand.Int(): case <-done: break Label } } close(ch) }() return ch } func main() { done := make(chan struct{}) ch := GenerateintA(done) fmt.Println(<-ch) fmt.Println(<-ch) close(done) fmt.Println(<-ch) fmt.Println(<-ch) println(\"NumGoroutine=\", runtime.NumGoroutine()) } // 输出结果 146870834388028874 7216694335601338127 0 0 NumGoroutine= 1 1.8 并发范式通过具体的程序示例来演示 Go 语言强大的并发处理能力,每个示例代表一个并发处理范式,这些范式具有典型的特征,在真实的程序中稍加改造就能使用。 1.8.1 生成器在应用系统编程中,常见的应用场景就是调用一个统一的全局的生成器服务, 用于生成全局事务号、订单号、序列号和随机数等。 Go 对这种场景的支持非常简单,下面以一个随机数生成器为例来说明。 最简单的带缓冲的生成器。 例如: // RandomNumber 是一个随机数生成器 func RandomNumber() chan int { ch := make(chan int, 10) // 启动一个 go routine 用于生成随机数,函数返回一个通道用于获取随机数 go func() { for { ch <- rand.Int() } }() return ch } func main() { ch := RandomNumber() fmt.Println(<-ch) fmt.Println(<-ch) } // 输出结果 8442295699646266936 6343099628820528177 多个 goroutine 增强型生成器。 例如: // RandomNumber1 是一个随机数生成器 func RandomNumber1() chan int { ch := make(chan int) // 启动一个 go routine 用于生成随机数,函数返回一个通道用于获取随机数 go func() { for { ch <- rand.Int() } }() return ch } // RandomNumber2 是一个随机数生成器 func RandomNumber2() chan int { ch := make(chan int) // 启动一个 go routine 用于生成随机数,函数返回一个通道用于获取随机数 go func() { for { ch <- rand.Int() } }() return ch } func GenerateInt() chan int { ch := make(chan int, 20) go func() { for { select { case ch <- <-RandomNumber1(): case ch <- <-RandomNumber2(): } } }() return ch } func main() { ch := GenerateInt() for i := 0; i < 100; i++ { fmt.Println(<-ch) } } // 输出结果 4732711589376798349 5980361011433472918 8507484322095864034 ...... 1.8.2 管道通道可以分为两个方向,一个是读,另一个是写,假如一个函数的输入参数和输出参数都是相同的 chan 类型, 则该函数可以调用自己,最终形成一个调用链。当然多个具有相同参数类型的函数也能组成一个调用链,这很像 UNIX 系统的管道,是一个有类型的管道。 下面通过具体的示例演示 Go 程序这种链式处理能力: package main import ( \"fmt\" ) // chain 函数的输入参数和输出参数类型相同,都是 chan int 类型 // chain 函数的功能是将 chan 内的数据统一加1 func chain(in chan int) chan int { out := make(chan int) go func(){ for v := range in{ out <- 1 + v } close(out) }() return out } func main() { in := make(chan int) go func() { for i := 0; i < 10; i++ { in <- i } close(in) }() // 连续调用 3 次 chan,相当于把 in 中的每个元素都加 3 out := chain(chain(chain(in))) for v := range out { fmt.Println(v) } } 1.8.3 每个请求一个 goroutine下面以计算 100 个自然数的和来举例,将计算任务拆分为多个 task,每个 task 启动一个 goroutine 进行处理,程序示例代码如下: package main import ( \"fmt\" \"sync\" ) // 工作任务 type task struct { begin int end int result chan<- int } // 任务执行:计算 begin 到 end 的和 // 执行结果写入到结果 chan result 中 func (t *task) do() { sum := 0 for i := t.begin; i <= t.end; i++ { sum += i } t.result <- sum } // 构建 task 并写入到 task 通道 func InitTask(taskchan chan<- task, r chan int, p int) { qu := p / 10 mod := p % 10 high := qu * 10 for j := 0; j < qu; j++ { b := 10*j + 1 e := 10 * (j + 1) tsk := task{ begin: b, end: e, result: r, } taskchan <- tsk } if mod != 0 { tsk := task{ begin: high + 1, end: p, result: r, } taskchan <- tsk } close(taskchan) } // 读取 task chan ,每个 task 一个 worker goroutine 处理 // 并等待每个 task 运行完,关闭结果通道 func DistributeTask(taskchan <-chan task, wait *sync.WaitGroup, result chan int) { for v := range taskchan { wait.Add(1) go ProcessTask(v, wait) } wait.Wait() close(result) } // 工作 goroutine 处理具体工作,并将处理结构发送到结果通道 func ProcessTask(t task, wait *sync.WaitGroup) { t.do() wait.Done() } // 读取结果通道,汇总结果 func ProcessResult(resultchan chan int) int { sum := 0 for r := range resultchan { sum += r } return sum } func main() { // 创建任务通道 taskchan := make(chan task, 10) // 创建结果通道 resultchan := make(chan int, 10) // wait 用于同步等待任务的执行 wait := &sync.WaitGroup{} // 初始化 task 的 goroutine,计算 100 个自然数之和 go InitTask(taskchan, resultchan, 100) //每个 task 启动一个 goroutine 处理, go DistributeTask(taskchan, wait, resultchan) // 通过结果通道获取结果并汇总 sum := ProcessResult(resultchan) fmt.Println(\"sum=\", sum) } // 结果 sum= 5050 程序的逻辑分析:(1)InitTask 函数构建 task 并发送到 task 通道中;(2)分发任务函数 DistributeTask 为每个 task 启动一个 goroutine 处理任务, 等待其处理完成, 然后关闭结果通道;(3)ProcessResult 函数读取并统计所有的结果。这几个函数分别在不同的 goroutine 中运行, 它们通过通道和sync.WaitGroup 进行通信和同步; 1.8.4 固定 worker 工作池服务器编程中使用最多的就是通过线程池来提升服务的井发处理能力。在 Go 语言编程中,一样可以轻松地构建固定数目的 goroutines 作为工作线程池。下面还是以计算多个整数的和为例来说明这种并发范式。程序中除了主要的 main goroutine ,还开启了如下几类 goroutine:(1)初始化任务的 goroutme;(2)分发任务的 goroutine;(3)等待所有 worker 结束通知,然后关闭结果通道的 goroutine;main 函数负责拉起上述 goroutine ,并从结果通道获取最终的结果;程序采用三个通道,分别是:(1)传递 task 任务的通道;(2)传递 task 结果的通道;(3)接收 worker 处理完任务后所发送通知的通道;相关的代码如下: package main import ( \"fmt\" ) // 工作池的 goroutine 数目 const ( NUMBER = 10 ) // 工作任务 type task struct { begin int end int result chan<- int } // 任务处理:计算 begin 到 end 的和 // 执行结果写入到结果 chan result 中 func (t *task) do() { sum := 0 for i := t.begin; i <= t.end; i++ { sum += i } t.result <- sum } // 初始化待处理 task chan func InitTask(taskchan chan<- task, r chan int, p int) { qu := p / 10 mod := p % 10 high := qu * 10 for j := 0; j < qu; j++ { b := 10*j + 1 e := 10 * (j + 1) tsk := task{ begin: b, end: e, result: r, } taskchan <- tsk } if mod != 0 { tsk := task{ begin: high + 1, end: p, result: r, } taskchan <- tsk } close(taskchan) } // 读取 task chan 分发到 worker goroutine 处理,workers 的总的数量是 workers func DistributeTask(taskchan <-chan task, workers int, done chan struct{}) { for i := 0; i < workers; i++ { go ProcessTask(taskchan, done) } } // 工作 goroutine 处理具体工作,并将处理结构发送到结果 chan func ProcessTask(taskchan <-chan task, done chan struct{}) { for t := range taskchan { t.do() } done <- struct{}{} } // 通过 done channel 来同步等待所有工作 goroutine 的结束,然后关闭结果 chan func CloseResult(done chan struct{}, resultchan chan int, workers int) { for i := 0; i < workers; i++ { <-done } close(done) close(resultchan) } // 读取结果通道,汇总结果 func ProcessResult(resultchan chan int) int { sum := 0 for r := range resultchan { sum += r } return sum } func main() { workers := NUMBER // 工作通道 taskchan := make(chan task, 10) // 结果通道 resultchan := make(chan int, 10) // worker 信号通道 done := make(chan struct{}, 10) // 初始化 task 的 goroutine,计算 1000 个自然数之和 go InitTask(taskchan, resultchan, 1000) // 分发任务在 NUMBER 个 goroutine 池 DistributeTask(taskchan, workers, done) // 获取各个 goroutine 处理完任务的通知,并关闭结果通道 go CloseResult(done, resultchan, workers) // 通过结果通道处理结果 sum := ProcessResult(resultchan) fmt.Println(\"sum=\", sum) } // 结果 sum= 5050 程序的逻辑分析:(1)构建 task 并发送到 task 通道中;(2)分别启动 n 个工作线程,不停地从 task 通道中获取任务,然后将结果写入结果通道。如果任务通道被关闭,则负责向收敛结果的 goroutine 发送通知,告诉其当前 worker 已经完成工作;(3)收敛结果的 goroutine 接收到所有 task 己经处理完毕的信号后,主动关闭结果通道;(4)main 中的函数 ProcessResult 读取并统计所有的结果; 1.8.5 future 模式编程中经常遇到在一个流程中需要调用多个子调用的情况,这些子调用相互之间没有依赖,如果串行地调用,则耗时会很长,此时可以使用 Go 并发编程中的 future 模式。future 模式的基本工作原理:(1)使用 chan 作为函数参数;(2)启动 goroutine 调用函数;(3)通过 chan 传入参数;(4)做其他可以并行处理的事情;(5)通过 chan 异步获取结果;下面通过一段抽象的代码来学习该模式: package main import ( \"fmt\" \"time\" ) // 一个查询结构体 // 这里的 sql 和 result 是一个简单的抽象,具体的应用,可能是更复杂的数据类型 type query struct { // 参数 Channel sql chan string // 结果 Channel result chan string } // 执行 Query func execQuery(q query) { // 启动协程 go func() { // 获取输入 sql := <-q.sql // 访问数据库 // 输出结果通道 q.result <- \"result from \" + sql }() } func main() { // 初始化 Query q := query{make(chan string, 1), make(chan string, 1)} // 执行 Query,注意执行的时候无需准备参数 go execQuery(q) //准备参数 q.sql <- \"select * from table;\" // do otherthings time.Sleep(1 * time.Second) //获取结果 fmt.Println(<-q.result) } future 最大的好处是将函数的同步调用转换为异步调用, 适用于一个交易需要多个子调用且这些子调用没有依赖的场景。 实际情况可能比上面示例复杂得多,要考虑错误和异常的处理。","categories":[{"name":"Go进阶","slug":"Go进阶","permalink":"http://coderedeng.github.io/categories/Go%E8%BF%9B%E9%98%B6/"}],"tags":[{"name":"Go进阶","slug":"Go进阶","permalink":"http://coderedeng.github.io/tags/Go%E8%BF%9B%E9%98%B6/"}]},{"title":"regexp2","slug":"Go常用库介绍 - regexp2","date":"2022-06-23T14:02:41.000Z","updated":"2024-01-22T12:58:59.000Z","comments":true,"path":"2022/06/23/Go常用库介绍 - regexp2/","link":"","permalink":"http://coderedeng.github.io/2022/06/23/Go%E5%B8%B8%E7%94%A8%E5%BA%93%E4%BB%8B%E7%BB%8D%20-%20regexp2/","excerpt":"","text":"21.regexp201.regexp2 Regexp2:https://blog.csdn.net/dianxin113/article/details/118769094 GitHub:https://github.com/dlclark/regexp2 package main import ( \"fmt\" \"github.com/dlclark/regexp2\" ) func Regexp2GroupMatch(m *regexp2.Match, re *regexp2.Regexp) [][]string { var matches [][]string for m != nil { var ret []string gps := m.Groups() for index, g := range gps { if index == 0 { continue } ret = append(ret, g.Captures[0].String()) } matches = append(matches, ret) m, _ = re.FindNextMatch(m) } return matches } func CompileRegexp(regex string) (*regexp2.Regexp, error) { msgRegexp, e := regexp2.Compile(regex, 0) if e != nil { fmt.Println(e) } return msgRegexp, nil } func main() { str := \"2022-8-12 2023-8-11\" expr := \"(\\\\d{4})-(\\\\d{1,2})-(\\\\d{1,2})\" // [[2022 8 12]] reg, _ := CompileRegexp(expr) m, _ := reg.FindStringMatch(str) ret := Regexp2GroupMatch(m, reg) fmt.Println(ret) // [[2022 8 12] [2023 8 11]] }","categories":[{"name":"Go常用库","slug":"Go常用库","permalink":"http://coderedeng.github.io/categories/Go%E5%B8%B8%E7%94%A8%E5%BA%93/"}],"tags":[{"name":"Go常用库","slug":"Go常用库","permalink":"http://coderedeng.github.io/tags/Go%E5%B8%B8%E7%94%A8%E5%BA%93/"}]},{"title":"machinery","slug":"Go常用库介绍 - machinery","date":"2022-06-22T14:52:21.000Z","updated":"2024-01-22T12:58:59.000Z","comments":true,"path":"2022/06/22/Go常用库介绍 - machinery/","link":"","permalink":"http://coderedeng.github.io/2022/06/22/Go%E5%B8%B8%E7%94%A8%E5%BA%93%E4%BB%8B%E7%BB%8D%20-%20machinery/","excerpt":"","text":"20.machinery01.异步框架machinery github地址(opens new window) 1.1 machinery介绍 go machinery框架类似python中常用celery框架,主要用于 异步任务和定时任务,有一下特性 任务重试机制 延迟任务支持 任务回调机制 任务结果记录 支持Workflow模式:Chain,Group,Chord 多Brokers支持:Redis, AMQP, AWS SQS(opens new window) 多Backends支持:Redis, Memcache, AMQP, MongoDB(opens new window) 1.2 架构 任务队列,简而言之就是一个放大的生产者消费者模型 用户请求会生成任务,队列的处理器程序充当消费者不断的消费任务。 基于这种框架设计思想,我们来看下machinery的简单设计结构图例 Sender:业务推送模块,生成具体任务,可根据业务逻辑中,按交互进行拆分; Broker:存储具体序列化后的任务,machinery中目前支持到Redis, AMQP,和SQS; Worker:工作进程,负责消费者功能,处理具体的任务; Backend:后端存储,用于存储任务执行状态的数据; 02.machinery使用2.1 异步和定时任务package main import ( \"fmt\" redisbackend \"github.com/RichardKnop/machinery/v2/backends/redis\" redisbroker \"github.com/RichardKnop/machinery/v2/brokers/redis\" eagerlock \"github.com/RichardKnop/machinery/v2/locks/eager\" \"github.com/RichardKnop/machinery/v2\" \"github.com/RichardKnop/machinery/v2/config\" \"github.com/RichardKnop/machinery/v2/tasks\" \"os\" \"time\" ) func main() { if len(os.Args) == 2 && os.Args[1] == \"worker\" { // 启动worker if err := worker(); err != nil { panic(err) } } TestPeriodicTask() // 触发一个定时任务(定时任务由客户端控制,客户端退出定时就会结束) TestAdd() // 触发一个异步任务 time.Sleep(time.Second * 1000) } /* 触发执行Add异步任务 */ func TestAdd() { server, _ := startServer() // 调用异步任务 Add 函数,执行 1+4=5这个逻辑 signature := &tasks.Signature{ Name: \"add\", Args: []tasks.Arg{ { Type: \"int64\", Value: 4, }, { Type: \"int64\", Value: 1, }, }, } asyncResult, _ := server.SendTask(signature) // 任务可以通过将Signature的实例传递给Server实例来调用 results,_ := asyncResult.Get(time.Millisecond * 5) // 您还可以执行同步阻塞调用来等待任务结果 for _, result := range results { fmt.Println(result.Interface()) } } /* 触发执行periodicTask异步任务 */ func TestPeriodicTask() { server, _ := startServer() signature := &tasks.Signature{ Name: \"periodicTask\", Args: []tasks.Arg{ }, } // 每分钟执行一次periodicTask函数,验证发现不支持秒级别定时任务 err := server.RegisterPeriodicTask(\"*/1 * * * ?\", \"periodic-task\", signature) if err != nil { fmt.Println(err) } asyncResult, _ := server.SendTask(signature) fmt.Println(asyncResult) } // 第一:配置Server并注册任务 func startServer() (*machinery.Server, error) { cnf := &config.Config{ DefaultQueue: \"machinery_tasks\", ResultsExpireIn: 3600, Redis: &config.RedisConfig{ MaxIdle: 3, IdleTimeout: 240, ReadTimeout: 15, WriteTimeout: 15, ConnectTimeout: 15, NormalTasksPollPeriod: 1000, DelayedTasksPollPeriod: 500, }, } // 创建服务器实例 broker := redisbroker.NewGR(cnf, []string{\"localhost:6379\"}, 0) backend := redisbackend.NewGR(cnf, []string{\"localhost:6379\"}, 0) lock := eagerlock.New() server := machinery.NewServer(cnf, broker, backend, lock) // 注册异步任务 tasksMap := map[string]interface{}{ \"add\": Add, \"periodicTask\": PeriodicTask, } return server, server.RegisterTasks(tasksMap) } // 第二步:启动Worker func worker() error { //消费者的标记 consumerTag := \"machinery_worker\" server, err := startServer() if err != nil { return err } //第二个参数并发数, 0表示不限制 worker := server.NewWorker(consumerTag, 0) //钩子函数 errorhandler := func(err error) {} pretaskhandler := func(signature *tasks.Signature) {} posttaskhandler := func(signature *tasks.Signature) {} worker.SetPostTaskHandler(posttaskhandler) worker.SetErrorHandler(errorhandler) worker.SetPreTaskHandler(pretaskhandler) return worker.Launch() } // 第三步:添加异步执行函数 func Add(args ...int64) (int64, error) { println(\"############# 执行Add方法 #############\") sum := int64(0) for _, arg := range args { sum += arg } return sum, nil } // 第四步:添加一个周期性任务 func PeriodicTask() error { fmt.Println(\"################ 执行周期任务PeriodicTask #################\") return nil } 2.2 启动服务并发送任务 go run main.go worker // 启动worker服务 go run main.go // 发送任务到worker 03.gin+machinery3.0 项目结构 go run main.go // 直接执行即可测试 xiaonaiqiang1@ZBMac-C02CW08SM work % tree ginWorker ginWorker ├── main.go // 项目入库 └── pkg └── task ├── server.go // machinery服务初始化 ├── start.go // 启动异步任务入口 ├── cronJobs.go // 触发周期性任务 ├── sendJobs.go // 触发异任务 └── workers └── tasks.go // 定义执行任务函数 3.1 main.gopackage main import ( \"fmt\" \"ginWorker/pkg/task\" \"github.com/gin-gonic/gin\" \"net/http\" ) func main() { go task.Start() // 启动异步任务worker go task.StartCron() // 启动定时任务 r := gin.Default() r.GET(\"/add\", func(c *gin.Context) { task.TaskAdd(4,5) // 测试执行异步任务 c.String(http.StatusOK, \"hello word\") }) fmt.Println(\"http://127.0.0.1:8000\") //监听端口默认为8080 r.Run(\":8000\") } 3.2 pkg/task/server.gopackage task import ( \"ginWorker/pkg/task/workers\" \"github.com/RichardKnop/machinery/v2\" redisbackend \"github.com/RichardKnop/machinery/v2/backends/redis\" redisbroker \"github.com/RichardKnop/machinery/v2/brokers/redis\" \"github.com/RichardKnop/machinery/v2/config\" eagerlock \"github.com/RichardKnop/machinery/v2/locks/eager\" \"github.com/RichardKnop/machinery/v2/tasks\" ) var AsyncTaskCenter *machinery.Server // 第一:配置Server并注册任务 func startServer() (*machinery.Server, error) { cnf := &config.Config{ DefaultQueue: \"machinery_tasks\", ResultsExpireIn: 3600, Redis: &config.RedisConfig{ MaxIdle: 3, IdleTimeout: 240, ReadTimeout: 15, WriteTimeout: 15, ConnectTimeout: 15, NormalTasksPollPeriod: 1000, DelayedTasksPollPeriod: 500, }, } // 创建服务器实例 broker := redisbroker.NewGR(cnf, []string{\"localhost:6379\"}, 0) backend := redisbackend.NewGR(cnf, []string{\"localhost:6379\"}, 0) lock := eagerlock.New() server := machinery.NewServer(cnf, broker, backend, lock) tasksMap := initAsyncTaskMap() AsyncTaskCenter = server return server, server.RegisterTasks(tasksMap) } // 第二步:启动Worker func worker() error { consumerTag := \"machinery_worker\" //消费者的标记 server, err := startServer() if err != nil { return err } worker := server.NewWorker(consumerTag, 0) //第二个参数并发数, 0表示不限制 //钩子函数 errorhandler := func(err error) {} pretaskhandler := func(signature *tasks.Signature) {} posttaskhandler := func(signature *tasks.Signature) {} worker.SetPostTaskHandler(posttaskhandler) worker.SetErrorHandler(errorhandler) worker.SetPreTaskHandler(pretaskhandler) return worker.Launch() } // 第三步:注册函数 func initAsyncTaskMap() map[string]interface{} { tasksMap := map[string]interface{}{ \"add\": workers.Add, \"periodicTask\": workers.PeriodicTask, } return tasksMap } 3.3 pkg/task/start.gopackage task func Start() { // 启动worker if err := worker(); err != nil { panic(err) } } // 启动周期性任务 func StartCron() { TestPeriodicTask() } 3.4 pkg/task/cronJobs.gopackage task import ( \"fmt\" \"github.com/RichardKnop/machinery/v2/tasks\" ) /* 触发执行periodicTask异步任务 */ func TestPeriodicTask() { server, _ := startServer() signature := &tasks.Signature{ Name: \"periodicTask\", Args: []tasks.Arg{ }, } // 每分钟执行一次periodicTask函数,验证发现不支持秒级别定时任务 err := server.RegisterPeriodicTask(\"*/1 * * * ?\", \"periodic-task\", signature) if err != nil { fmt.Println(err) } asyncResult, _ := server.SendTask(signature) fmt.Println(asyncResult) } 3.5 pkg/task/sendJobs.gopackage task import ( \"fmt\" \"github.com/RichardKnop/machinery/v2/tasks\" ) /* 触发执行Add异步任务 */ func TaskAdd(a,b int64) { signature := &tasks.Signature{ Name: \"add\", Args: []tasks.Arg{ { Type: \"int64\", Value: a, }, { Type: \"int64\", Value: b, }, }, } _, err := AsyncTaskCenter.SendTask(signature) // 任务可以通过将Signature的实例传递给Server实例来调用 if err != nil { fmt.Println(err) } } 3.6 pkg/task/workers/tasks.gopackage workers import ( \"fmt\" \"time\" ) // 添加异步执行函数 func Add(args ...int64) (int64, error) { println(\"############# 执行Add方法 #############\") time.Sleep(10 * time.Second) // 模拟执行耗时任务 sum := int64(0) for _, arg := range args { sum += arg } println(\"############# Add方法Done #############\") return sum, nil } // 添加一个周期性任务 func PeriodicTask() error { fmt.Println(\"################ 执行周期任务PeriodicTask #################\") return nil } 3.7 运行结果 执行周期任务:每秒执行一次 通过接口触发异步任务 http://127.0.0.1:8000/add","categories":[{"name":"Go常用库","slug":"Go常用库","permalink":"http://coderedeng.github.io/categories/Go%E5%B8%B8%E7%94%A8%E5%BA%93/"}],"tags":[{"name":"Go常用库","slug":"Go常用库","permalink":"http://coderedeng.github.io/tags/Go%E5%B8%B8%E7%94%A8%E5%BA%93/"}]},{"title":"cron","slug":"Go常用库介绍 - cron","date":"2022-06-21T13:24:57.000Z","updated":"2024-01-22T12:58:59.000Z","comments":true,"path":"2022/06/21/Go常用库介绍 - cron/","link":"","permalink":"http://coderedeng.github.io/2022/06/21/Go%E5%B8%B8%E7%94%A8%E5%BA%93%E4%BB%8B%E7%BB%8D%20-%20cron/","excerpt":"","text":"19.cron定时01.cron基本使用1.1 使用举例package main import ( \"fmt\" \"github.com/robfig/cron\" ) //主函数 func main() { cron2 := cron.New() //创建一个cron实例 //执行定时任务(每5秒执行一次) err:= cron2.AddFunc(\"*/5 * * * * *\", print5) if err!=nil{ fmt.Println(err) } //启动/关闭 cron2.Start() defer cron2.Stop() select { //查询语句,保持程序运行,在这里等同于for{} } } //执行函数 func print5() { fmt.Println(\"每5s执行一次cron\") } 1.2 配置┌─────────────second 范围 (0 - 60) │ ┌───────────── min (0 - 59) │ │ ┌────────────── hour (0 - 23) │ │ │ ┌─────────────── day of month (1 - 31) │ │ │ │ ┌──────────────── month (1 - 12) │ │ │ │ │ ┌───────────────── day of week (0 - 6) │ │ │ │ │ │ │ │ │ │ │ │ * * * * * * 1.3 多个crontab任务package main import ( \"fmt\" \"github.com/robfig/cron\" ) type TestJob struct { } func (this TestJob) Run() { fmt.Println(\"testJob1...\") } type Test2Job struct { } func (this Test2Job) Run() { fmt.Println(\"testJob2...\") } //启动多个任务 func main() { c := cron.New() spec := \"*/5 * * * * ?\" //AddJob方法 c.AddJob(spec, TestJob{}) c.AddJob(spec, Test2Job{}) //启动计划任务 c.Start() //关闭着计划任务, 但是不能关闭已经在执行中的任务. defer c.Stop() select {} } /* testJob1... testJob2... testJob1... testJob2... */ 02.gin框架cron应用 目录结构 . ├── main.go └── pkg └── jobs ├── job_cron.go // 分布式任务配置 └── test_task.go // 具体任务实例 2.1 main.gopackage main import ( \"go_cron_demo/pkg/jobs\" \"net/http\" \"github.com/gin-gonic/gin\" ) func main() { jobs.InitJobs() r := gin.Default() r.GET(\"/\", func(c *gin.Context) { c.String(http.StatusOK, \"hello World!\") }) r.Run(\":8000\") } 2.2 pkg/jobs/job_cron.gopackage jobs import ( \"github.com/robfig/cron\" ) var mainCron *cron.Cron func init() { mainCron = cron.New() mainCron.Start() } func InitJobs() { // 每5s钟调度一次,并传参 mainCron.AddJob( \"*/5 * * * * ?\", TestJob{Id: 1, Name: \"zhangsan\"}, ) } /* 运行结果 1 zhangsan testJob1... 1 zhangsan testJob1... */ 2.3 pkg/jobs/test_task.gopackage jobs import \"fmt\" type TestJob struct { Id int Name string } func (this TestJob) Run() { fmt.Println(this.Id, this.Name) fmt.Println(\"testJob1...\") }","categories":[{"name":"Go常用库","slug":"Go常用库","permalink":"http://coderedeng.github.io/categories/Go%E5%B8%B8%E7%94%A8%E5%BA%93/"}],"tags":[{"name":"Go常用库","slug":"Go常用库","permalink":"http://coderedeng.github.io/tags/Go%E5%B8%B8%E7%94%A8%E5%BA%93/"}]},{"title":"logrus","slug":"Go常用库介绍 - logrus","date":"2022-06-20T13:54:53.000Z","updated":"2024-01-22T12:58:59.000Z","comments":true,"path":"2022/06/20/Go常用库介绍 - logrus/","link":"","permalink":"http://coderedeng.github.io/2022/06/20/Go%E5%B8%B8%E7%94%A8%E5%BA%93%E4%BB%8B%E7%BB%8D%20-%20logrus/","excerpt":"","text":"18.logrus01.logrus基础 参考GitHub(opens new window) 参考博客1(opens new window) 参考博客2(opens new window) 安装 go get github.com/sirupsen/logrus 1.1 简介 Logrus是Go(golang)的结构化logger,与标准库logger完全API兼容,它有以下特点 完全兼容标准日志库,拥有七种日志级别:Trace, Debug, Info, Warning, Error, Fataland Panic。 可扩展的Hook机制,允许使用者通过Hook的方式将日志分发到任意地方 如本地文件系统,logstash,elasticsearch或者mq等,或者通过Hook定义日志内容和格式等 可选的日志输出格式,内置了两种日志格式JSONFormater和TextFormatter,还可以自定义日志格式 Field机制,通过Filed机制进行结构化的日志记录 线程安全 1.2 简单导报使用package main import ( log \"github.com/sirupsen/logrus\" ) func main() { log.WithFields(log.Fields{ \"animal\": \"dog\", }).Info(\"测试info日志\") } // INFO[0000] 测试info日志 animal=dog 1.3 日志级别package main import ( \"github.com/sirupsen/logrus\" ) // 创建一个新的logger实例。可以创建任意多个。 var log = logrus.New() func main() { log.Trace(\"Something very low level.\") log.Debug(\"Useful debugging information.\") log.Info(\"Something noteworthy happened!\") log.Warn(\"You should probably take a look at this.\") log.Error(\"Something failed but I'm not quitting.\") // 记完日志后会调用os.Exit(1) log.Fatal(\"Bye.\") // 记完日志后会调用 panic() log.Panic(\"I'm bailing.\") } /* INFO[0000] Something noteworthy happened! WARN[0000] You should probably take a look at this. ERRO[0000] Something failed but I'm not quitting. FATA[0000] Bye. */ 1.4 设置日志级别// 会记录info及以上级别 (warn, error, fatal, panic) log.SetLevel(log.InfoLevel) 1.5 字段 Logrus鼓励通过日志字段进行谨慎的结构化日志记录,而不是冗长的、不可解析的错误消息。 例如,区别于使用log.Fatalf("Failed to send event %s to topic %s with key %d") 你应该使用如下方式记录更容易发现的内容 package main import ( log \"github.com/sirupsen/logrus\" ) func main() { log.WithFields(log.Fields{ \"event\": \"event\", \"topic\": \"topic\", \"key\": \"key\", }).Fatal(\"Failed to send event\") } // FATA[0000] Failed to send event event=event key=key topic=topic 1.6 默认字段 通常,将一些字段始终附加到应用程序的全部或部分的日志语句中会很有帮助。 例如,你可能希望始终在请求的上下文中记录request_id和user_ip。 区别于在每一行日志中写上log.WithFields(log.Fields{"request_id": request_id, "user_ip": user_ip}) 你可以向下面的示例代码一样创建一个logrus.Entry去传递这些字段。 package main import log \"github.com/sirupsen/logrus\" func main() { requestLogger := log.WithFields(log.Fields{\"request_id\": \"request_id\", \"user_ip\": \"user_ip\"}) requestLogger.Info(\"something happened on that request\") // will log request_id and user_ip requestLogger.Warn(\"something not great happened\") } /* INFO[0000] something happened on that request request_id=request_id user_ip=user_ip WARN[0000] something not great happened request_id=request_id user_ip=user_ip */ 1.7 Hooks 你可以添加日志级别的钩子(Hook)。 例如,向异常跟踪服务发送Error、Fatal和Panic、信息到StatsD或同时将日志发送到多个位置,例如syslog。 Logrus配有内置钩子,在init中添加这些内置钩子或你自定义的钩子 GitHub参考(opens new window) package main import ( log \"github.com/sirupsen/logrus\" \"gopkg.in/gemnasium/logrus-airbrake-hook.v2\" // the package is named \"airbrake\" logrus_syslog \"github.com/sirupsen/logrus/hooks/syslog\" \"log/syslog\" ) func init() { // Use the Airbrake hook to report errors that have Error severity or above to // an exception tracker. You can create custom hooks, see the Hooks section. log.AddHook(airbrake.NewHook(123, \"xyz\", \"production\")) hook, err := logrus_syslog.NewSyslogHook(\"udp\", \"localhost:514\", syslog.LOG_INFO, \"\") if err != nil { log.Error(\"Unable to connect to local syslog daemon\") } else { log.AddHook(hook) } } 1.8 格式化package main import ( \"github.com/sirupsen/logrus\" ) var log = logrus.New() func main() { log.Formatter = &logrus.JSONFormatter{} //log.SetReportCaller(true) // 可以开启记录函数名,但是会消耗性能 log.WithFields(logrus.Fields{ \"event\": \"event\", \"topic\": \"topic\", \"key\": \"key\", }).Info(\"Failed to send event\") } /* { \"event\":\"event\", \"key\":\"key\", \"level\":\"info\", \"msg\":\"Failed to send event\", \"time\":\"2021-12-23T12:21:55+08:00\", \"topic\":\"topic\" } */ 1.9 gin中使用logruspackage main import ( \"fmt\" \"github.com/sirupsen/logrus\" \"os\" \"github.com/gin-gonic/gin\" ) var log = logrus.New() func init() { // Log as JSON instead of the default ASCII formatter. log.Formatter = &logrus.JSONFormatter{} // Output to stdout instead of the default stderr // Can be any io.Writer, see below for File example f, _ := os.Create(\"./gin.log\") log.Out = f gin.SetMode(gin.ReleaseMode) gin.DefaultWriter = log.Out // Only log the warning severity or above. log.Level = logrus.InfoLevel } func main() { // 创建一个默认的路由引擎 r := gin.Default() // GET:请求方式;/hello:请求的路径 // 当客户端以GET方法请求/hello路径时,会执行后面的匿名函数 r.GET(\"/hello\", func(c *gin.Context) { log.WithFields(logrus.Fields{ \"animal\": \"walrus\", \"size\": 10, }).Warn(\"A group of walrus emerges from the ocean\") // c.JSON:返回JSON格式的数据 c.JSON(200, gin.H{ \"message\": \"Hello world!\", }) }) // 启动HTTP服务,默认在0.0.0.0:8080启动服务 fmt.Println(`http://127.0.0.1:8080/hello`) r.Run(\":8080\") } 记录日志 {\"animal\":\"walrus\",\"level\":\"warning\",\"msg\":\"A group of walrus emerges from the ocean\",\"size\":10,\"time\":\"2021-12-23T12:37:21+08:00\"} [GIN] 2021/12/23 - 12:37:21 | 200 | 705.823µs | 127.0.0.1 | GET \"/hello\" 02.在gin中封装使用2.0 目录结构logrus-demo ├── main.go └── middleware └── logger.go 2.1 main.gopackage main import ( \"cobra-demo/middleware\" \"fmt\" \"github.com/gin-gonic/gin\" \"github.com/sirupsen/logrus\" ) func helloWorld(c *gin.Context) { // 测试写入日志 middleware.Logger.WithFields(logrus.Fields{ \"data\" : \"访问/hello\", }).Info(\"测试写入info\") // c.JSON:返回JSON格式的数据 c.JSON(200, gin.H{ \"message\": \"Hello world!\", }) } func main() { r := gin.Default() r.Use(middleware.LoggerMiddleware()) r.GET(\"/hello\", helloWorld) // 启动HTTP服务,默认在0.0.0.0:8080启动服务 fmt.Println(`http://127.0.0.1:8080/hello`) r.Run(\":8080\") } 2.2 middleware/logger.gepackage middleware import ( \"fmt\" \"github.com/gin-gonic/gin\" rotatelogs \"github.com/lestrrat-go/file-rotatelogs\" \"github.com/rifflock/lfshook\" \"github.com/sirupsen/logrus\" \"os\" \"path\" \"time\" ) var ( logFilePath = \"./\" logFileName = \"system.log\" ) func LoggerMiddleware() gin.HandlerFunc { // 日志文件 fileName := path.Join(logFilePath, logFileName) // 写入文件 src, err := os.OpenFile(fileName, os.O_APPEND|os.O_WRONLY, os.ModeAppend) if err != nil { fmt.Println(\"err\", err) } // 实例化 logger := logrus.New() //设置日志级别 logger.SetLevel(logrus.DebugLevel) //设置输出 logger.Out = src // 设置 rotatelogs logWriter, err := rotatelogs.New( // 分割后的文件名称 fileName+\".%Y%m%d.log\", // 生成软链,指向最新日志文件 rotatelogs.WithLinkName(fileName), // 设置最大保存时间(7天) rotatelogs.WithMaxAge(7*24*time.Hour), // 设置日志切割时间间隔(1天) rotatelogs.WithRotationTime(24*time.Hour), ) writeMap := lfshook.WriterMap{ logrus.InfoLevel: logWriter, logrus.FatalLevel: logWriter, logrus.DebugLevel: logWriter, logrus.WarnLevel: logWriter, logrus.ErrorLevel: logWriter, logrus.PanicLevel: logWriter, } logger.AddHook(lfshook.NewHook(writeMap, &logrus.JSONFormatter{ TimestampFormat: \"2006-01-02 15:04:05\", })) return func(c *gin.Context) { //开始时间 startTime := time.Now() //处理请求 c.Next() //结束时间 endTime := time.Now() // 执行时间 latencyTime := endTime.Sub(startTime) //请求方式 reqMethod := c.Request.Method //请求路由 reqUrl := c.Request.RequestURI //状态码 statusCode := c.Writer.Status() //请求ip clientIP := c.ClientIP() // 日志格式 logger.WithFields(logrus.Fields{ \"status_code\": statusCode, \"latency_time\": latencyTime, \"client_ip\": clientIP, \"req_method\": reqMethod, \"req_uri\": reqUrl, }).Info() } } 2.3 logging/logger.gopackage logging import ( setting \"bamboo.com/pipeline/Go-assault-squad/config\" \"fmt\" \"github.com/sirupsen/logrus\" \"os\" ) var WebLog *logrus.Logger func Init() { initWebLog() } func initWebLog() { WebLog = initLog(setting.Conf.LogConfig.WebLogName) } // 初始化日志句柄 func initLog(logFileName string) *logrus.Logger{ log := logrus.New() log.Formatter = &logrus.JSONFormatter{ TimestampFormat: \"2006-01-02 15:04:05\", } logFilePath := setting.Conf.LogFilePath logName := logFilePath + logFileName var f *os.File var err error //判断日志文件夹是否存在,不存在则创建 if _, err := os.Stat(logFilePath); os.IsNotExist(err) { os.MkdirAll(logFilePath, os.ModePerm) } //判断日志文件是否存在,不存在则创建,否则就直接打开 if _, err := os.Stat(logName); os.IsNotExist(err) { f, err = os.Create(logName) } else { f, err = os.OpenFile(logName,os.O_APPEND|os.O_WRONLY, os.ModeAppend) } if err != nil { fmt.Println(\"open log file failed\") } log.Out = f log.Level = logrus.InfoLevel return log } /* ---- 日志写入测试 ---- WebLog.WithFields(logrus.Fields{ \"data\" : \"访问/hello\", }).Info(\"测试写入info\") ---- 写入结构如下 ---- {\"data\":\"访问/hello\",\"level\":\"info\",\"msg\":\"测试写入info\",\"time\":\"2021-12-29 18:15:54\"} */ 2.4 访问测试 go run main.go http://127.0.0.1:8080/hello 写入日志格式 {\"data\":\"访问/hello\",\"level\":\"info\",\"msg\":\"测试写入info\",\"time\":\"2021-12-23 15:27:37\"} {\"client_ip\":\"127.0.0.1\",\"latency_time\":418116,\"level\":\"info\",\"msg\":\"\",\"req_method\":\"GET\",\"req_uri\":\"/hello\",\"status_code\":200,\"time\":\"2021-12-23 15:27:37\"}","categories":[{"name":"Go常用库","slug":"Go常用库","permalink":"http://coderedeng.github.io/categories/Go%E5%B8%B8%E7%94%A8%E5%BA%93/"}],"tags":[{"name":"Go常用库","slug":"Go常用库","permalink":"http://coderedeng.github.io/tags/Go%E5%B8%B8%E7%94%A8%E5%BA%93/"}]},{"title":"Cobor","slug":"Go常用库介绍 - cobor","date":"2022-06-19T14:26:53.000Z","updated":"2024-03-27T12:42:47.444Z","comments":true,"path":"2022/06/19/Go常用库介绍 - cobor/","link":"","permalink":"http://coderedeng.github.io/2022/06/19/Go%E5%B8%B8%E7%94%A8%E5%BA%93%E4%BB%8B%E7%BB%8D%20-%20cobor/","excerpt":"","text":"17.cobor01.cobra使用 GitHub地址: https://github.com/spf13/cobra/blob/master/user_guide.md 参考博客:https://www.qikqiak.com/post/create-cli-app-with-cobra/ 安装 go get -u github.com/spf13/cobra 1.1 基本使用 初始项目 $ mkdir cobra-demo && cd cobra-demo $ go mod init cobra-demo 2)下载cobra # 强烈推荐配置该环境变量 $ export GOPROXY=https://goproxy.cn $ go get -u github.com/spf13/cobra/cobra 3)cobra init 命令来初始化 CLI 应用的脚手架 $ cobra init 1.2 初始化结构说明 目录结构 ├── cmd │ └── root.go └── main.go main.go package main import \"cobra-demo/cmd\" func main() { cmd.Execute() } cmd/root.go package cmd import ( \"os\" \"github.com/spf13/cobra\" ) var rootCmd = &cobra.Command{ Use: \"cobra-demo\", Short: \"A brief description of your application\", Long: Run: func(cmd *cobra.Command, args []string) { fmt.Println(\"Hello Cobra CLI\") }, } // 然后再执行 execute 方法 func Execute() { err := rootCmd.Execute() if err != nil { os.Exit(1) } } // 每当执行或者调用命令的时候,它都会先执行 init 函数中的所有函数 func init() { rootCmd.Flags().BoolP(\"toggle\", \"t\", false, \"Help message for toggle\") } rootCmd 根命令就会首先运行 initConfig 函数,当所有的初始化函数执行完成后,才会执行 rootCmd 的 RUN: func 执行函数 我们可以在 initConfig 函数里面添加一些 Debug 信息 func initConfig() { fmt.Println(\"I'm inside initConfig function in cmd/root.go\") } 02.cobra项目使用2.0 目录结构 目录结构 cobra-demo ├── cmd │ ├── root.go │ └── serve.go └── main.go 2.1 main.gopackage main import \"cobra-demo/cmd\" func main() { cmd.Execute() } 2.2 cmd/root.gopackage cmd import ( \"errors\" \"github.com/spf13/cobra\" \"log\" \"os\" ) var rootCmd = &cobra.Command{ Use: \"demo\", // 命令行时关键字 Short: \"cobra demo example\", // 命令简单描述 Long: `cobra demo example ....`, // 命令详细描述 Args: func(cmd *cobra.Command, args []string) error { if len(args) < 1 { return errors.New(\"requires at least one arg\") } return nil }, PersistentPreRunE: func(*cobra.Command, []string) error { return nil }, Run: func(cmd *cobra.Command, args []string) { // 钩子函数 usageStr := `可以使用 -h 查看命令` log.Printf(\"%s\\n\", usageStr) }, } // 第二步:然后再执行 execute 方法 func Execute() { err := rootCmd.Execute() if err != nil { os.Exit(1) } } // 第一步:每当执行或者调用命令的时候,它都会先执行 init 函数中的所有函数 func init() { rootCmd.AddCommand(StartCmd) } 2.3 cmd/serve.gopackage cmd import ( \"fmt\" \"github.com/spf13/cobra\" \"log\" ) var ( config string // 启动配置文件位置 port string // 启动端口号 mode string // 启动模式 StartCmd = &cobra.Command{ // go run main.go server -c=config/settings.dev.yml Use: \"server\", // 启动时要添加 server关键字 Short: \"Start API server\", // 对命令简单描述 Example: \"ferry server config/settings.yml\", // 运行命令例子 PreRun: func(cmd *cobra.Command, args []string) { // 钩子函数,在RunE前执行 usage() setup() }, RunE: func(cmd *cobra.Command, args []string) error { // 钩子函数 return run() }, } ) func init() { // 为 Command 添加选项(flags) StartCmd.PersistentFlags().StringVarP(&config, \"config\", \"c\", \"config/settings.yml\", \"Start server with provided configuration file\") StartCmd.PersistentFlags().StringVarP(&port, \"port\", \"p\", \"8002\", \"Tcp port server listening on\") StartCmd.PersistentFlags().StringVarP(&mode, \"mode\", \"m\", \"dev\", \"server mode ; eg:dev,test,prod\") } // 记录日志 func usage() { usageStr := `starting api server` log.Printf(\"%s\\n\", usageStr) } // 初始化项目 func setup() { // 1. 读取配置 fmt.Println(\"启动命令配置文件:\",config) // 2. 初始化数据库链接 // 3. 启动异步任务队列 } func run() error { // 1.获取当前启动模式 fmt.Println(\"启动命令当前模式:\", mode) // 2.获取当前启动端口 fmt.Println(\"启动命令当前端口\", port) return nil } 2.4 运行测试 我们可以根据当前命令行传入的 配置文件位置、端口号、启动模式 来启动项目 xiaonaiqiang1@ZBMac-C02CW08SM cobra-demo % go run main.go server -c=config/settings.dev.yml -p=8888 -m=release 2021/12/23 11:09:12 starting api server 启动命令配置文件: config/settings.dev.yml 启动命令当前模式: release 启动命令当前端口 8888","categories":[{"name":"Go常用库","slug":"Go常用库","permalink":"http://coderedeng.github.io/categories/Go%E5%B8%B8%E7%94%A8%E5%BA%93/"}],"tags":[{"name":"Go常用库","slug":"Go常用库","permalink":"http://coderedeng.github.io/tags/Go%E5%B8%B8%E7%94%A8%E5%BA%93/"}]},{"title":"Makefile","slug":"Go常用库介绍 - Makefile","date":"2022-06-17T14:21:58.000Z","updated":"2024-01-22T12:58:59.000Z","comments":true,"path":"2022/06/17/Go常用库介绍 - Makefile/","link":"","permalink":"http://coderedeng.github.io/2022/06/17/Go%E5%B8%B8%E7%94%A8%E5%BA%93%E4%BB%8B%E7%BB%8D%20-%20Makefile/","excerpt":"","text":"16.Makefile01.介绍1.1 make介绍 make是一个构建自动化工具,会在当前目录下寻找Makefile或makefile文件 如果存在相应的文件,它就会依据其中定义好的规则完成构建任务。 1.2 Makefile介绍 借助Makefile我们在编译过程中不再需要每次手动输入编译的命令和编译的参数,可以极大简化项目编译过程。 我们可以把Makefile简单理解为它定义了一个项目文件的编译规则。 借助Makefile我们在编译过程中不再需要每次手动输入编译的命令和编译的参数,可以极大简化项目编译过程。 同时使用Makefile也可以在项目中确定具体的编译规则和流程,很多开源项目中都会定义Makefile文件。 1.3 win10安装make MinGW下载网页:http://sourceforge.net/projects/mingw/files/latest/download?source=files 右击计算机->属性->高级系统设置->环境变量,在系统变量中找到PATH 将MinGW安装目录里的bin文件夹的地址添加到PATH里面。 打开MinGW的安装目录,打开bin文件夹,将mingw32-make.exe重命名为make.exe。 经过以上步骤后,控制台可以输入make。 1.4 规则介绍 Makefile由多条规则组成,每条规则主要由两个部分组成,分别是依赖的关系和执行的命令。 其结构如下所示: [target] ... : [prerequisites] ... <tab>[command] ... ... 其中: targets:规则的目标 prerequisites:可选的要生成 targets 需要的文件或者是目标。 command:make 需要执行的命令(任意的 shell 命令)。可以有多条命令,每一条命令占一行。 举个例子: build: CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o xx 02.makefile基本使用2.1 main.gopackage main import ( \"fmt\" \"net/http\" ) func main() { http.HandleFunc(\"/\", hello) server := &http.Server{ Addr: \":8888\", } fmt.Println(\"server startup...\") if err := server.ListenAndServe(); err != nil { fmt.Printf(\"server startup failed, err:%v\\n\", err) } } func hello(w http.ResponseWriter, _ *http.Request) { w.Write([]byte(\"hello v5blog.cn!\")) } 2.2 示例 BINARY="xxx"是定义变量 .PHONY用来定义伪目标,不创建目标文件,而是去执行这个目标下面的命令 .PHONY: all build run gotool clean help # 编译后的项目名 BINARY=\"xxx\" # 如果make后面不加任何参数,默认执行all all: gotool build build: CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o ${BINARY} run: @go run ./main.go #@go run ./main.go conf/config.yaml gotool: go fmt ./ go vet ./ clean: @if [ -f ${BINARY} ] ; then rm ${BINARY} ; fi help: @echo \"make - 格式化 Go 代码, 并编译生成二进制文件\" @echo \"make build - 编译 Go 代码, 生成二进制文件\" @echo \"make run - 直接运行 Go 代码\" @echo \"make clean - 移除二进制文件和 vim swap files\" @echo \"make gotool - 运行 Go 工具 'fmt' and 'vet'\" 2.3 使用 03.完整.PHONY: all build run gotool clean help BINARY=\"bluebell\" all: gotool build build: CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags \"-s -w\" -o ./bin/${BINARY} run: @go run ./main.go conf/config.yaml gotool: go fmt ./ go vet ./ clean: @if [ -f ${BINARY} ] ; then rm ${BINARY} ; fi help: @echo \"make - 格式化 Go 代码, 并编译生成二进制文件\" @echo \"make build - 编译 Go 代码, 生成二进制文件\" @echo \"make run - 直接运行 Go 代码\" @echo \"make clean - 移除二进制文件和 vim swap files\" @echo \"make gotool - 运行 Go 工具 'fmt' and 'vet'\"","categories":[{"name":"Go常用库","slug":"Go常用库","permalink":"http://coderedeng.github.io/categories/Go%E5%B8%B8%E7%94%A8%E5%BA%93/"}],"tags":[{"name":"Go常用库","slug":"Go常用库","permalink":"http://coderedeng.github.io/tags/Go%E5%B8%B8%E7%94%A8%E5%BA%93/"}]},{"title":"go-wrk","slug":"Go常用库介绍 - go-wrk","date":"2022-06-15T13:52:47.000Z","updated":"2024-01-22T12:58:59.000Z","comments":true,"path":"2022/06/15/Go常用库介绍 - go-wrk/","link":"","permalink":"http://coderedeng.github.io/2022/06/15/Go%E5%B8%B8%E7%94%A8%E5%BA%93%E4%BB%8B%E7%BB%8D%20-%20go-wrk/","excerpt":"","text":"15.go-wrk压测01.压测介绍1.1 压测作用 在项目正式上线之前,我们通常需要通过压测来评估当前系统能够支撑的请求量、排查可能存在的隐藏bug 同时了解了程序的实际处理能力能够帮我们更好的匹配项目的实际需求,节约资源成本。 1.2 压测相关术语 响应时间(RT) :指系统对请求作出响应的时间. 吞吐量(Throughput) :指系统在单位时间内处理请求的数量 QPS每秒查询率(Query Per Second) :“每秒查询率”,是一台服务器每秒能够响应的查询次数 是对一个特定的查询服务器在规定时间内所处理流量多少的衡量标准。 TPS(TransactionPerSecond):每秒钟系统能够处理的交易或事务的数量 并发连接数:某个时刻服务器所接受的请求总数 02.压力测试工具2.1 ab ab全称Apache Bench,是Apache自带的性能测试工具。 使用这个工具,只须指定同时连接数、请求数以及URL,即可测试网站或网站程序的性能。 通过ab发送请求模拟多个访问者同时对某一URL地址进行访问,可以得到每秒传送字节数、每秒处理请求数、每请求处理时间等统计数据。 命令格式: ab [options] [http://]hostname[:port]/path 常用参数如下: -n requests 总请求数 -c concurrency 一次产生的请求数,可以理解为并发数 -t timelimit 测试所进行的最大秒数, 可以当做请求的超时时间 -p postfile 包含了需要POST的数据的文件 -T content-type POST数据所使用的Content-type头信息 更多参数请查看官方文档 (opens new window)。 例如测试某个GET请求接口: ab -n 10000 -c 100 -t 10 \"http://127.0.0.1:8080/api/v1/posts?size=10\" 测试POST请求接口: ab -n 10000 -c 100 -t 10 -p post.json -T \"application/json\" \"http://127.0.0.1:8080/api/v1/post\" 2.2 wrk 是一款开源的HTTP性能测试工具,它和上面提到的ab同属于HTTP性能测试工具 它比ab功能更加强大,可以通过编写lua脚本来支持更加复杂的测试场景。 Mac下安装 brew install wrk 常用命令参数: -c --conections:保持的连接数 -d --duration:压测持续时间(s) -t --threads:使用的线程总数 -s --script:加载lua脚本 -H --header:在请求头部添加一些参数 --latency 打印详细的延迟统计信息 --timeout 请求的最大超时时间(s) 使用示例: wrk -t8 -c100 -d30s --latency http://127.0.0.1:8080/api/v1/posts?size=10 输出结果: Running 30s test @ http://127.0.0.1:8080/api/v1/posts?size=10 8 threads and 100 connections Thread Stats Avg Stdev Max +/- Stdev Latency 14.55ms 2.02ms 31.59ms 76.70% Req/Sec 828.16 85.69 0.97k 60.46% Latency Distribution 50% 14.44ms 75% 15.76ms 90% 16.63ms 99% 21.07ms 198091 requests in 30.05s, 29.66MB read Requests/sec: 6592.29 Transfer/sec: 0.99MB 2.3 go-wrk 是Go语言版本的wrk Windows同学可以使用它来测试,使用如下命令来安装go-wrk go get github.com/adeven/go-wrk 使用方法同wrk类似,基本格式如下: go-wrk [flags] url 常用的参数: -H=\"User-Agent: go-wrk 0.1 bechmark\\nContent-Type: text/html;\": 由'\\n'分隔的请求头 -c=100: 使用的最大连接数 -k=true: 是否禁用keep-alives -i=false: if TLS security checks are disabled -m=\"GET\": HTTP请求方法 -n=1000: 请求总数 -t=1: 使用的线程数 -b=\"\" HTTP请求体 -s=\"\" 如果指定,它将计算响应中包含搜索到的字符串s的频率 执行测试: go-wrk -t=8 -c=100 -n=10000 \"http://127.0.0.1:8080/api/v1/posts?size=10\" 输出结果: ==========================BENCHMARK========================== URL: http://127.0.0.1:8080/api/v1/posts?size=10 Used Connections: 100 Used Threads: 8 Total number of calls: 10000 ===========================TIMINGS=========================== Total time passed: 2.74s Avg time per request: 27.11ms Requests per second: 3644.53 Median time per request: 26.88ms 99th percentile time: 39.16ms Slowest time for request: 45.00ms =============================DATA============================= Total response body sizes: 340000 Avg response body per request: 34.00 Byte Transfer rate per second: 123914.11 Byte/s (0.12 MByte/s) ==========================RESPONSES========================== 20X Responses: 10000 (100.00%) 30X Responses: 0 (0.00%) 40X Responses: 0 (0.00%) 50X Responses: 0 (0.00%) Errors: 0 (0.00%)","categories":[{"name":"Go常用库","slug":"Go常用库","permalink":"http://coderedeng.github.io/categories/Go%E5%B8%B8%E7%94%A8%E5%BA%93/"}],"tags":[{"name":"Go常用库","slug":"Go常用库","permalink":"http://coderedeng.github.io/tags/Go%E5%B8%B8%E7%94%A8%E5%BA%93/"}]},{"title":"jwt-go","slug":"Go常用库介绍 - jwt-go","date":"2022-06-13T13:01:24.000Z","updated":"2024-01-22T12:58:59.000Z","comments":true,"path":"2022/06/13/Go常用库介绍 - jwt-go/","link":"","permalink":"http://coderedeng.github.io/2022/06/13/Go%E5%B8%B8%E7%94%A8%E5%BA%93%E4%BB%8B%E7%BB%8D%20-%20jwt-go/","excerpt":"","text":"14.jwt-go01.JWT介绍1.1 什么是JWT? JWT全称JSON Web Token是一种跨域认证解决方案,属于一个开放的标准,它规定了一种Token实现方式 目前多用于前后端分离项目和OAuth2.0业务场景下。 1.2 jwt三部分 基于JWT技术及RSA非对称加密实现真正无状态的单点登录 02.JWT基本用法2.1 定义需求 我们需要定制自己的需求来决定JWT中保存哪些数据 比如我们规定在JWT中要存储username信息 那么我们就定义一个MyClaims结构体如下 // MyClaims 自定义声明结构体并内嵌jwt.StandardClaims // jwt包自带的jwt.StandardClaims只包含了官方字段 // 我们这里需要额外记录一个username字段,所以要自定义结构体 // 如果想要保存更多信息,都可以添加到这个结构体中 type MyClaims struct { Username string `json:\"username\"` jwt.StandardClaims } 然后我们定义JWT的过期时间,这里以2小时为例: const TokenExpireDuration = time.Hour * 2 接下来还需要定义Secret: var MySecret = []byte(\"夏天夏天悄悄过去\") 2.2 生成JWT// GenToken 生成JWT func GenToken(username string) (string, error) { // 创建一个我们自己的声明 c := MyClaims{ username, // 自定义字段 jwt.StandardClaims{ ExpiresAt: time.Now().Add(TokenExpireDuration).Unix(), // 过期时间 Issuer: \"my-project\", // 签发人 }, } // 使用指定的签名方法创建签名对象 token := jwt.NewWithClaims(jwt.SigningMethodHS256, c) // 使用指定的secret签名并获得完整的编码后的字符串token return token.SignedString(MySecret) } 2.3 解析JWT// ParseToken 解析JWT func ParseToken(tokenString string) (*MyClaims, error) { // 解析token token, err := jwt.ParseWithClaims(tokenString, &MyClaims{}, func(token *jwt.Token) (i interface{}, err error) { return MySecret, nil }) if err != nil { return nil, err } if claims, ok := token.Claims.(*MyClaims); ok && token.Valid { // 校验token return claims, nil } return nil, errors.New(\"invalid token\") } 03.gin使用JWT3.0 demo结构 3.1 main.gopackage main import ( \"github.com/gin-gonic/gin\" \"jwt-test/controllers\" \"jwt-test/middlewares\" ) func main() { r := gin.Default() r.POST(\"/auth\", controllers.AuthHandler) r.GET(\"/home\", middlewares.JWTAuthMiddleware(), controllers.HomeHandler) r.Run(\":8000\") } 3.2 controllers/user.gopackage controllers import ( \"github.com/gin-gonic/gin\" \"jwt-test/pkg/jwt\" \"net/http\" ) // ParamSignUp 注册请求参数 type UserInfo struct { Username string `json:\"username\" binding:\"required\"` Password string `json:\"password\" binding:\"required\"` RePassword string `json:\"confirm_password\" binding:\"required,eqfield=Password\"` //RePassword string `json:\"re_password\" binding:\"required,eqfield=Password\"` } func AuthHandler(c *gin.Context) { // 用户发送用户名和密码过来 var user UserInfo err := c.ShouldBind(&user) if err != nil { c.JSON(http.StatusOK, gin.H{ \"code\": 2001, \"msg\": \"无效的参数\", }) return } // 校验用户名和密码是否正确 if user.Username == \"zhangsan\" && user.Password == \"123456\" { // 生成Token tokenString, _ := jwt.GenToken(user.Username) c.JSON(http.StatusOK, gin.H{ \"code\": 2000, \"msg\": \"success\", \"data\": gin.H{\"token\": tokenString}, }) return } c.JSON(http.StatusOK, gin.H{ \"code\": 2002, \"msg\": \"鉴权失败\", }) return } func HomeHandler(c *gin.Context) { username := c.MustGet(\"username\").(string) c.JSON(http.StatusOK, gin.H{ \"code\": 2000, \"msg\": \"success\", \"data\": gin.H{\"username\": username}, }) } 3.3 pkg/jwt/jwt.gopackage jwt import ( \"errors\" \"github.com/dgrijalva/jwt-go\" \"time\" ) // MyClaims 自定义声明结构体并内嵌jwt.StandardClaims // jwt包自带的jwt.StandardClaims只包含了官方字段 // 我们这里需要额外记录一个username字段,所以要自定义结构体 // 如果想要保存更多信息,都可以添加到这个结构体中 type MyClaims struct { Username string `json:\"username\"` jwt.StandardClaims } const TokenExpireDuration = time.Hour * 2 var MySecret = []byte(\"夏天夏天悄悄过去\") // GenToken 生成JWT func GenToken(username string) (string, error) { // 创建一个我们自己的声明 c := MyClaims{ username, // 自定义字段 jwt.StandardClaims{ ExpiresAt: time.Now().Add(TokenExpireDuration).Unix(), // 过期时间 Issuer: \"my-project\", // 签发人 }, } // 使用指定的签名方法创建签名对象 token := jwt.NewWithClaims(jwt.SigningMethodHS256, c) // 使用指定的secret签名并获得完整的编码后的字符串token return token.SignedString(MySecret) } // ParseToken 解析JWT func ParseToken(tokenString string) (*MyClaims, error) { // 解析token token, err := jwt.ParseWithClaims(tokenString, &MyClaims{}, func(token *jwt.Token) (i interface{}, err error) { return MySecret, nil }) if err != nil { return nil, err } if claims, ok := token.Claims.(*MyClaims); ok && token.Valid { // 校验token return claims, nil } return nil, errors.New(\"invalid token\") } 3.4 middlewares/auth.gopackage middlewares import ( \"github.com/gin-gonic/gin\" \"jwt-test/pkg/jwt\" \"net/http\" \"strings\" ) // JWTAuthMiddleware 基于JWT的认证中间件 func JWTAuthMiddleware() func(c *gin.Context) { return func(c *gin.Context) { // 客户端携带Token有三种方式 1.放在请求头 2.放在请求体 3.放在URI // 这里假设Token放在Header的Authorization中,并使用Bearer开头 // 这里的具体实现方式要依据你的实际业务情况决定 authHeader := c.Request.Header.Get(\"Authorization\") if authHeader == \"\" { c.JSON(http.StatusOK, gin.H{ \"code\": 2003, \"msg\": \"请求头中auth为空\", }) c.Abort() return } // 按空格分割 parts := strings.SplitN(authHeader, \" \", 2) if !(len(parts) == 2 && parts[0] == \"Bearer\") { c.JSON(http.StatusOK, gin.H{ \"code\": 2004, \"msg\": \"请求头中auth格式有误\", }) c.Abort() return } // parts[1]是获取到的tokenString,我们使用之前定义好的解析JWT的函数来解析它 mc, err := jwt.ParseToken(parts[1]) if err != nil { c.JSON(http.StatusOK, gin.H{ \"code\": 2005, \"msg\": \"无效的Token\", }) c.Abort() return } // 将当前请求的username信息保存到请求的上下文c上 c.Set(\"username\", mc.Username) c.Next() // 后续的处理函数可以用过c.Get(\"username\")来获取当前请求的用户信息 } } 04.测试4.1 登录获取token http://127.0.0.1:8000/auth { \"username\":\"zhangsan\", \"password\": \"123456\", \"confirm_password\": \"123456\" } 4.2 携带token访问 http://127.0.0.1:8000/home Authorization Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InpoYW5nc2FuIiwiZXhwIjoxNjIzNjY3NzUzLCJpc3MiOiJteS1wcm9qZWN0In0.j9SFygMnMq1-ymsDcTLN59svQb4-BTgO3DLaBeUAAVY","categories":[{"name":"Go常用库","slug":"Go常用库","permalink":"http://coderedeng.github.io/categories/Go%E5%B8%B8%E7%94%A8%E5%BA%93/"}],"tags":[{"name":"Go常用库","slug":"Go常用库","permalink":"http://coderedeng.github.io/tags/Go%E5%B8%B8%E7%94%A8%E5%BA%93/"}]},{"title":"gRPC","slug":"Go常用库介绍 - gRPC","date":"2022-06-11T14:11:19.000Z","updated":"2024-01-22T12:58:59.000Z","comments":true,"path":"2022/06/11/Go常用库介绍 - gRPC/","link":"","permalink":"http://coderedeng.github.io/2022/06/11/Go%E5%B8%B8%E7%94%A8%E5%BA%93%E4%BB%8B%E7%BB%8D%20-%20gRPC/","excerpt":"","text":"13.gRPC01.gRPC基础1.1 RPC是什么 在分布式计算,远程过程调用(英语:Remote Procedure Call,缩写为 RPC)是一个计算机通信协议。 该协议允许运行于一台计算机的程序调用另一个地址空间(通常为一个开放网络的一台计算机)的子程序 而程序员就像调用本地程序一样,无需额外地为这个交互作用编程(无需关注细节)。 RPC是一种服务器-客户端(Client/Server)模式,经典实现是一个通过发送请求-接受回应进行信息交互的系统。 1.2 gRPC是什么 gRPC是一种现代化开源的高性能RPC框架,能够运行于任意环境之中。 最初由谷歌进行开发,它使用HTTP/2作为传输协议。 在gRPC里,客户端可以像调用本地方法一样直接调用其他机器上的服务端应用程序的方法,帮助你更容易创建分布式应用程序和服务。 与许多RPC系统一样,gRPC是基于定义一个服务,指定一个可以远程调用的带有参数和返回类型的的方法。 在服务端程序中实现这个接口并且运行gRPC服务处理客户端调用。 在客户端,有一个stub提供和服务端相同的方 1.3 为什么要用gRPC 使用gRPC, 我们可以一次性的在一个.proto文件中定义服务并使用任何支持它的语言去实现客户端和服务端 反过来,它们可以应用在各种场景中,从Google的服务器到你自己的平板电脑—— gRPC帮你解决了不同语言及环境间通信的复杂性。 使用protocol buffers还能获得其他好处,包括高效的序列号,简单的IDL以及容易进行接口更新。 总之一句话,使用gRPC能让我们更容易编写跨语言的分布式代码。 02.安装gRPC2.1 安装gRPCgo get -u google.golang.org/grpc 2.2 安装Protocol Buffers v3 安装用于生成gRPC服务代码的协议编译器,最简单的方法是从下面的链接 https://github.com/google/protobuf/releases 下载适合你平台的预编译好的二进制文件(protoc-<version>-<platform>.zip)。 下载完之后,执行下面的步骤: 1、解压下载好的文件 2、把protoc二进制文件的路径加到环境变量中 接下来执行下面的命令安装protoc的Go插件 go get -u github.com/golang/protobuf/protoc-gen-go 编译插件protoc-gen-go将会安装到$GOBIN,默认是$GOPATH/bin,它必须在你的$PATH中以便协议编译器protoc能够找到它。","categories":[{"name":"Go常用库","slug":"Go常用库","permalink":"http://coderedeng.github.io/categories/Go%E5%B8%B8%E7%94%A8%E5%BA%93/"}],"tags":[{"name":"Go常用库","slug":"Go常用库","permalink":"http://coderedeng.github.io/tags/Go%E5%B8%B8%E7%94%A8%E5%BA%93/"}]},{"title":"viper配置管理","slug":"Go常用库介绍 - viper","date":"2022-06-09T13:24:37.000Z","updated":"2024-01-22T12:58:59.000Z","comments":true,"path":"2022/06/09/Go常用库介绍 - viper/","link":"","permalink":"http://coderedeng.github.io/2022/06/09/Go%E5%B8%B8%E7%94%A8%E5%BA%93%E4%BB%8B%E7%BB%8D%20-%20viper/","excerpt":"","text":"12.viper配置管理01.viper介绍 参考博客(opens new window) 1.1 viper是什么? Viper (opens new window)是适用于Go应用程序的完整配置解决方案。 它被设计用于在应用程序中工作,并且可以处理所有类型的配置需求和格式 viper功能 设置默认值 从JSON、TOML、YAML、HCL、envfile和Java properties格式的配置文件读取配置信息 实时监控和重新读取配置文件(可选) 从环境变量中读取 从远程配置系统(etcd或Consul)读取并监控配置变化 从命令行参数读取配置 从buffer读取配置 显式配置值 1.2 为什么选择Viper? 在构建现代应用程序时,你无需担心配置文件格式; Viper能够为你执行下列操作: 查找、加载和反序列化JSON、TOML、YAML、HCL、INI、envfile和Java properties格式的配置文件。 提供一种机制为你的不同配置选项设置默认值。 提供一种机制来通过命令行参数覆盖指定选项的值。 提供别名系统,以便在不破坏现有代码的情况下轻松重命名参数。 当用户提供了与默认值相同的命令行或配置文件时,可以很容易地分辨出它们之间的区别。 Viper会按照下面的优先,每个项目的优先级都高于它下面的项目 显示调用Set设置值 命令行参数(flag) 环境变量 配置文件 key/value存储 默认值 重要: 目前Viper配置的键(Key)是大小写不敏感的 1.3 viper安装go get github.com/spf13/viper 02.viper设置配置2.1 建立默认值 一个好的配置系统应该支持默认值。 键不需要默认值,但如果没有通过配置文件、环境变量、远程配置或命令行标志(flag)设置键,则默认值非常有用。 例如: viper.SetDefault(\"ContentDir\", \"content\") viper.SetDefault(\"LayoutDir\", \"layouts\") viper.SetDefault(\"Taxonomies\", map[string]string{\"tag\": \"tags\", \"category\": \"categories\"}) 2.2 读取配置文件 Viper需要最少知道在哪里查找配置文件的配置。 Viper支持JSON、TOML、YAML、HCL、envfile和Java properties格式的配置文件。 Viper可以搜索多个路径,但目前单个Viper实例只支持单个配置文件。 viper.SetConfigFile(\"./config.yaml\") // 指定配置文件路径 viper.SetConfigName(\"config\") // 配置文件名称(无扩展名) viper.SetConfigType(\"yaml\") // 如果配置文件的名称中没有扩展名,则需要配置此项 viper.AddConfigPath(\"/etc/appname/\") // 查找配置文件所在的路径 viper.AddConfigPath(\"$HOME/.appname\") // 多次调用以添加多个搜索路径 viper.AddConfigPath(\".\") // 还可以在工作目录中查找配置 err := viper.ReadInConfig() // 查找并读取配置文件 if err != nil { // 处理读取配置文件的错误 panic(fmt.Errorf(\"Fatal error config file: %s \\n\", err)) } 2.3 写入配置文件 从配置文件中读取配置文件是有用的,但是有时你想要存储在运行时所做的所有修改。 为此,可以使用下面一组命令,每个命令都有自己的用途 viper.WriteConfig() // 将当前配置写入“viper.AddConfigPath()”和“viper.SetConfigName”设置的预定义路径 viper.SafeWriteConfig() viper.WriteConfigAs(\"/path/to/my/.config\") viper.SafeWriteConfigAs(\"/path/to/my/.config\") // 因为该配置文件写入过,所以会报错 viper.SafeWriteConfigAs(\"/path/to/my/.other_config\") 2.4 监控并重新读取配置文件 确保在调用WatchConfig()之前添加了所有的配置路径。 viper.WatchConfig() viper.OnConfigChange(func(e fsnotify.Event) { // 配置文件发生变更之后会调用的回调函数 fmt.Println(\"Config file changed:\", e.Name) }) 2.4 覆盖设置 这些可能来自命令行标志,也可能来自你自己的应用程序逻辑。 viper.Set(\"Verbose\", true) viper.Set(\"LogFile\", LogFile) 03.viper读取配置3.1 几种访问值的方法 在Viper中,有几种方法可以根据值的类型获取值 Get(key string) : interface{} GetBool(key string) : bool GetFloat64(key string) : float64 GetInt(key string) : int GetIntSlice(key string) : []int GetString(key string) : string GetStringMap(key string) : map[string]interface{} GetStringMapString(key string) : map[string]string GetStringSlice(key string) : []string GetTime(key string) : time.Time GetDuration(key string) : time.Duration IsSet(key string) : bool AllSettings() : map[string]interface{} 例如: viper.GetString(\"logfile\") // 不区分大小写的设置和获取 if viper.GetBool(\"verbose\") { fmt.Println(\"verbose enabled\") } 3.2 访问嵌套的键 问器方法也接受深度嵌套键的格式化路径 例如,如果加载下面的JSON文件 { \"host\": { \"address\": \"localhost\", \"port\": 5799 }, \"datastore\": { \"metric\": { \"host\": \"127.0.0.1\", \"port\": 3099 }, \"warehouse\": { \"host\": \"198.0.0.1\", \"port\": 2112 } } } Viper可以通过传入.分隔的路径来访问嵌套字段: GetString(\"datastore.metric.host\") // (返回 \"127.0.0.1\") 3.3 提取子树 例如,viper实例现在代表了以下配置: app: cache1: max-items: 100 item-size: 64 cache2: max-items: 200 item-size: 80 执行后: subv := viper.Sub(\"app.cache1\") subv现在就代表: max-items: 100 item-size: 64 假设我们现在有这么一个函数: func NewCache(cfg *Viper) *Cache {...} 它基于subv格式的配置信息创建缓存。现在,可以轻松地分别创建这两个缓存,如下所示: cfg1 := viper.Sub(\"app.cache1\") cache1 := NewCache(cfg1) cfg2 := viper.Sub(\"app.cache2\") cache2 := NewCache(cfg2) 3.4 反序列化你还可以选择将所有或特定的值解析到结构体、map等。 有两种方法可以做到这一点: Unmarshal(rawVal interface{}) : error UnmarshalKey(key string, rawVal interface{}) : error main.go package main import ( \"fmt\" \"github.com/spf13/viper\" ) type Config struct { Port int `mapstructure:\"port\"` Version string `mapstructure:\"version\"` MySQLConfig `mapstructure:\"mysql\"` } type MySQLConfig struct { Host string `mapstructure:\"host\"` DbName string `mapstructure:\"dbname\"` Port int `mapstructure:\"port\"` } func main() { // 读取配置文件 viper.SetConfigFile(\"./config.yaml\") // 指定配置文件路径 err := viper.ReadInConfig() // 查找并读取配置文件 if err != nil { // 处理读取配置文件的错误 panic(fmt.Errorf(\"Fatal error config file: %s \\n\", err)) } var c Config if err := viper.Unmarshal(&c); err != nil { fmt.Printf(\"viper.Unmarshal failed, err:%v\\n\", err) return } fmt.Printf(\"c:%#v\\n\", c) } config.yaml port: 8081 version: \"v0.0.2\" mysql: host: \"127.0.0.1\" port: 13306 dbname: \"sql_demo\" 04.使用Viper示例 目录结构 4.1 ./conf/config.yamlport: 8123 version: \"v1.2.3\" 4.2 gin中使用viper案例 这里用一个demo演示如何在gin框架搭建的web项目中使用viper,使用viper加载配置文件中的信息 并在代码中直接使用viper.GetXXX()方法获取对应的配置值。 package main import ( \"fmt\" \"github.com/fsnotify/fsnotify\" \"github.com/gin-gonic/gin\" \"github.com/spf13/viper\" \"net/http\" ) func main() { // 第一:viper配置 viper.AddConfigPath(\".\") // 还可以在工作目录中查找配置 viper.SetConfigName(\"config\") // 配置文件名称(无扩展名) viper.SetConfigType(\"yaml\") // 如果配置文件的名称中没有扩展名,则需要配置此项 viper.AddConfigPath(\"./conf/\") // 指定查找配置文件的路径 err := viper.ReadInConfig() // 读取配置信息 if err != nil { // 读取配置信息失败 panic(fmt.Errorf(\"Fatal error config file: %s \\n\", err)) } // 第二:实时监控配置文件的变化 viper.WatchConfig() viper.OnConfigChange(func(e fsnotify.Event) { // 配置文件发生变更之后会调用的回调函数 fmt.Println(\"Config file changed:\", e.Name) }) // 第三:读取配置 r := gin.Default() r.GET(\"/version\", func(c *gin.Context) { c.String(http.StatusOK, viper.GetString(\"version\")) }) r.Run() } 4.3 结构体变量保存配置package main import ( \"fmt\" \"github.com/spf13/viper\" ) type Config struct { Port int `mapstructure:\"port\"` Version string `mapstructure:\"version\"` } var Conf = new(Config) func main() { // 第一:viper配置 viper.AddConfigPath(\".\") // 还可以在工作目录中查找配置 viper.SetConfigName(\"config\") // 配置文件名称(无扩展名) viper.SetConfigType(\"yaml\") // 如果配置文件的名称中没有扩展名,则需要配置此项 viper.AddConfigPath(\"./conf/\") // 指定查找配置文件的路径 err := viper.ReadInConfig() // 读取配置信息 if err != nil { // 读取配置信息失败 panic(fmt.Errorf(\"Fatal error config file: %s \\n\", err)) } // 第二:将读取的配置信息保存至全局变量Conf if err := viper.Unmarshal(Conf); err != nil { panic(fmt.Errorf(\"unmarshal conf failed, err:%s \\n\", err)) } fmt.Printf(\"Conf:%#v\\n\", Conf) // 打印 } /* Conf:&main.Config{Port:8123, Version:\"v1.2.3\"} */","categories":[{"name":"Go常用库","slug":"Go常用库","permalink":"http://coderedeng.github.io/categories/Go%E5%B8%B8%E7%94%A8%E5%BA%93/"}],"tags":[{"name":"Go常用库","slug":"Go常用库","permalink":"http://coderedeng.github.io/tags/Go%E5%B8%B8%E7%94%A8%E5%BA%93/"}]},{"title":"zap","slug":"Go常用库介绍 - zap","date":"2022-06-07T12:12:28.000Z","updated":"2024-03-27T12:43:14.617Z","comments":true,"path":"2022/06/07/Go常用库介绍 - zap/","link":"","permalink":"http://coderedeng.github.io/2022/06/07/Go%E5%B8%B8%E7%94%A8%E5%BA%93%E4%BB%8B%E7%BB%8D%20-%20zap/","excerpt":"","text":"11.zap日志包01.日志模块介绍 参考博客(opens new window) 1.1 介绍在许多Go语言项目中,我们需要一个好的日志记录器能够提供下面这些功能 能够将事件记录到文件中,而不是应用程序控制台。 日志切割-能够根据文件大小、时间或间隔等来切割日志文件。 支持不同的日志级别。例如INFO,DEBUG,ERROR等。 能够打印基本信息,如调用文件/函数名和行号,日志时间等。 1.2 默认的Go Logger 实现一个Go语言中的日志记录器非常简单——创建一个新的日志文件,然后设置它为日志的输出位置。 package main import ( \"log\" \"net/http\" \"os\" ) // 第一:设置Logger func SetupLogger() { logFileLocation, _ := os.OpenFile(\"./test.log\", os.O_CREATE|os.O_APPEND|os.O_RDWR, 0744) log.SetOutput(logFileLocation) } // 第二:使用Logger func simpleHttpGet(url string) { resp, err := http.Get(url) if err != nil { log.Printf(\"Error fetching url %s : %s\", url, err.Error()) } else { log.Printf(\"Status Code for %s : %s\", url, resp.Status) resp.Body.Close() } } func main() { SetupLogger() simpleHttpGet(\"www.baidu.com\") simpleHttpGet(\"http://www.baidu.com\") } 1.3 Go Logger的优势和劣势 优势 它最大的优点是使用非常简单。 我们可以设置任何io.Writer作为日志记录输出并向其发送要写入的日志。 劣势 仅限基本的日志级别 只有一个Print选项。不支持INFO/DEBUG等多个级别。 缺乏日志格式化的能力——例如记录调用者的函数名和行号,格式化日期和时间格式。等等。 不提供日志切割的能力。 02.zap基本使用2.1 zap介绍 Uber-go zap优势 它同时提供了结构化日志记录和printf风格的日志记录 它非常的快 安装 go get -u go.uber.org/zap 1 2.2 Sugared Logger和Logger Zap提供了两种类型的日志记录器—Sugared Logger和Logger。 在性能很好但不是很关键的上下文中,使用SugaredLogger。 它比其他结构化日志记录包快4-10倍,并且支持结构化和printf风格的日志记录。 在每一微秒和每一次内存分配都很重要的上下文中,使用Logger。 它甚至比SugaredLogger更快,内存分配次数也更少,但它只支持强类型的结构化日志记录。 2.3 zap日志记录器Logger 通过调用zap.NewProduction()/zap.NewDevelopment()或者zap.Example()创建一个Logger。 唯一的区别在于它将记录的信息不同 例如production logger默认记录调用函数信息、日期和时间等。 通过Logger调用Info/Error等。 默认情况下日志都会打印到应用程序的console界面。 package main import ( \"go.uber.org/zap\" \"net/http\" ) // 第一步:创建一个Logger var logger *zap.Logger func InitLogger() { logger, _ = zap.NewProduction() } // 第二步:通过Logger调用Info/Error等输出日志 func simpleHttpGet(url string) { resp, err := http.Get(url) if err != nil { logger.Error( \"Error fetching url..\", zap.String(\"url\", url), zap.Error(err)) } else { logger.Info(\"Success..\", zap.String(\"statusCode\", resp.Status), zap.String(\"url\", url)) resp.Body.Close() } } func main() { InitLogger() defer logger.Sync() simpleHttpGet(\"www.google.com\") simpleHttpGet(\"http://www.google.com\") } /* 在控制台会输出一下日志信息: {\"level\":\"error\",\"ts\":1623143450.9749353,\"caller\":\"gin_demo/main.go:23\",\"msg\":\"Error fetching url..\",\"url\":\"www.google.com\",\"error\":\"Get \\\"www.google.com\\\": unsupported protocol scheme \\\"\\\"\",\"stacktrace\":\"main.simpleHttpGet\\n\\tC:/aaa/gin_demo/main.go:23\\nmain.main\\n\\tC:/aaa/gin_demo/main.go:12\\nruntime.main\\n\\tC:/Go/src/runtime/proc.go:225\"} {\"level\":\"error\",\"ts\":1623143472.030105,\"caller\":\"gin_demo/main.go:23\",\"msg\":\"Error fetching url..\",\"url\":\"http://www.google.com\",\"error\":\"Get \\\"http://www.google.com\\\": dial tcp 69.171.247.32:80: connectex: A connection attempt failed because the connected party did not properly respond after a period of time, or established connection failed because connected host has failed to respond.\",\"stacktrace\":\"main.simpleHttpGet\\n\\tC:/aaa/gin_demo/main.go:23\\nmain.main\\n\\tC:/aaa/gin_demo/main.go:13\\nruntime.main\\n\\tC:/Go/src/runtime/proc.go:225\"} */ 2.4 zap日志记录器Sugared Logger现在让我们使用Sugared Logger来实现相同的功能。 大部分的实现基本都相同。 惟一的区别是,我们通过调用主logger的. Sugar()方法来获取一个SugaredLogger。 然后使用SugaredLogger以printf格式记录语句 package main import ( \"go.uber.org/zap\" \"net/http\" ) // 第一步:创建一个Sugared Logger var sugarLogger *zap.SugaredLogger func InitLogger() { logger, _ := zap.NewProduction() sugarLogger = logger.Sugar() } // 第二步:通过Logger调用Info/Error等输出日志 func simpleHttpGet(url string) { sugarLogger.Debugf(\"Trying to hit GET request for %s\", url) resp, err := http.Get(url) if err != nil { sugarLogger.Errorf(\"Error fetching URL %s : Error = %s\", url, err) } else { sugarLogger.Infof(\"Success! statusCode = %s for URL %s\", resp.Status, url) resp.Body.Close() } } func main() { InitLogger() defer sugarLogger.Sync() simpleHttpGet(\"www.google.com\") simpleHttpGet(\"http://www.google.com\") } /* 在控制台会输出一下日志信息: {\"level\":\"error\",\"ts\":1623143450.9749353,...... {\"level\":\"error\",\"ts\":1623143450.9749353,...... */ 03.定制logger3.1 测试定制loggerpackage main import ( \"github.com/natefinch/lumberjack\" // Lumberjack进行日志切割归档 \"go.uber.org/zap\" \"go.uber.org/zap/zapcore\" \"net/http\" ) // 第一步:创建一个Sugared Logger var sugarLogger *zap.SugaredLogger // 第二步:将编码器从JSON Encoder更改为普通Encoder func getEncoder() zapcore.Encoder { encoderConfig := zap.NewProductionEncoderConfig() encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder // 为此,我们需要将NewJSONEncoder()更改为NewConsoleEncoder() return zapcore.NewConsoleEncoder(encoderConfig) } // 第三步:使用Lumberjack进行日志切割归档 func getLogWriter() zapcore.WriteSyncer { lumberJackLogger := &lumberjack.Logger{ Filename: \"./test.log\", // 指定日志将写到哪里去 MaxSize: 1, // 每次满1M进行切割 MaxBackups: 5, // 最多报错5个文件 MaxAge: 30, // 文件最多保存30天 Compress: false, // 是否压缩/归档旧文件 } return zapcore.AddSync(lumberJackLogger) } // 第四步:重写InitLogger()方法 func InitLogger() { writeSyncer := getLogWriter() encoder := getEncoder() // zapcore.Core需要三个配置——Encoder,WriteSyncer,LogLevel core := zapcore.NewCore(encoder, writeSyncer, zapcore.DebugLevel) /* Encoder:编码器(如何写入日志),我们将使用开箱即用的NewJSONEncoder(),并使用预先设置的 WriterSyncer :指定日志将写到哪里去。我们使用zapcore.AddSync()函数并且将打开的文件句柄传进去 Log Level:哪种级别的日志将被写入。 */ // 我们将使用zap.New(…)方法来手动传递所有配置,而不是使用像zap.NewProduction()这样的预置方法来创建logger。 logger := zap.New(core, zap.AddCaller()) sugarLogger = logger.Sugar() // 实例化全局变量sugarLogger } // 第五步:函数调用全局sugarLogger写入log日志 func simpleHttpGet(url string) { sugarLogger.Debugf(\"Trying to hit GET request for %s\", url) resp, err := http.Get(url) if err != nil { sugarLogger.Errorf(\"Error fetching URL %s : Error = %s\", url, err) } else { sugarLogger.Infof(\"Success! statusCode = %s for URL %s\", resp.Status, url) resp.Body.Close() } } func main() { InitLogger() defer sugarLogger.Sync() simpleHttpGet(\"www.sogo.com\") simpleHttpGet(\"http://www.sogo.com\") }","categories":[{"name":"Go常用库","slug":"Go常用库","permalink":"http://coderedeng.github.io/categories/Go%E5%B8%B8%E7%94%A8%E5%BA%93/"}],"tags":[{"name":"Go常用库","slug":"Go常用库","permalink":"http://coderedeng.github.io/tags/Go%E5%B8%B8%E7%94%A8%E5%BA%93/"}]},{"title":"reflect","slug":"Go常用库介绍 - reflect","date":"2022-06-05T14:14:35.000Z","updated":"2024-01-22T12:58:59.000Z","comments":true,"path":"2022/06/05/Go常用库介绍 - reflect/","link":"","permalink":"http://coderedeng.github.io/2022/06/05/Go%E5%B8%B8%E7%94%A8%E5%BA%93%E4%BB%8B%E7%BB%8D%20-%20reflect/","excerpt":"","text":"10.reflect01.反射 反射是指在程序运行期对程序本身进行访问和修改的能力 1.1 变量的内在机制 变量包含类型信息和值信息 var arr [10]int arr[0] = 10 类型信息:是静态的元信息,是预先定义好的 值信息:是程序运行过程中动态改变的 1.2 反射的使用 反射是指在程序运行期对程序本身进行访问和修改的能力。 程序在编译时,变量被转换为内存地址,变量名不会被编译器写入到可执行部分。 在运行程序时,程序无法获取自身的信息。 支持反射的语言可以在程序编译期将变量的反射信息,如字段名称、类型信息、结构体信息等整合到可执行文件中 并给程序提供接口访问反射信息,这样就可以在程序运行期获取类型的反射信息,并且有能力修改它们。 Go程序在运行期使用reflect包访问程序的反射信息。 02.反射方法 反射可以在运行时动态获取程序的各种详细信息 2.1 TypeOf reflect.TypeOf()获取类型信息 package main import ( \"fmt\" \"reflect\" ) func reflectType(x interface{}) { v := reflect.TypeOf(x) fmt.Printf(\"type:%v\\n\", v) } func main() { var a float32 = 3.14 reflectType(a) // type:float32 var b int64 = 100 reflectType(b) // type:int64 } 2.2 ValueOf reflect.Value获取值 reflect.Value类型提供的获取原始值的方法如下 方法 说明 Interface() interface {} 将值以 interface{} 类型返回,可以通过类型断言转换为指定类型 Int() int64 将值以 int 类型返回,所有有符号整型均可以此方式返回 Uint() uint64 将值以 uint 类型返回,所有无符号整型均可以此方式返回 Float() float64 将值以双精度(float64)类型返回,所有浮点数(float32、float64)均可以此方式返回 Bool() bool 将值以 bool 类型返回 Bytes() []bytes 将值以字节数组 []bytes 类型返回 String() string 将值以字符串类型返回 package main import ( \"fmt\" \"reflect\" ) func reflectValue(x interface{}) { v := reflect.ValueOf(x) k := v.Kind() switch k { case reflect.Int64: // v.Int()从反射中获取整型的原始值,然后通过int64()强制类型转换 fmt.Printf(\"type is int64, value is %d\\n\", int64(v.Int())) case reflect.Float32: // v.Float()从反射中获取浮点型的原始值,然后通过float32()强制类型转换 fmt.Printf(\"type is float32, value is %f\\n\", float32(v.Float())) case reflect.Float64: // v.Float()从反射中获取浮点型的原始值,然后通过float64()强制类型转换 fmt.Printf(\"type is float64, value is %f\\n\", float64(v.Float())) } } func main() { var a float32 = 3.14 var b int64 = 100 reflectValue(a) // type is float32, value is 3.140000 reflectValue(b) // type is int64, value is 100 // 将int类型的原始值转换为reflect.Value类型 c := reflect.ValueOf(10) fmt.Printf(\"type c :%T\\n\", c) // type c :reflect.Value } 2.3 修改值 想要在函数中通过反射修改变量的值,需要注意函数参数传递的是值拷贝,必须传递变量地址才能修改变量值。 而反射中使用专有的Elem()方法来获取指针对应的值。 package main import ( \"fmt\" \"reflect\" ) func reflectSetValue1(x interface{}) { v := reflect.ValueOf(x) if v.Kind() == reflect.Int64 { v.SetInt(200) //修改的是副本,reflect包会引发panic } } func reflectSetValue2(x interface{}) { v := reflect.ValueOf(x) // 反射中使用 Elem()方法获取指针对应的值 if v.Elem().Kind() == reflect.Int64 { v.Elem().SetInt(200) } } func main() { var a int64 = 100 // reflectSetValue1(a) //panic: reflect: reflect.Value.SetInt using unaddressable value reflectSetValue2(&a) fmt.Println(a) } 2.4 isNil()和isValid() isNil() IsNil()报告v持有的值是否为nil。 v持有的值的分类必须是通道、函数、接口、映射、指针、切片之一; 否则IsNil函数会导致panic。 isValid() IsValid()返回v是否持有一个值。 如果v是Value零值会返回假,此时v除了IsValid、String、Kind之外的方法都会导致panic。 IsNil()常被用于判断指针是否为空;IsValid()常被用于判定返回值是否有效。 func main() { // *int类型空指针 var a *int fmt.Println(\"var a *int IsNil:\", reflect.ValueOf(a).IsNil()) // nil值 fmt.Println(\"nil IsValid:\", reflect.ValueOf(nil).IsValid()) // 实例化一个匿名结构体 b := struct{}{} // 尝试从结构体中查找\"abc\"字段 fmt.Println(\"不存在的结构体成员:\", reflect.ValueOf(b).FieldByName(\"abc\").IsValid()) // 尝试从结构体中查找\"abc\"方法 fmt.Println(\"不存在的结构体方法:\", reflect.ValueOf(b).MethodByName(\"abc\").IsValid()) // map c := map[string]int{} // 尝试从map中查找一个不存在的键 fmt.Println(\"map中不存在的键:\", reflect.ValueOf(c).MapIndex(reflect.ValueOf(\"娜扎\")).IsValid()) } 03.结构体与反射3.1 查看类型、字段和方法package main import ( \"fmt\" \"reflect\" ) // 定义结构体 type User struct { Id int Name string Age int } // 绑方法 func (u User) Hello() { fmt.Println(\"Hello\") } // 传入interface{} func Poni(o interface{}) { t := reflect.TypeOf(o) fmt.Println(\"类型:\", t) fmt.Println(\"字符串类型:\", t.Name()) // 获取值 v := reflect.ValueOf(o) fmt.Println(v) // 可以获取所有属性 // 获取结构体字段个数:t.NumField() for i := 0; i < t.NumField(); i++ { // 取每个字段 f := t.Field(i) fmt.Printf(\"%s : %v\", f.Name, f.Type) // 获取字段的值信息 // Interface():获取字段对应的值 val := v.Field(i).Interface() fmt.Println(\"val :\", val) } fmt.Println(\"=================方法====================\") for i := 0; i < t.NumMethod(); i++ { m := t.Method(i) fmt.Println(m.Name) fmt.Println(m.Type) } } func main() { u := User{1, \"zs\", 20} Poni(u) } 3.2 查看匿名字段package main import ( \"fmt\" \"reflect\" ) // 定义结构体 type User struct { Id int Name string Age int } // 匿名字段 type Boy struct { User Addr string } func main() { m := Boy{User{1, \"zs\", 20}, \"bj\"} t := reflect.TypeOf(m) fmt.Println(t) // Anonymous:匿名 fmt.Printf(\"%#v\\n\", t.Field(0)) // 值信息 fmt.Printf(\"%#v\\n\", reflect.ValueOf(m).Field(0)) } 3.3 修改结构体的值package main import ( \"fmt\" \"reflect\" ) // 定义结构体 type User struct { Id int Name string Age int } // 修改结构体值 func SetValue(o interface{}) { v := reflect.ValueOf(o) // 获取指针指向的元素 v = v.Elem() // 取字段 f := v.FieldByName(\"Name\") if f.Kind() == reflect.String { f.SetString(\"kuteng\") } } func main() { u := User{1, \"5lmh.com\", 20} SetValue(&u) fmt.Println(u) } 3.4 调用方法package main import ( \"fmt\" \"reflect\" ) // 定义结构体 type User struct { Id int Name string Age int } func (u User) Hello(name string) { fmt.Println(\"Hello:\", name) } func main() { u := User{1, \"5lmh.com\", 20} v := reflect.ValueOf(u) // 获取方法 m := v.MethodByName(\"Hello\") // 构建一些参数 args := []reflect.Value{reflect.ValueOf(\"6666\")} // 没参数的情况下:var args2 []reflect.Value // 调用方法,需要传入方法的参数 m.Call(args) } 3.5 获取字段的tagpackage main import ( \"fmt\" \"reflect\" ) type Student struct { Name string `json:\"name1\" db:\"name2\"` } func main() { var s Student v := reflect.ValueOf(&s) // 类型 t := v.Type() // 获取字段 f := t.Elem().Field(0) fmt.Println(f.Tag.Get(\"json\")) fmt.Println(f.Tag.Get(\"db\")) } 04.反射练习 任务:解析如下配置文件 序列化:将结构体序列化为配置文件数据并保存到硬盘 反序列化:将配置文件内容反序列化到程序的结构体 配置文件有server和mysql相关配置 #this is comment ;this a comment ;[]表示一个section [server] ip = 10.238.2.2 port = 8080 [mysql] username = root passwd = admin database = test host = 192.168.10.10 port = 8000 timeout = 1.2","categories":[{"name":"Go常用库","slug":"Go常用库","permalink":"http://coderedeng.github.io/categories/Go%E5%B8%B8%E7%94%A8%E5%BA%93/"}],"tags":[{"name":"Go常用库","slug":"Go常用库","permalink":"http://coderedeng.github.io/tags/Go%E5%B8%B8%E7%94%A8%E5%BA%93/"}]},{"title":"Context","slug":"Go常用库介绍 - Context","date":"2022-06-03T14:42:57.000Z","updated":"2024-04-06T15:03:14.175Z","comments":true,"path":"2022/06/03/Go常用库介绍 - Context/","link":"","permalink":"http://coderedeng.github.io/2022/06/03/Go%E5%B8%B8%E7%94%A8%E5%BA%93%E4%BB%8B%E7%BB%8D%20-%20Context/","excerpt":"","text":"09.Context01.context介绍1.1 context由来 context在Go1.7之后就进入标准库中了,是在于控制goroutine的生命周期。 由于在Golang severs中,每个request都是在单个goroutine中完成 并且在单个goroutine(不妨称之为A)中也会有请求其他服务(启动另一个goroutine(称之为B)去完成)的场景 这就会涉及多个Goroutine之间的调用 如果某一时刻请求其他服务被取消或者超时,则作为深陷其中的当前goroutine B需要立即退出,然后系统才可回收B所占用的资源。 即一个request中通常包含多个goroutine,这些goroutine之间通常会有交互。 那么,如何有效管理这些goroutine成为一个问题(主要是退出通知和元数据传递问题) Google的解决方法是Context机制,相互调用的goroutine之间通过传递context变量保持关联 这样在不用暴露各goroutine内部实现细节的前提下,有效地控制各goroutine的运行。 如此一来,通过传递Context就可以追踪goroutine调用树,并在这些调用树之间传递通知和元数据。 虽然goroutine之间是平行的,没有继承关系,但是Context设计成是包含父子关系的,这样可以更好的描述goroutine调用之间的树型关系。 1.2 context常用方法 context.Context是一个接口,该接口定义了四个需要实现的方法 type Context interface { Deadline() (deadline time.Time, ok bool) Done() <-chan struct{} Err() error Value(key interface{}) interface{} } Done 方法 在Context被取消或超时时返回一个close的channel,close的channel可以作为广播通知 告诉给context相关的函数要停止当前工作然后返回。 当一个父operation启动一个goroutine用于子operation,这些子operation不能够取消父operation。 下面描述的WithCancel函数提供一种方式可以取消新创建的Context. 开发者可以把一个Context传递给任意多个goroutine然后cancel这个context的时候就能够通知到所有的goroutine。 Err方法 返回context为什么被取消 Deadline 返回context何时会超时。 Value 返回context相关的数据。 02.为什么需要context 在 Go http包的Server中,每一个请求在都有一个对应的 goroutine 去处理。 请求处理函数通常会启动额外的 goroutine 用来访问后端服务,比如数据库和RPC服务。 用来处理一个请求的 goroutine 通常需要访问一些与请求特定的数据,比如终端用户的身份认证信息、验证相关的token、请求的截止时间。 当一个请求被取消或超时时,所有用来处理该请求的 goroutine 都应该迅速退出,然后系统才能释放这些 goroutine 占用的资源。 2.1 全局变量解决// 全局变量方式存在的问题: // 1. 使用全局变量在跨包调用时不容易统一 // 2. 如果worker中再启动goroutine,就不太好控制了。 package main import ( \"fmt\" \"sync\" \"time\" ) var wg sync.WaitGroup var exit bool func worker() { for { fmt.Println(\"worker\") time.Sleep(time.Second) if exit { break } } wg.Done() } func main() { wg.Add(1) go worker() time.Sleep(time.Second * 3) // sleep3秒以免程序过快退出 exit = true // 修改全局变量实现子goroutine的退出 wg.Wait() fmt.Println(\"over\") } 2.2 通道方式package main import ( \"fmt\" \"sync\" \"time\" ) var wg sync.WaitGroup // 管道方式存在的问题: // 1. 使用全局变量在跨包调用时不容易实现规范和统一,需要维护一个共用的channel func worker(exitChan chan struct{}) { LOOP: for { fmt.Println(\"worker\") time.Sleep(time.Second) select { case <-exitChan: // 等待接收上级通知 break LOOP default: } } wg.Done() } func main() { var exitChan = make(chan struct{}) wg.Add(1) go worker(exitChan) time.Sleep(time.Second * 3) // sleep3秒以免程序过快退出 exitChan <- struct{}{} // 给子goroutine发送退出信号 close(exitChan) wg.Wait() fmt.Println(\"over\") } 2.3 官方版的方案package main import ( \"context\" \"fmt\" \"sync\" \"time\" ) var wg sync.WaitGroup func worker(ctx context.Context) { LOOP: for { fmt.Println(\"worker\") time.Sleep(time.Second) select { case <-ctx.Done(): // 等待上级通知 break LOOP default: } } wg.Done() } func main() { ctx, cancel := context.WithCancel(context.Background()) wg.Add(1) go worker(ctx) time.Sleep(time.Second * 3) cancel() // 通知子goroutine结束 wg.Wait() fmt.Println(\"over\") } 03.With系列函数此外,context包中还定义了四个With系列函数。 3.1 WithCancel WithCancel返回带有新Done通道的父节点的副本。 当调用返回的cancel函数或当关闭父上下文的Done通道时,将关闭返回上下文的Done通道,无论先发生什么情况。 取消此上下文将释放与其关联的资源,因此代码应该在此上下文中运行的操作完成后立即调用cancel。 示例 上面的示例代码中,gen函数在单独的goroutine中生成整数并将它们发送到返回的通道。 gen的调用者在使用生成的整数之后需要取消上下文,以免gen启动的内部goroutine发生泄漏。 package main import ( \"context\" \"fmt\" ) func gen(ctx context.Context) <-chan int { dst := make(chan int) n := 1 go func() { for { select { case <-ctx.Done(): return // return结束该goroutine,防止泄露 case dst <- n: n++ } } }() return dst } func main() { ctx, cancel := context.WithCancel(context.Background()) defer cancel() // 当我们取完需要的整数后调用cancel for n := range gen(ctx) { fmt.Println(n) if n == 5 { break } } } /* 1 2 3 4 5 */ 3.2 WithDeadline 下面的代码中,定义了一个50毫秒之后过期的deadline 然后我们调用context.WithDeadline(context.Background(), d)得到一个上下文(ctx)和一个取消函数(cancel) 然后使用一个select让主程序陷入等待:等待1秒后打印overslept退出或者等待ctx过期后退出。 因为ctx50秒后就过期,所以ctx.Done()会先接收到值,上面的代码会打印ctx.Err()取消原因。 package main import ( \"context\" \"fmt\" \"time\" ) func main() { d := time.Now().Add(50 * time.Millisecond) ctx, cancel := context.WithDeadline(context.Background(), d) // 尽管ctx会过期,但在任何情况下调用它的cancel函数都是很好的实践。 // 如果不这样做,可能会使上下文及其父类存活的时间超过必要的时间。 defer cancel() select { case <-time.After(1 * time.Second): fmt.Println(\"overslept\") case <-ctx.Done(): fmt.Println(ctx.Err()) } } 3.3WithTimeout 取消此上下文将释放与其相关的资源,因此代码应该在此上下文中运行的操作完成后立即调用cancel 通常用于数据库或者网络连接的超时控制 package main import ( \"context\" \"fmt\" \"sync\" \"time\" ) // context.WithTimeout var wg sync.WaitGroup func worker(ctx context.Context) { LOOP: for { fmt.Println(\"db connecting ...\") time.Sleep(time.Millisecond * 10) // 假设正常连接数据库耗时10毫秒 select { case <-ctx.Done(): // 50毫秒后自动调用 break LOOP default: } } fmt.Println(\"worker done!\") wg.Done() } func main() { // 设置一个50毫秒的超时 ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*50) wg.Add(1) go worker(ctx) time.Sleep(time.Second * 5) cancel() // 通知子goroutine结束 wg.Wait() fmt.Println(\"over\") } 3.4 WithValue WithValue函数能够将请求作用域的数据与 Context 对象建立关系 WithValue返回父节点的副本,其中与key关联的值为val。 仅对API和进程间传递请求域的数据使用上下文值,而不是使用它来传递可选参数给函数。 所提供的键必须是可比较的,并且不应该是string类型或任何其他内置类型,以避免使用上下文在包之间发生冲突。 WithValue的用户应该为键定义自己的类型。为了避免在分配给interface{}时进行分配,上下文键通常具有具体类型struct{}。 或者,导出的上下文关键变量的静态类型应该是指针或接口。 package main import ( \"context\" \"fmt\" \"sync\" \"time\" ) // context.WithValue type TraceCode string var wg sync.WaitGroup func worker(ctx context.Context) { key := TraceCode(\"TRACE_CODE\") traceCode, ok := ctx.Value(key).(string) // 在子goroutine中获取trace code if !ok { fmt.Println(\"invalid trace code\") } LOOP: for { fmt.Printf(\"worker, trace code:%s\\n\", traceCode) time.Sleep(time.Millisecond * 10) // 假设正常连接数据库耗时10毫秒 select { case <-ctx.Done(): // 50毫秒后自动调用 break LOOP default: } } fmt.Println(\"worker done!\") wg.Done() } func main() { // 设置一个50毫秒的超时 ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*50) // 在系统的入口中设置trace code传递给后续启动的goroutine实现日志数据聚合 ctx = context.WithValue(ctx, TraceCode(\"TRACE_CODE\"), \"12512312234\") wg.Add(1) go worker(ctx) time.Sleep(time.Second * 5) cancel() // 通知子goroutine结束 wg.Wait() fmt.Println(\"over\") } 04.客户端超时取消示例调用服务端API时如何在客户端实现超时控制? 4.1 server端// context_timeout/server/main.go package main import ( \"fmt\" \"math/rand\" \"net/http\" \"time\" ) // server端,随机出现慢响应 func indexHandler(w http.ResponseWriter, r *http.Request) { number := rand.Intn(2) if number == 0 { time.Sleep(time.Second * 10) // 耗时10秒的慢响应 fmt.Fprintf(w, \"slow response\") return } fmt.Fprint(w, \"quick response\") } func main() { http.HandleFunc(\"/\", indexHandler) err := http.ListenAndServe(\":8000\", nil) if err != nil { panic(err) } } 4.2 client端// context_timeout/client/main.go package main import ( \"context\" \"fmt\" \"io/ioutil\" \"net/http\" \"sync\" \"time\" ) // 客户端 type respData struct { resp *http.Response err error } func doCall(ctx context.Context) { transport := http.Transport{ // 请求频繁可定义全局的client对象并启用长链接 // 请求不频繁使用短链接 DisableKeepAlives: true, } client := http.Client{ Transport: &transport, } respChan := make(chan *respData, 1) req, err := http.NewRequest(\"GET\", \"http://127.0.0.1:8000/\", nil) if err != nil { fmt.Printf(\"new requestg failed, err:%v\\n\", err) return } req = req.WithContext(ctx) // 使用带超时的ctx创建一个新的client request var wg sync.WaitGroup wg.Add(1) defer wg.Wait() go func() { resp, err := client.Do(req) fmt.Printf(\"client.do resp:%v, err:%v\\n\", resp, err) rd := &respData{ resp: resp, err: err, } respChan <- rd wg.Done() }() select { case <-ctx.Done(): //transport.CancelRequest(req) fmt.Println(\"call api timeout\") case result := <-respChan: fmt.Println(\"call server api success\") if result.err != nil { fmt.Printf(\"call server api failed, err:%v\\n\", result.err) return } defer result.resp.Body.Close() data, _ := ioutil.ReadAll(result.resp.Body) fmt.Printf(\"resp:%v\\n\", string(data)) } } func main() { // 定义一个100毫秒的超时 ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*100) defer cancel() // 调用cancel释放子goroutine资源 doCall(ctx) }","categories":[{"name":"Go常用库","slug":"Go常用库","permalink":"http://coderedeng.github.io/categories/Go%E5%B8%B8%E7%94%A8%E5%BA%93/"}],"tags":[{"name":"Go常用库","slug":"Go常用库","permalink":"http://coderedeng.github.io/tags/Go%E5%B8%B8%E7%94%A8%E5%BA%93/"}]},{"title":"net/http","slug":"Go常用库介绍 - net http","date":"2022-06-01T13:35:53.000Z","updated":"2024-01-22T12:58:59.000Z","comments":true,"path":"2022/06/01/Go常用库介绍 - net http/","link":"","permalink":"http://coderedeng.github.io/2022/06/01/Go%E5%B8%B8%E7%94%A8%E5%BA%93%E4%BB%8B%E7%BB%8D%20-%20net%20http/","excerpt":"","text":"08.net/http01.net/http(GET) Go语言内置的net/http包十分的优秀,提供了HTTP客户端和服务端的实现。 1.1 无参GET 使用net/http包编写一个简单的发送HTTP请求的Client端 package main import ( \"fmt\" \"io/ioutil\" \"net/http\" ) func main() { resp, err := http.Get(\"https://www.baidu.com/\") if err != nil { fmt.Println(\"get failed, err:\", err) return } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { fmt.Println(\"read from resp.Body failed,err:\", err) return } fmt.Print(string(body)) } 1.2 带参GET 关于GET请求的参数需要使用Go语言内置的net/url这个标准库来处理。 关于GET请求的参数需要使用Go语言内置的net/url这个标准库来处理。 func main() { apiUrl := \"http://127.0.0.1:9090/get\" // URL param data := url.Values{} data.Set(\"name\", \"枯藤\") data.Set(\"age\", \"18\") u, err := url.ParseRequestURI(apiUrl) if err != nil { fmt.Printf(\"parse url requestUrl failed,err:%v\\n\", err) } u.RawQuery = data.Encode() // URL encode fmt.Println(u.String()) resp, err := http.Get(u.String()) if err != nil { fmt.Println(\"post failed, err:%v\\n\", err) return } defer resp.Body.Close() b, err := ioutil.ReadAll(resp.Body) if err != nil { fmt.Println(\"get resp failed,err:%v\\n\", err) return } fmt.Println(string(b)) } 对应的Server端HandlerFunc如下: func getHandler(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() data := r.URL.Query() fmt.Println(data.Get(\"name\")) fmt.Println(data.Get(\"age\")) answer := `{\"status\": \"ok\"}` w.Write([]byte(answer)) } 1.3 自定义Client 要管理HTTP客户端的头域、重定向策略和其他设置,创建一个Client: client := &http.Client{ CheckRedirect: redirectPolicyFunc, } resp, err := client.Get(\"http://5lmh.com\") // ... req, err := http.NewRequest(\"GET\", \"http://5lmh.com\", nil) // ... req.Header.Add(\"If-None-Match\", `W/\"wyzzy\"`) resp, err := client.Do(req) // ... 1.4 自定义Transport 要管理代理、TLS配置、keep-alive、压缩和其他设置,创建一个Transport tr := &http.Transport{ TLSClientConfig: &tls.Config{RootCAs: pool}, DisableCompression: true, } client := &http.Client{Transport: tr} resp, err := client.Get(\"https://5lmh.com\") Client和Transport类型都可以安全的被多个go程同时使用,出于效率考虑,应该一次建立、尽量重用。 02.net/http(POST)2.1 Post请求示例 上面演示了使用net/http包发送GET请求的示例,发送POST请求的示例代码如下: package main import ( \"fmt\" \"io/ioutil\" \"net/http\" \"strings\" ) // net/http post demo func main() { url := \"http://127.0.0.1:9090/post\" // 表单数据 //contentType := \"application/x-www-form-urlencoded\" //data := \"name=枯藤&age=18\" // json contentType := \"application/json\" data := `{\"name\":\"枯藤\",\"age\":18}` resp, err := http.Post(url, contentType, strings.NewReader(data)) if err != nil { fmt.Println(\"post failed, err:%v\\n\", err) return } defer resp.Body.Close() b, err := ioutil.ReadAll(resp.Body) if err != nil { fmt.Println(\"get resp failed,err:%v\\n\", err) return } fmt.Println(string(b)) } 2.2 Server端 对应的Server端HandlerFunc如下: func postHandler(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() // 1. 请求类型是application/x-www-form-urlencoded时解析form数据 r.ParseForm() fmt.Println(r.PostForm) // 打印form数据 fmt.Println(r.PostForm.Get(\"name\"), r.PostForm.Get(\"age\")) // 2. 请求类型是application/json时从r.Body读取数据 b, err := ioutil.ReadAll(r.Body) if err != nil { fmt.Println(\"read request.Body failed, err:%v\\n\", err) return } fmt.Println(string(b)) answer := `{\"status\": \"ok\"}` w.Write([]byte(answer)) } 03.服务端3.1 默认的Server ListenAndServe使用指定的监听地址和处理器启动一个HTTP服务端。 处理器参数通常是nil,这表示采用包变量DefaultServeMux作为处理器。 Handle和HandleFunc函数可以向DefaultServeMux添加处理器。 http.Handle(\"/foo\", fooHandler) http.HandleFunc(\"/bar\", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, \"Hello, %q\", html.EscapeString(r.URL.Path)) }) log.Fatal(http.ListenAndServe(\":8080\", nil)) 示例 package main import ( \"fmt\" \"net/http\" ) func sayHello(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, \"Hello World!\") } func main() { http.HandleFunc(\"/\", sayHello) err := http.ListenAndServe(\":9090\", nil) if err != nil { fmt.Printf(\"http server failed, err:%v\\n\", err) return } } 3.2 自定义Server 要管理服务端的行为,可以创建一个自定义的Server: s := &http.Server{ Addr: \":8080\", Handler: myHandler, ReadTimeout: 10 * time.Second, WriteTimeout: 10 * time.Second, MaxHeaderBytes: 1 << 20, } log.Fatal(s.ListenAndServe())","categories":[{"name":"Go常用库","slug":"Go常用库","permalink":"http://coderedeng.github.io/categories/Go%E5%B8%B8%E7%94%A8%E5%BA%93/"}],"tags":[{"name":"Go常用库","slug":"Go常用库","permalink":"http://coderedeng.github.io/tags/Go%E5%B8%B8%E7%94%A8%E5%BA%93/"}]},{"title":"Strconv","slug":"Go常用库介绍 - Strconv","date":"2022-05-31T14:24:31.000Z","updated":"2024-01-22T12:58:59.000Z","comments":true,"path":"2022/05/31/Go常用库介绍 - Strconv/","link":"","permalink":"http://coderedeng.github.io/2022/05/31/Go%E5%B8%B8%E7%94%A8%E5%BA%93%E4%BB%8B%E7%BB%8D%20-%20Strconv/","excerpt":"","text":"07.Strconv01.string与int类型转换1.0 strconv包介绍 strconv包实现了基本数据类型与其字符串表示的转换 主要有以下常用函数: Atoi()、Itia()、parse系列、format系列、append系列。 更多函数请查看官方文档 (opens new window)。 1.1 Atoi()转int Atoi()函数用于将字符串类型的整数转换为int类型,函数签名如下。 package main import ( \"fmt\" \"strconv\" ) func main() { s1 := \"100\" i1, err := strconv.Atoi(s1) if err != nil { fmt.Println(\"can't convert to int\") } else { fmt.Printf(\"type:%T value:%#v\\n\", i1, i1) //type:int value:100 } } 1.2 Itoa()转str Itoa()函数用于将int类型数据转换为对应的字符串表示 package main import ( \"fmt\" \"strconv\" ) func main() { i2 := 200 s2 := strconv.Itoa(i2) fmt.Printf(\"type:%T value:%#v\\n\", s2, s2) //type:string value:\"200\" } 1.3 string转字符package main import ( \"fmt\" ) func main() { s := \"hello 张三\" for _, r := range s { //rune // 104(h) 101(e) 108(l) 108(l) 111(o) 32( ) 24352(张) 19977(三) fmt.Printf(\"%v(%c) \", r, r) } fmt.Println() } 02.Parse系列函数1.1 ParseInt()package main import ( \"fmt\" \"strconv\" ) func main() { var s = \"1234\" i64, _ := strconv.ParseInt(s, 10, 64) fmt.Printf(\"值:%v 类型:%T\", i64, i64) // 值:1234 类型:int64 } 1.2 ParseFloat()package main import ( \"fmt\" \"strconv\" ) func main() { str := \"3.1415926535\" v1, _ := strconv.ParseFloat(str, 32) v2, _ := strconv.ParseFloat(str, 64) fmt.Printf(\"值:%v 类型:%T\\n\", v1, v1) // 值:3.1415927410125732 类型:float64 fmt.Printf(\"值:%v 类型:%T\", v2, v2) // 值:3.1415926535 类型:float64 } 1.3 ParseBool()package main import ( \"fmt\" \"strconv\" ) func main() { b, _ := strconv.ParseBool(\"true\") // string 转 bool fmt.Printf(\"值:%v 类型:%T\", b, b) // 值:true 类型:bool } 03.Format系列函数 Format系列函数实现了将给定类型数据格式化为string类型数据的功能 package main import ( \"fmt\" \"strconv\" ) func main() { s1 := strconv.FormatBool(true) s2 := strconv.FormatFloat(3.1415, 'E', -1, 64) s3 := strconv.FormatInt(-2, 16) s4 := strconv.FormatUint(2, 16) fmt.Printf(\"%v --%T \\n\", s1, s1) // true --string fmt.Printf(\"%v --%T \\n\", s2, s2) // 3.1415E+00 --string fmt.Printf(\"%v --%T \\n\", s3, s3) // -2 --string fmt.Printf(\"%v --%T \\n\", s4, s4) // 2 --string }","categories":[{"name":"Go常用库","slug":"Go常用库","permalink":"http://coderedeng.github.io/categories/Go%E5%B8%B8%E7%94%A8%E5%BA%93/"}],"tags":[{"name":"Go常用库","slug":"Go常用库","permalink":"http://coderedeng.github.io/tags/Go%E5%B8%B8%E7%94%A8%E5%BA%93/"}]},{"title":"IO","slug":"Go常用库介绍 - IO操作","date":"2022-05-29T14:13:26.000Z","updated":"2024-01-22T12:58:59.000Z","comments":true,"path":"2022/05/29/Go常用库介绍 - IO操作/","link":"","permalink":"http://coderedeng.github.io/2022/05/29/Go%E5%B8%B8%E7%94%A8%E5%BA%93%E4%BB%8B%E7%BB%8D%20-%20IO%E6%93%8D%E4%BD%9C/","excerpt":"","text":"06.IO操作01.打开和关闭文件 os.Open()函数能够打开一个文件,返回一个*File和一个err。 对得到的文件实例调用close()方法能够关闭文件。 为了防止文件忘记关闭,我们通常使用defer注册文件关闭语句。 package main import ( \"fmt\" \"os\" ) func main() { // 只读方式打开当前目录下的main.go文件 file, err := os.Open(\"./main.go\") if err != nil { fmt.Println(\"open file failed!, err:\", err) return } // 关闭文件 file.Close() } 02.读取文件2.1 file.Read()指定读取sizepackage main import ( \"fmt\" \"io\" \"os\" ) func main() { // 只读方式打开当前目录下的main.go文件 file, err := os.Open(\"./main.go\") if err != nil { fmt.Println(\"open file failed!, err:\", err) return } defer file.Close() // 使用Read方法读取数据,每次只读取128个字节 var tmp = make([]byte, 128) n, err := file.Read(tmp) if err == io.EOF { fmt.Println(\"文件读完了\") return } if err != nil { fmt.Println(\"read file failed, err:\", err) return } fmt.Printf(\"读取了%d字节数据\\n\", n) fmt.Println(string(tmp[:n])) } 2.2 循环读取 使用for循环读取文件中的所有数据。 package main import ( \"fmt\" \"io\" \"os\" ) func main() { // 只读方式打开当前目录下的main.go文件 file, err := os.Open(\"./main.go\") if err != nil { fmt.Println(\"open file failed!, err:\", err) return } defer file.Close() // 循环读取文件 var content []byte var tmp = make([]byte, 128) for { n, err := file.Read(tmp) if err == io.EOF { fmt.Println(\"文件读完了\") break } if err != nil { fmt.Println(\"read file failed, err:\", err) return } content = append(content, tmp[:n]...) } fmt.Println(string(content)) } 2.3 bufio按行读取 bufio是在file的基础上封装了一层API,支持更多的功能。 package main import ( \"bufio\" \"fmt\" \"io\" \"os\" ) // bufio按行读取示例 func main() { file, err := os.Open(\"./main.go\") if err != nil { fmt.Println(\"open file failed, err:\", err) return } defer file.Close() reader := bufio.NewReader(file) for { line, err := reader.ReadString('\\n') //注意是字符 if err == io.EOF { if len(line) != 0 { fmt.Println(line) } fmt.Println(\"文件读完了\") break } if err != nil { fmt.Println(\"read file failed, err:\", err) return } fmt.Print(line) } } 2.4 ioutil读取整个文件 io/ioutil包的ReadFile方法能够读取完整的文件,只需要将文件名作为参数传入。 package main import ( \"fmt\" \"io/ioutil\" ) // ioutil.ReadFile读取整个文件 func main() { content, err := ioutil.ReadFile(\"./main.go\") if err != nil { fmt.Println(\"read file failed, err:\", err) return } fmt.Println(string(content)) } 03.写入文件3.0 参数说明 os.OpenFile()函数能够以指定模式打开文件,从而实现文件写入相关功能。 func OpenFile(name string, flag int, perm FileMode) (*File, error) { ... } 其中: name:要打开的文件名 flag:打开文件的模式,模式有以下几种 模式 含义 os.O_WRONLY 只写 os.O_CREATE 创建文件 os.O_RDONLY 只读 os.O_RDWR 读写 os.O_TRUNC 清空 os.O_APPEND 追加 perm :文件权限,一个八进制数 - r(读)04,w(写)02,x(执行)01。 ### 3.1 Write和WriteString ```go package main import ( "fmt" "os" ) func main() { file, err := os.OpenFile("xx.txt", os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666) if err != nil { fmt.Println("open file failed, err:", err) return } defer file.Close() str := "hello 沙河" file.Write([]byte(str)) //写入字节切片数据 file.WriteString("hello 小王子") //直接写入字符串数据 } 3.2 bufio.NewWriterpackage main import ( \"bufio\" \"fmt\" \"os\" ) func main() { file, err := os.OpenFile(\"xx.txt\", os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666) if err != nil { fmt.Println(\"open file failed, err:\", err) return } defer file.Close() writer := bufio.NewWriter(file) for i := 0; i < 10; i++ { writer.WriteString(\"hello沙河\\n\") //将数据先写入缓存 } writer.Flush() //将缓存中的内容写入文件 } 3.3 ioutil.WriteFilepackage main import ( \"fmt\" \"io/ioutil\" ) func main() { str := \"hello 沙河\" err := ioutil.WriteFile(\"./xx.txt\", []byte(str), 0666) if err != nil { fmt.Println(\"write file failed, err:\", err) return } } 04.练习4.1 copyFile 借助io.Copy()实现一个拷贝文件函数。 // CopyFile 拷贝文件函数 func CopyFile(dstName, srcName string) (written int64, err error) { // 以读方式打开源文件 src, err := os.Open(srcName) if err != nil { fmt.Printf(\"open %s failed, err:%v.\\n\", srcName, err) return } defer src.Close() // 以写|创建的方式打开目标文件 dst, err := os.OpenFile(dstName, os.O_WRONLY|os.O_CREATE, 0644) if err != nil { fmt.Printf(\"open %s failed, err:%v.\\n\", dstName, err) return } defer dst.Close() return io.Copy(dst, src) //调用io.Copy()拷贝内容 } func main() { _, err := CopyFile(\"dst.txt\", \"src.txt\") if err != nil { fmt.Println(\"copy file failed, err:\", err) return } fmt.Println(\"copy done!\") } 4.2 实现一个cat命令 使用文件操作相关知识,模拟实现linux平台cat命令的功能。 package main import ( \"bufio\" \"flag\" \"fmt\" \"io\" \"os\" ) // cat命令实现 func cat(r *bufio.Reader) { for { buf, err := r.ReadBytes('\\n') //注意是字符 if err == io.EOF { // 退出之前将已读到的内容输出 fmt.Fprintf(os.Stdout, \"%s\", buf) break } fmt.Fprintf(os.Stdout, \"%s\", buf) } } func main() { flag.Parse() // 解析命令行参数 if flag.NArg() == 0 { // 如果没有参数默认从标准输入读取内容 cat(bufio.NewReader(os.Stdin)) } // 依次读取每个指定文件的内容并打印到终端 for i := 0; i < flag.NArg(); i++ { f, err := os.Open(flag.Arg(i)) if err != nil { fmt.Fprintf(os.Stdout, \"reading from %s failed, err:%v\\n\", flag.Arg(i), err) continue } cat(bufio.NewReader(f)) } }","categories":[{"name":"Go常用库","slug":"Go常用库","permalink":"http://coderedeng.github.io/categories/Go%E5%B8%B8%E7%94%A8%E5%BA%93/"}],"tags":[{"name":"Go常用库","slug":"Go常用库","permalink":"http://coderedeng.github.io/tags/Go%E5%B8%B8%E7%94%A8%E5%BA%93/"}]},{"title":"Log","slug":"Go常用库介绍 - Log","date":"2022-05-27T14:17:35.000Z","updated":"2024-01-22T12:58:59.000Z","comments":true,"path":"2022/05/27/Go常用库介绍 - Log/","link":"","permalink":"http://coderedeng.github.io/2022/05/27/Go%E5%B8%B8%E7%94%A8%E5%BA%93%E4%BB%8B%E7%BB%8D%20-%20Log/","excerpt":"","text":"05.Log01.日志模块介绍 参考博客(opens new window) 1.1 介绍在许多Go语言项目中,我们需要一个好的日志记录器能够提供下面这些功能 能够将事件记录到文件中,而不是应用程序控制台。 日志切割-能够根据文件大小、时间或间隔等来切割日志文件。 支持不同的日志级别。例如INFO,DEBUG,ERROR等。 能够打印基本信息,如调用文件/函数名和行号,日志时间等。 1.2 默认的Go Logger 实现一个Go语言中的日志记录器非常简单——创建一个新的日志文件,然后设置它为日志的输出位置。 package main import ( \"log\" \"net/http\" \"os\" ) // 第一:设置Logger func SetupLogger() { logFileLocation, _ := os.OpenFile(\"./test.log\", os.O_CREATE|os.O_APPEND|os.O_RDWR, 0744) log.SetOutput(logFileLocation) } // 第二:使用Logger func simpleHttpGet(url string) { resp, err := http.Get(url) if err != nil { log.Printf(\"Error fetching url %s : %s\", url, err.Error()) } else { log.Printf(\"Status Code for %s : %s\", url, resp.Status) resp.Body.Close() } } func main() { SetupLogger() simpleHttpGet(\"www.baidu.com\") simpleHttpGet(\"http://www.baidu.com\") } 1.3 Go Logger的优势和劣势 优势 它最大的优点是使用非常简单。 我们可以设置任何io.Writer作为日志记录输出并向其发送要写入的日志。 劣势 仅限基本的日志级别 只有一个Print选项。不支持INFO/DEBUG等多个级别。 缺乏日志格式化的能力——例如记录调用者的函数名和行号,格式化日期和时间格式。等等。 不提供日志切割的能力。","categories":[{"name":"Go常用库","slug":"Go常用库","permalink":"http://coderedeng.github.io/categories/Go%E5%B8%B8%E7%94%A8%E5%BA%93/"}],"tags":[{"name":"Go常用库","slug":"Go常用库","permalink":"http://coderedeng.github.io/tags/Go%E5%B8%B8%E7%94%A8%E5%BA%93/"}]},{"title":"Flag","slug":"Go常用库介绍 - Flag","date":"2022-05-26T14:01:24.000Z","updated":"2024-01-22T12:58:59.000Z","comments":true,"path":"2022/05/26/Go常用库介绍 - Flag/","link":"","permalink":"http://coderedeng.github.io/2022/05/26/Go%E5%B8%B8%E7%94%A8%E5%BA%93%E4%BB%8B%E7%BB%8D%20-%20Flag/","excerpt":"","text":"04.Flag01.Flag Go语言内置的flag包实现了命令行参数的解析,flag包使得开发命令行工具更为简单。 1.1 os.Args 如果你只是简单的想要获取命令行参数,可以像下面的代码示例一样使用os.Args来获取命令行参数 os.Args是一个存储命令行参数的字符串切片,它的第一个元素是执行文件的名称。 package main import ( \"fmt\" \"os\" ) //os.Args demo func main() { //os.Args是一个[]string if len(os.Args) > 0 { for index, arg := range os.Args { fmt.Printf(\"args[%d]=%v\\n\", index, arg) } } } /* C:\\aaa\\gin_demo> go run main.go a b c args[1]=a args[2]=b args[3]=c */ 1.2 flag.Parse()通过以上两种方法定义好命令行flag参数后,需要通过调用flag.Parse()来对命令行参数进行解析。 支持的命令行参数格式有以下几种: -flag xxx (使用空格,一个-符号) –flag xxx (使用空格,两个-符号) -flag=xxx (使用等号,一个-符号) –flag=xxx (使用等号,两个-符号) 其中,布尔类型的参数必须使用等号的方式指定。 Flag解析在第一个非flag参数(单个”-“不是flag参数)之前停止,或者在终止符”–“之后停止。 1.3 其他函数 flag.Args() ////返回命令行参数后的其他参数,以[]string类型 flag.NArg() //返回命令行参数后的其他参数个数 flag.NFlag() //返回使用的命令行参数个数 02.完整示例2.1 main.gopackage main import ( \"flag\" \"fmt\" \"time\" ) func main() { //定义命令行参数方式1 var name string var age int var married bool var delay time.Duration flag.StringVar(&name, \"name\", \"张三\", \"姓名\") flag.IntVar(&age, \"age\", 18, \"年龄\") flag.BoolVar(&married, \"married\", false, \"婚否\") flag.DurationVar(&delay, \"d\", 0, \"延迟的时间间隔\") //解析命令行参数 flag.Parse() fmt.Println(name, age, married, delay) //返回命令行参数后的其他参数 fmt.Println(flag.Args()) //返回命令行参数后的其他参数个数 fmt.Println(flag.NArg()) //返回使用的命令行参数个数 fmt.Println(flag.NFlag()) } 2.2 查看帮助C:\\aaa\\gin_demo> go run main.go --help -age int 年龄 (default 18) -d duration 延迟的时间间隔 -married 婚否 -name string 姓名 (default \"张三\") 2.3 flag参数演示C:\\aaa\\gin_demo> go run main.go -name pprof --age 28 -married=false -d=1h30m pprof 28 false 1h30m0s [] 0 4 2.4 非flag命令行参数C:\\aaa\\gin_demo>go run main.go a b c 张三 18 false 0s [a b c] 3 0","categories":[{"name":"Go常用库","slug":"Go常用库","permalink":"http://coderedeng.github.io/categories/Go%E5%B8%B8%E7%94%A8%E5%BA%93/"}],"tags":[{"name":"Go常用库","slug":"Go常用库","permalink":"http://coderedeng.github.io/tags/Go%E5%B8%B8%E7%94%A8%E5%BA%93/"}]},{"title":"encodingjson","slug":"Go常用库介绍 - encodingjson","date":"2022-05-24T13:11:52.000Z","updated":"2024-01-22T12:58:59.000Z","comments":true,"path":"2022/05/24/Go常用库介绍 - encodingjson/","link":"","permalink":"http://coderedeng.github.io/2022/05/24/Go%E5%B8%B8%E7%94%A8%E5%BA%93%E4%BB%8B%E7%BB%8D%20-%20encodingjson/","excerpt":"","text":"03.encoding/json包01.struct与json 比如我们 Golang 要给 App 或者小程序提供 Api 接口数据,这个时候就需要涉及到结构体和Json 之间的相互转换 GolangJSON 序列化是指把结构体数据转化成 JSON 格式的字符串 Golang JSON 的反序列化是指把 JSON 数据转化成 Golang 中的结构体对象 Golang 中 的 序 列 化 和 反 序 列 化 主 要 通 过 “encoding/json” 包 中 的 json.Marshal() 和json.Unmarshal()方法实现 1.1 struct转Json字符串package main import ( \"encoding/json\" \"fmt\" ) type Student struct { ID int Gender string name string //私有属性不能被 json 包访问 Sno string } func main() { var s1 = Student{ ID: 1, Gender: \"男\", name: \"李四\", Sno: \"s0001\", } fmt.Printf(\"%#v\\n\", s1) // main.Student{ID:1, Gender:\"男\", name:\"李四\", Sno:\"s0001\"} var s, _ = json.Marshal(s1) jsonStr := string(s) fmt.Println(jsonStr) // {\"ID\":1,\"Gender\":\"男\",\"Sno\":\"s0001\"} } 1.2 Json字符串转structpackage main import ( \"encoding/json\" \"fmt\" ) type Student struct { ID int Gender string Name string Sno string } func main() { var jsonStr = `{\"ID\":1,\"Gender\":\"男\",\"Name\":\"李四\",\"Sno\":\"s0001\"}` var student Student //定义一个 Monster 实例 err := json.Unmarshal([]byte(jsonStr), &student) if err != nil { fmt.Printf(\"unmarshal err=%v\\n\", err) } // 反序列化后 student=main.Student{ID:1, Gender:\"男\", Name:\"李四\", Sno:\"s0001\"} student.Name=李四 fmt.Printf(\"反序列化后 student=%#v student.Name=%v \\n\", student, student.Name) } 02. struct tag2.1 Tag标签说明 Tag 是结构体的元信息,可以在运行的时候通过反射的机制读取出来。 Tag 在结构体字段的后方定义,由一对反引号包裹起来 具体的格式如下: key1:\"value1\" key2:\"value2\" 1 结构体 tag 由一个或多个键值对组成。键与值使用冒号分隔,值用双引号括起来。 同一个结构体字段可以设置多个键值对 tag,不同的键值对之间使用空格分隔。 注意事项: 为结构体编写 Tag 时,必须严格遵守键值对的规则。 结构体标签的解析代码的容错能力很差,一旦格式写错,编译和运行时都不会提示任何错误,通过反射也无法正确取值。 例如不要在 key 和 value 之间添加空格。 2.2 Tag结构体转化Json字符串package main import ( \"encoding/json\" \"fmt\" ) type Student struct { ID int `json:\"id\"` //通过指定 tag 实现 json 序列化该字段时的 key Gender string `json:\"gender\"` Name string Sno string } func main() { var s1 = Student{ ID: 1, Gender: \"男\", Name: \"李四\", Sno: \"s0001\", } // main.Student{ID:1, Gender:\"男\", Name:\"李四\", Sno:\"s0001\"} fmt.Printf(\"%#v\\n\", s1) var s, _ = json.Marshal(s1) jsonStr := string(s) fmt.Println(jsonStr) // {\"id\":1,\"gender\":\"男\",\"Name\":\"李四\",\"Sno\":\"s0001\"} } 2.3 Json字符串转成Tag结构体package main import ( \"encoding/json\" \"fmt\" ) type Student struct { ID int `json:\"id\"` //通过指定 tag 实现 json 序列化该字段时的 key Gender string `json:\"gender\"` Name string Sno string } func main() { var s2 Student var str = `{\"id\":1,\"gender\":\"男\",\"Name\":\"李四\",\"Sno\":\"s0001\"}` err := json.Unmarshal([]byte(str), &s2) if err != nil { fmt.Println(err) } // main.Student{ID:1, Gender:\"男\", Name:\"李四\", Sno:\"s0001\"} fmt.Printf(\"%#v\", s2) } 2.4 加tag坑 如果变量首字母小写,则为private。无论如何不能转,因为取不到反射信息。 如果变量首字母大写,则为public。 不加tag ,可以正常转为 json 里的字段, json 内字段名跟结构体内字段 原名一致 。 - `加了tag`,从`struct`转`json`的时候,`json`的字段名就是`tag`里的字段名,原字段名已经没用。 ```go package main import ( "encoding/json" "fmt" ) type J struct { a string // 首字母小写,不能转换成json b string `json:"B"` // 首字母小写,不能转换成json C string // 不加`tag`则`json`内的字段跟结构体字段`原名一致`。 D string `json:"dd"` // 而`大写的`加了`tag`可以`取别名` } func main() { j := J { a: "1", b: "2", C: "3", D: "4", } fmt.Printf("转为json前j结构体的内容 = %+v\\n", j) // 转为json前j结构体的内容 = {a:1 b:2 C:3 D:4} jsonInfo, _ := json.Marshal(j) fmt.Printf("转为json后的内容 = %+v\\n", string(jsonInfo)) // 转为json后的内容 = {"C":"3","dd":"4"} } 结构体里定义了四个字段,分别对应 小写无tag,小写+tag,大写无tag,大写+tag。 转为json后首字母小写的不管加不加tag都不能转为json里的内容,而大写的加了tag可以取别名,不加tag则json内的字段跟结构体字段原名一致。 03.嵌套struct和JSON#3.1 结构化转Json字符串package main import ( \"encoding/json\" \"fmt\" ) type Student struct { //Student 学生 ID int Gender string Name string } type Class struct { //Class 班级 Title string Students []Student } func main() { c := &Class{ Title: \"001\", Students: make([]Student, 0, 200), } for i := 0; i < 10; i++ { stu := Student{ Name: fmt.Sprintf(\"stu%02d\", i), Gender: \"男\", ID: i, } c.Students = append(c.Students, stu) } //JSON 序列化:结构体-->JSON 格式的字符串 data, err := json.Marshal(c) if err != nil { fmt.Println(\"json marshal failed\") return } fmt.Printf(\"json:%s\\n\", data) } /* { \"Title\":\"001\", \"Students\":[ { \"ID\":0, \"Gender\":\"男\", \"Name\":\"stu00\" }, { \"ID\":1, \"Gender\":\"男\", \"Name\":\"stu01\" }, { \"ID\":2, \"Gender\":\"男\", \"Name\":\"stu02\" }, .... ] } */ 3.2 Json字符串转结构化package main import ( \"encoding/json\" \"fmt\" ) type Student struct { //Student 学生 ID int Gender string Name string } type Class struct { //Class 班级 Title string Students []Student } func main() { str := `{\"Title\":\"001\",\"Students\":[{\"ID\":0,\"Gender\":\"男\",\"Name\":\"stu00\"},{\"ID\":1,\"Gender\":\"男\",\"Name\":\"stu01\"},{\"ID\":2,\"Gender\":\"男\",\"Name\":\"stu02\"},{\"ID\":3,\"Gender\":\"男\",\"Name\":\"stu03\"}]}` c1 := &Class{} err := json.Unmarshal([]byte(str), c1) if err != nil { fmt.Println(\"json unmarshal failed!\") return } fmt.Printf(\"%#v\\n\", c1) // &main.Class{Title:\"001\", Students:[]main.Student{main.Student{ID:0, Gender:\"男\", Name:\"stu00\"}, main.Student{ID:1, Gender:\"男\", Name:\"stu01\"}, main.Student{ID:2, Gender:\"男\", Name:\"stu02\"}, main.Student{ID:3, Gender:\"男\", Name:\"stu03\"}}} }","categories":[{"name":"Go常用库","slug":"Go常用库","permalink":"http://coderedeng.github.io/categories/Go%E5%B8%B8%E7%94%A8%E5%BA%93/"}],"tags":[{"name":"Go常用库","slug":"Go常用库","permalink":"http://coderedeng.github.io/tags/Go%E5%B8%B8%E7%94%A8%E5%BA%93/"}]},{"title":"Time","slug":"Go常用库介绍 - Time","date":"2022-05-22T13:22:12.000Z","updated":"2024-01-22T12:58:59.000Z","comments":true,"path":"2022/05/22/Go常用库介绍 - Time/","link":"","permalink":"http://coderedeng.github.io/2022/05/22/Go%E5%B8%B8%E7%94%A8%E5%BA%93%E4%BB%8B%E7%BB%8D%20-%20Time/","excerpt":"","text":"02.Time01.时间类型 我们可以通过 time.Now()函数获取当前的时间对象,然后获取时间对象的年月日时分秒等信息。 注意:**%02d** 中的 2 表示宽度,如果整数不够 2 列就补上 0 package main import ( \"fmt\" \"time\" ) func main() { now := time.Now() //获取当前时间 fmt.Printf(\"current time:%v\\n\", now) year := now.Year() //年 month := now.Month() //月 day := now.Day() //日 hour := now.Hour() //小时 minute := now.Minute() //分钟 second := now.Second() //秒 // 打印结果为:2021-05-19 09:20:06 fmt.Printf(\"%d-%02d-%02d %02d:%02d:%02d\\n\", year, month, day, hour, minute, second) } 02.时间戳package main import ( \"fmt\" \"time\" ) func main() { now := time.Now() //获取当前时间 timestamp1 := now.Unix() //时间戳 timestamp2 := now.UnixNano() //纳秒时间戳 fmt.Printf(\"current timestamp1:%v\\n\", timestamp1) // current timestamp1:1623560753 fmt.Printf(\"current timestamp2:%v\\n\", timestamp2) // current timestamp2:1623560753965606600 } 使用time.Unix()函数可以将时间戳转为时间格式 func timestampDemo2(timestamp int64) { timeObj := time.Unix(timestamp, 0) //将时间戳转为时间格式 fmt.Println(timeObj) year := timeObj.Year() //年 month := timeObj.Month() //月 day := timeObj.Day() //日 hour := timeObj.Hour() //小时 minute := timeObj.Minute() //分钟 second := timeObj.Second() //秒 fmt.Printf(\"%d-%02d-%02d %02d:%02d:%02d\\n\", year, month, day, hour, minute, second) } 03.时间间隔 time.Duration是time包定义的一个类型,它代表两个时间点之间经过的时间,以纳秒为单位。 time.Duration表示一段时间间隔,可表示的最长时间段大约290年。 time包中定义的时间间隔类型的常量如下: const ( Nanosecond Duration = 1 Microsecond = 1000 * Nanosecond Millisecond = 1000 * Microsecond Second = 1000 * Millisecond Minute = 60 * Second Hour = 60 * Minute ) 04.时间格式化 时间类型有一个自带的方法Format进行格式化 需要注意的是Go语言中格式化时间模板不是常见的Y-m-d H:M:S 而是使用Go的诞生时间2006年1月2号15点04分(记忆口诀为2006 1 2 3 4)。 补充:如果想格式化为12小时方式,需指定PM。 package main import ( \"fmt\" \"time\" ) func main() { now := time.Now() // 格式化的模板为Go的出生时间2006年1月2号15点04分 Mon Jan // 24小时制 fmt.Println(now.Format(\"2006-01-02 15:04:05.000 Mon Jan\")) // 2021-06-13 13:10:18.143 Sun Jun // 12小时制 fmt.Println(now.Format(\"2006-01-02 03:04:05.000 PM Mon Jan\")) // 2021-06-13 01:10:18.143 PM Sun Jun fmt.Println(now.Format(\"2006/01/02 15:04\")) // 2021/06/13 13:10 fmt.Println(now.Format(\"15:04 2006/01/02\")) // 13:10 2021/06/13 fmt.Println(now.Format(\"2006/01/02\")) // 2021/06/13 } 解析字符串格式的时间 package main import ( \"fmt\" \"time\" ) func main() { now := time.Now() fmt.Println(now) // 2021-06-13 13:11:29.0679475 +0800 CST m=+0.001573301 // 加载时区 loc, err := time.LoadLocation(\"Asia/Shanghai\") if err != nil { fmt.Println(err) return } // 按照指定时区和指定格式解析字符串时间 timeObj, err := time.ParseInLocation(\"2006/01/02 15:04:05\", \"2019/08/04 14:15:20\", loc) if err != nil { fmt.Println(err) return } fmt.Println(timeObj) // 2019-08-04 14:15:20 +0800 CST fmt.Println(timeObj.Sub(now)) // -16294h56m9.0679475s } 05.时间操作函数 Add 我们在日常的编码过程中可能会遇到要求时间+时间间隔的需求 Go 语言的时间对象有提供Add 方法如下 Sub 求两个时间之间的差值 package main import ( \"fmt\" \"time\" ) func main() { now := time.Now() // 获取当前时间 // 10分钟前 m, _ := time.ParseDuration(\"-1m\") m1 := now.Add(m) fmt.Println(m1) // 8个小时前 h, _ := time.ParseDuration(\"-1h\") h1 := now.Add(8 * h) fmt.Println(h1) // 一天前 d, _ := time.ParseDuration(\"-24h\") d1 := now.Add(d) fmt.Println(d1) // 10分钟后 mm, _ := time.ParseDuration(\"1m\") mm1 := now.Add(mm) fmt.Println(mm1) // 8小时后 hh, _ := time.ParseDuration(\"1h\") hh1 := now.Add(hh) fmt.Println(hh1) // 一天后 dd, _ := time.ParseDuration(\"24h\") dd1 := now.Add(dd) fmt.Println(dd1) // Sub 计算两个时间差 subM := now.Sub(m1) fmt.Println(subM.Minutes(), \"分钟\") // 1 分钟 sumH := now.Sub(h1) fmt.Println(sumH.Hours(), \"小时\") // 8 小时 sumD := now.Sub(d1) fmt.Printf(\"%v 天\\n\", sumD.Hours()/24) // 1 天 } Equal 判断两个时间是否相同,会考虑时区的影响,因此不同时区标准的时间也可以正确比较。 本方法和用t==u不同,这种方法还会比较地点和时区信息。 Before 如果t代表的时间点在u之前,返回真;否则返回假。 After 如果t代表的时间点在u之后,返回真;否则返回假。 06.定时器 使用time.Tick(时间间隔)来设置定时器,定时器的本质上是一个通道(channel)。 func tickDemo() { ticker := time.Tick(time.Second) //定义一个1秒间隔的定时器 for i := range ticker { fmt.Println(i)//每秒都会执行的任务 } }","categories":[{"name":"Go常用库","slug":"Go常用库","permalink":"http://coderedeng.github.io/categories/Go%E5%B8%B8%E7%94%A8%E5%BA%93/"}],"tags":[{"name":"Go常用库","slug":"Go常用库","permalink":"http://coderedeng.github.io/tags/Go%E5%B8%B8%E7%94%A8%E5%BA%93/"}]},{"title":"fmt","slug":"Go常用库介绍 - fmt","date":"2022-05-20T12:31:14.000Z","updated":"2024-01-22T12:58:59.000Z","comments":true,"path":"2022/05/20/Go常用库介绍 - fmt/","link":"","permalink":"http://coderedeng.github.io/2022/05/20/Go%E5%B8%B8%E7%94%A8%E5%BA%93%E4%BB%8B%E7%BB%8D%20-%20fmt/","excerpt":"","text":"01.fmt01.常用占位符 动词 功能 %v 按值的本来值输出 %+v 在 %v 的基础上,对结构体字段名和值进行展开 %#v 输出 Go 语言语法格式的值 %T 输出 Go 语言语法格式的类型和值 %% 输出 %% 本体 %b 整型以二进制方式显示 %o 整型以八进制方式显示 %d 整型以十进制方式显示 %x 整型以 十六进制显示 %X 整型以十六进制、字母大写方式显示 %U Unicode 字符 %f 浮点数 %p 指针,十六进制方式显示 02. Print Println: 一次输入多个值的时候 Println 中间有空格 Println 会自动换行,Print 不会 Print: 一次输入多个值的时候 Print 没有 中间有空格 Print 不会自动换行 Printf Printf 是格式化输出,在很多场景下比 Println 更方便 package main import \"fmt\" func main() { fmt.Print(\"zhangsan\", \"lisi\", \"wangwu\") // zhangsanlisiwangwu fmt.Println(\"zhangsan\", \"lisi\", \"wangwu\") // zhangsan lisi wangwu name := \"zhangsan\" age := 20 fmt.Printf(\"%s 今年 %d 岁\\n\", name, age) // zhangsan 今年 20 岁 fmt.Printf(\"值:%v --> 类型: %T\", name, name) // 值:zhangsan --> 类型: string } 03.Sprint Sprint系列函数会把传入的数据生成并返回一个字符串。 package main import \"fmt\" func main() { s1 := fmt.Sprint(\"枯藤\") fmt.Println(s1) // 枯藤 name := \"枯藤\" age := 18 s2 := fmt.Sprintf(\"name:%s,age:%d\", name, age) // name:枯藤,age:18 fmt.Println(s2) s3 := fmt.Sprintln(\"枯藤\") // 枯藤 有空格 fmt.Println(s3) } 04. Fprint Fprint系列函数会将内容输出到一个io.Writer接口类型的变量w中 我们通常用这个函数往文件中写入内容。 package main import ( \"fmt\" \"os\" ) func main() { // 方法1:输出到控制台 fmt.Fprintln(os.Stdout, \"向标准输出写入内容\") // 方法2:将文件写入到 xx.txt 文件中 fileObj, err := os.OpenFile(\"./xx.txt\", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) if err != nil { fmt.Println(\"打开文件出错,err:\", err) return } name := \"枯藤\" // 向打开的文件句柄中写入内容 fmt.Fprintf(fileObj, \"往文件中写如信息:%s\", name) }","categories":[{"name":"Go常用库","slug":"Go常用库","permalink":"http://coderedeng.github.io/categories/Go%E5%B8%B8%E7%94%A8%E5%BA%93/"}],"tags":[{"name":"Go常用库","slug":"Go常用库","permalink":"http://coderedeng.github.io/tags/Go%E5%B8%B8%E7%94%A8%E5%BA%93/"}]},{"title":"Go设计模式 - 抽象工厂方法模式","slug":"Go设计模式 - 抽象工厂方法模式","date":"2022-05-17T14:15:10.000Z","updated":"2024-01-22T12:58:59.000Z","comments":true,"path":"2022/05/17/Go设计模式 - 抽象工厂方法模式/","link":"","permalink":"http://coderedeng.github.io/2022/05/17/Go%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%20-%20%E6%8A%BD%E8%B1%A1%E5%B7%A5%E5%8E%82%E6%96%B9%E6%B3%95%E6%A8%A1%E5%BC%8F/","excerpt":"","text":"6. 抽象工厂方法模式6.1 抽象工厂方法模式中的角色和职责抽象工厂(Abstract Factory)角色:它声明了一组用于创建一族产品的方法,每一个方法对应一种产品。 具体工厂(Concrete Factory)角色:它实现了在抽象工厂中声明的创建产品的方法,生成一组具体产品,这些产品构成了一个产品族,每一个产品都位于某个产品等级结构中。 抽象产品(Abstract Product)角色:它为每种产品声明接口,在抽象产品中声明了产品所具有的业务方法。 具体产品(Concrete Product)角色:它定义具体工厂生产的具体产品对象,实现抽象产品接口中声明的业务方法。 6.2 抽象工厂方法模式的实现抽象工厂方法模式的实现代码如下: package main import \"fmt\" /* 练习: 设计一个电脑主板架构,电脑包括(显卡,内存,CPU)3个固定的插口, 显卡具有显示功能(display,功能实现只要打印出意义即可),内存 具有存储功能(storage),cpu具有计算功能(calculate)。 现有Intel厂商,nvidia厂商,Kingston厂商,均会生产以上三种硬件。 要求组装两台电脑, 1台(Intel的CPU,Intel的显卡,Intel的内存) 1台(Intel的CPU, nvidia的显卡,Kingston的内存) 用抽象工厂模式实现。 */ // ======= 抽象层 ========= type AbstractCPU interface { Calculate() } type AbstractGraphics interface { Display() } type AbstractMemory interface { Storage() } // 抽象工厂 type AbstractFactoryFn interface { CreateCPU() AbstractCPU CreateGraphics() AbstractGraphics CreateMemory() AbstractMemory } // ======= 实现层 ========= /* Inter 产品族 */ type InterCPU struct{} func (ic *InterCPU) Calculate() { fmt.Println(\"Inter's CPU calculation\") } type InterGraphics struct{} func (ig *InterGraphics) Display() { fmt.Println(\"Inter's graphics display\") } type InterMemory struct{} func (im *InterMemory) Storage() { fmt.Println(\"Inter's memory storage\") } type InterFactory struct { AbstractFactoryFn } func (f *InterFactory) CreateCPU() AbstractCPU { var cpu *InterCPU cpu = new(InterCPU) return cpu } func (f *InterFactory) CreateGraphics() AbstractGraphics { var graphics *InterGraphics graphics = new(InterGraphics) return graphics } func (f *InterFactory) CreateMemory() AbstractMemory { var memory *InterMemory memory = new(InterMemory) return memory } /* Nvidia 产品族 */ type NvidiaCPU struct{} func (nc *NvidiaCPU) Calculate() { fmt.Println(\"Nvidia's CPU calculation\") } type NvidiaGraphics struct{} func (ng *NvidiaGraphics) Display() { fmt.Println(\"Nvidia's graphics display\") } type NvidiaMemory struct{} func (nm *NvidiaMemory) Storage() { fmt.Println(\"Nvidia's memory storage\") } type NvidiaFactory struct { AbstractFactoryFn } func (f *NvidiaFactory) CreateCPU() AbstractCPU { var cpu *NvidiaCPU cpu = new(NvidiaCPU) return cpu } func (f *NvidiaFactory) CreateGraphics() AbstractGraphics { var graphics *NvidiaGraphics graphics = new(NvidiaGraphics) return graphics } func (f *NvidiaFactory) CreateMemory() AbstractMemory { var memory *NvidiaMemory memory = new(NvidiaMemory) return memory } /* Kingston 产品族 */ type KingstonCPU struct{} func (ic *KingstonCPU) Calculate() { fmt.Println(\"Kingston's CPU calculation\") } type KingstonGraphics struct{} func (ig *KingstonGraphics) Display() { fmt.Println(\"Kingston's graphics display\") } type KingstonMemory struct{} func (im *KingstonMemory) Storage() { fmt.Println(\"Kingston's memory storage\") } type KingstonFactory struct { AbstractFactoryFn } func (f *KingstonFactory) CreateCPU() AbstractCPU { var cpu *KingstonCPU cpu = new(KingstonCPU) return cpu } func (f *KingstonFactory) CreateGraphics() AbstractGraphics { var graphics *KingstonGraphics graphics = new(KingstonGraphics) return graphics } func (f *KingstonFactory) CreateMemory() AbstractMemory { var memory *KingstonMemory memory = new(KingstonMemory) return memory } // ======= 业务逻辑层 ========= func main() { // 要求组装两台电脑, // 1台(Intel的CPU,Intel的显卡,Intel的内存) fmt.Println(\"第1台:(Intel的CPU,Intel的显卡,Intel的内存)\") iFac := new(InterFactory) iCpu := iFac.CreateCPU() iCpu.Calculate() iGraphics := iFac.CreateGraphics() iGraphics.Display() iMemory := iFac.CreateMemory() iMemory.Storage() // 1台(Intel的CPU, nvidia的显卡,Kingston的内存) fmt.Println(\"第2台:(Intel的CPU,nvidia的显卡,Kingston的内存)\") inFac := new(InterFactory) inCpu := inFac.CreateCPU() inCpu.Calculate() nGraphics := new(NvidiaFactory).CreateGraphics() nGraphics.Display() kMemory := new(KingstonFactory).CreateMemory() kMemory.Storage() } 以上是一个使用抽象工厂模式实现的电脑主板架构代码。这个系统可以组装不同厂商生产的CPU、显卡和内存来创建电脑。 该代码中定义了抽象层的接口,包括AbstractCPU、AbstractGraphics和AbstractMemory,以及抽象工厂接口AbstractFactory。然后通过实现这些接口来创建具体产品族的硬件和工厂。 在实现层中,有三个产品族的具体实现:Inter、Nvidia和Kingston。每个产品族都有对应的CPU、显卡和内存,并实现了抽象层的接口。 在业务逻辑层的main函数中,创建了两台电脑并进行组装。第一台电脑使用Intel的CPU、显卡和内存,第二台电脑使用Intel的CPU、Nvidia的显卡和Kingston的内存。组装过程通过抽象工厂来创建对应的硬件,并调用其相应的功能。 运行该代码将输出每个硬件的功能实现结果,例如CPU的计算、显卡的显示和内存的存储。 请注意,这只是一个示例代码,具体的电脑主板架构根据实际需求可能会有所不同。 6.3 抽象工厂方法模式的优缺点优点: 拥有工厂方法模式的优点 当一个产品族中的多个对象被设计成一起工作时,它能够保证客户端始终只使用同一个产品族中的对象。 增加新的产品族很方便,无须修改已有系统,符合“开闭原则”。 对于新产品的创建,符合开闭原则。 缺点: 增加新的产品等级结构麻烦,需要对原有系统进行较大的修改,甚至需要修改抽象层代码,这显然会带来较大的不便,违背了“开闭原则”。 适用场景: 系统中有多于一个的产品族。而每次只使用其中某一产品族。可以通过配置文件等方式来使得用户可以动态改变产品族,也可以很方便地增加新的产品族。 产品等级结构稳定。设计完成之后,不会向系统中增加新的产品等级结构或者删除已有的产品等级结构。","categories":[{"name":"Go设计模式","slug":"Go设计模式","permalink":"http://coderedeng.github.io/categories/Go%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/"}],"tags":[{"name":"Go设计模式","slug":"Go设计模式","permalink":"http://coderedeng.github.io/tags/Go%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/"}]},{"title":"Go设计模式 - 工厂方法模式","slug":"Go设计模式 - 工厂方法模式","date":"2022-05-15T13:28:35.000Z","updated":"2024-01-22T12:58:59.000Z","comments":true,"path":"2022/05/15/Go设计模式 - 工厂方法模式/","link":"","permalink":"http://coderedeng.github.io/2022/05/15/Go%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%20-%20%E5%B7%A5%E5%8E%82%E6%96%B9%E6%B3%95%E6%A8%A1%E5%BC%8F/","excerpt":"","text":"5. 工厂方法模式5.1 工厂方法模式中的角色和职责抽象工厂(Abstract Factory)角色:工厂方法模式的核心,任何工厂类都必须实现这个接口。 工厂(Concrete Factory)角色:具体工厂类是抽象工厂的一个实现,负责实例化产品对象。 抽象产品(Abstract Product)角色:工厂方法模式所创建的所有对象的父类,它负责描述所有实例所共有的公共接口。 具体产品(Concrete Product)角色:工厂方法模式所创建的具体实例对象。 5.2 工厂方法模式的实现工厂方法模式的实现代码如下: package main import \"fmt\" // TODO 工厂模式 // ----抽象层---- // 文具类 type AbstractStationery interface { Show() } // 工厂类 type AbstractFactory interface { CreateStationery() AbstractStationery // 生产文具的(抽象)类方法 } // ----实现层---- type ActualPencil struct { AbstractStationery // 实际的铅笔继承抽象的文具 } func (stationery *ActualPencil) Show() { fmt.Println(\"生产铅笔\") } type ActualPen struct { AbstractStationery // 实际的钢笔继承抽象的文具 } func (stationery *ActualPen) Show() { fmt.Println(\"生产钢笔\") } // ----工厂模块---- // 铅笔工厂 type PencilFactory struct { AbstractFactory // 实际的铅笔工厂继承抽象的工厂 } func (pencil *PencilFactory) CreateStationery() AbstractStationery { return &ActualPencil{} // 返回实际的铅笔 } // 钢笔工厂 type PenFactory struct { AbstractFactory // 实际的钢笔工厂继承抽象的工厂 } func (pen *PenFactory) CreateStationery() AbstractStationery { return &ActualPen{} // 返回实际的钢笔 } func main() { // 生产铅笔 pencilFactory := new(PencilFactory) pencil := pencilFactory.CreateStationery() pencil.Show() // 生产钢笔 penFactory := new(PenFactory) pen := penFactory.CreateStationery() pen.Show() } 上述代码是通过面向抽象层开发,业务逻辑层的main()函数逻辑,依然是只与工厂耦合,且只与抽象的工厂和抽象的水果类耦合,这样就遵循了面向抽象层接口编程的原则。 5.3 工厂方法模式的优缺点优点: 不需要记住具体类名,甚至连具体参数都不用记忆。 实现了对象创建和使用的分离。 系统的可扩展性也就变得非常好,无需修改接口和原类。 对于新产品的创建,符合开闭原则。 缺点: 增加系统中类的个数,复杂度和理解度增加。 增加了系统的抽象性和理解难度。 适用场景: 客户端不知道它所需要的对象的类。 抽象工厂类通过其子类来指定创建哪个对象。","categories":[{"name":"Go设计模式","slug":"Go设计模式","permalink":"http://coderedeng.github.io/categories/Go%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/"}],"tags":[{"name":"Go设计模式","slug":"Go设计模式","permalink":"http://coderedeng.github.io/tags/Go%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/"}]},{"title":"Go设计模式 - 简单工厂模式","slug":"Go设计模式 - 简单工厂模式","date":"2022-05-10T13:22:57.000Z","updated":"2024-01-22T12:58:59.000Z","comments":true,"path":"2022/05/10/Go设计模式 - 简单工厂模式/","link":"","permalink":"http://coderedeng.github.io/2022/05/10/Go%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%20-%20%E7%AE%80%E5%8D%95%E5%B7%A5%E5%8E%82%E6%A8%A1%E5%BC%8F/","excerpt":"","text":"4. 简单工厂模式4.1 简单工厂模式角色和职责简单工厂模式并不属于GoF的23种设计模式。他是开发者自发认为的一种非常简易的设计模式,其角色和职责如下: 工厂(Factory)角色:简单工厂模式的核心,它负责实现创建所有实例的内部逻辑。工厂类可以被外界直接调用,创建所需的产品对象。 抽象产品(AbstractProduct)角色:简单工厂模式所创建的所有对象的父类,它负责描述所有实例所共有的公共接口。 具体产品(Concrete Product)角色:简单工厂模式所创建的具体实例对象。 4.2 简单工厂模式实现简单工厂方法模式的实现代码如下: package main import \"fmt\" // TODO 简单工厂模式 // ----抽象层---- type Stationery interface { Show() } // ----实现层---- type Pencil struct { Stationery } func (stationery *Pencil) Show() { fmt.Println(\"生产铅笔\") } type Pen struct { Stationery } func (stationery *Pen) Show() { fmt.Println(\"生产钢笔\") } // ----工厂模块---- type Factory struct { } func (fac *Factory) CreateStationery(kind string) Stationery { var sationery Stationery if kind == \"pencil\" { sationery = new(Pencil) } else if kind == \"pen\" { sationery = new(Pen) } return sationery } func main() { factory := Factory{} // 生产铅笔 pencil := factory.CreateStationery(\"pencil\") pencil.Show() // 生产钢笔 pen := factory.CreateStationery(\"pen\") pen.Show() } 上述代码可以看出,业务逻辑层只会和工厂模块进行依赖,这样业务逻辑层将不再关心Stationery类是具体怎么创建基础对象的。 4.3 简单工厂方法模式的优缺点优点: 实现了对象创建和使用的分离。 2. 不需要记住具体类名,记住参数即可,减少使用者记忆量。 缺点: 对工厂类职责过重,一旦不能工作,系统受到影响。 2. 增加系统中类的个数,复杂度和理解度增加。 3. 违反“开闭原则”,添加新产品需要修改工厂逻辑,工厂越来越复杂。 适用场景: 工厂类负责创建的对象比较少,由于创建的对象较少,不会造成工厂方法中的业务逻辑太过复杂。 2. 客户端只知道传入工厂类的参数,对于如何创建对象并不关心。","categories":[{"name":"Go设计模式","slug":"Go设计模式","permalink":"http://coderedeng.github.io/categories/Go%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/"}],"tags":[{"name":"Go设计模式","slug":"Go设计模式","permalink":"http://coderedeng.github.io/tags/Go%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/"}]},{"title":"Go设计模式 - 创建型模式","slug":"Go设计模式 - 创建型模式","date":"2022-05-05T12:55:14.000Z","updated":"2024-01-22T12:58:59.000Z","comments":true,"path":"2022/05/05/Go设计模式 - 创建型模式/","link":"","permalink":"http://coderedeng.github.io/2022/05/05/Go%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%20-%20%E5%88%9B%E5%BB%BA%E5%9E%8B%E6%A8%A1%E5%BC%8F/","excerpt":"","text":"3. 创建型模式 模式名称 模式名称 作用 创建型模式 Creational Pattern(6) 单例模式★★★★☆ 是保证一个类仅有一个实例,并提供一个访问它的全局访问点。 简单工厂模式★★★☆☆ 通过专门定义一个类来负责创建其他类的实例,被创建的实例通常都具有共同的父类。 工厂方法模式★★★★★ 定义一个创建产品对象的工厂接口,将实际创建工作推迟到子类中。 抽象工厂模式★★★★★ 提供一个创建一系列相关或者相互依赖的接口,而无需指定它们具体的类。 原型模式★★★☆☆ 用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。 建造者模式★★☆☆☆ 将一个复杂的构建与其表示相分离,使得同样的构建过程可以创建不同的表示。 目前标准的创建型设计模式共有6种(注:设计模式种类并非仅仅局限于此,设计模式实则是一种编程思想,开发者可以根据自身经验来总结出很多种设计模式思想,这6中创建型设计模式为早期官方认可的标准模式) 本章节主要介绍常用的“单例模式”、“简单工程模式”、“工厂方法模式”、“抽象工厂模式”等。“原型模式”、“建造者模式”思想类似,作为读者选修篇幅,本章暂时先不介绍。","categories":[{"name":"Go设计模式","slug":"Go设计模式","permalink":"http://coderedeng.github.io/categories/Go%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/"}],"tags":[{"name":"Go设计模式","slug":"Go设计模式","permalink":"http://coderedeng.github.io/tags/Go%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/"}]},{"title":"Go设计模式 - 面向对象设计原则","slug":"Go设计模式 - 面向对象设计原则","date":"2022-05-03T13:15:11.000Z","updated":"2024-01-22T12:58:59.000Z","comments":true,"path":"2022/05/03/Go设计模式 - 面向对象设计原则/","link":"","permalink":"http://coderedeng.github.io/2022/05/03/Go%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%20-%20%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E8%AE%BE%E8%AE%A1%E5%8E%9F%E5%88%99/","excerpt":"","text":"2. 面向对象设计原则对于面向对象软件系统的设计而言,在支持可维护性的同时,提高系统的可复用性是一个至关重要的问题,如何同时提高一个软件系统的可维护性和可复用性是面向对象设计需要解决的核心问题之一。在面向对象设计中,可维护性的复用是以设计原则为基础的。每一个原则都蕴含一些面向对象设计的思想,可以从不同的角度提升一个软件结构的设计水平。 面向对象设计原则为支持可维护性复用而诞生,这些原则蕴含在很多设计模式中,它们是从许多设计方案中总结出的指导性原则。面向对象设计原则也是我们用于评价一个设计模式的使用效果的重要指标之一。 原则的目的: 高内聚,低耦合 2.1 面向对象设计原则表 名称 定义 单一职责原则(Single Responsibility Principle, SRP)★★★★☆ 类的职责单一,对外只提供一种功能,而引起类变化的原因都应该只有一个。 开闭原则(Open-Closed Principle, OCP)★★★★★ 类的改动是通过增加代码进行的,而不是修改源代码。 里氏代换原则(Liskov Substitution Principle, LSP)★★★★★ 任何抽象类(interface接口)出现的地方都可以用他的实现类进行替换,实际就是虚拟机制,语言级别实现面向对象功能。 依赖倒转原则(Dependence Inversion Principle, DIP)★★★★★ 依赖于抽象(接口),不要依赖具体的实现(类),也就是针对接口编程。 接口隔离原则(Interface Segregation Principle, ISP)★★☆☆☆ 不应该强迫用户的程序依赖他们不需要的接口方法。一个接口应该只提供一种对外功能,不应该把所有操作都封装到一个接口中去。 合成复用原则(Composite Reuse Principle,CRP)★★★★☆ 如果使用继承,会导致父类的任何变换都可能影响到子类的行为。如果使用对象组合,就降低了这种依赖关系。对于继承和组合,优先使用组合。 迪米特法则(Law of Demeter,LoD)★★★☆☆ 一个对象应当对其他对象尽可能少的了解,从而降低各个对象之间的耦合,提高系统的可维护性。例如在一个程序中,各个模块之间相互调用时,通常会提供一个统一的接口来实现。这样其他模块不需要了解另外一个模块的内部实现细节,这样当一个模块内部的实现发生改变时,不会影响其他模块的使用。(黑盒原理) 2.2 单一职责原则类的职责单一,对外只提供一种功能,而引起类变化的原因都应该只有一个。 package main import \"fmt\" type ClothesShop struct {} func (cs *ClothesShop) OnShop() { fmt.Println(\"休闲的装扮\") } type ClothesWork struct {} func (cw *ClothesWork) OnWork() { fmt.Println(\"工作的装扮\") } func main() { //工作的时候 cw := new(ClothesWork) cw.OnWork() //shopping的时候 cs := new(ClothesShop) cs.OnShop() } 在面向对象编程的过程中,设计一个类,建议对外提供的功能单一,接口单一,影响一个类的范围就只限定在这一个接口上,一个类的一个接口具备这个类的功能含义,职责单一不复杂。 2.3 开闭原则类的改动是通过增加代码实现的,而不是修改源码。 package main import \"fmt\" // TODO 开闭原则 // 类的改动是通过增加代码实现的,而不是修改源码 // AbstractBanker 创建一个抽象业务员接口 type AbstractBanker interface { DoBusi() } // SaveBanker 通过存款业务实例化业务员接口 type SaveBanker struct { AbstractBanker } // DoBusi 方法实例化了抽象接口 func (sb *SaveBanker) DoBusi() { fmt.Println(\"进行存款业务\") } type TranferBanker struct { AbstractBanker } func (tb *TranferBanker) DoBusi() { fmt.Println(\"进行了转账业务\") } type SharesBanker struct { AbstractBanker } func (sb *SharesBanker) DoBusi() { fmt.Println(\"进行了股票业务\") } // BankBusiness 实现一个架构层(基于抽象层进行业务封装-针对interface接口进行封装) func BankBusiness(banker AbstractBanker) { // 通过接口向下调用(多态现象) banker.DoBusi() } func main() { BankBusiness(&SaveBanker{}) BankBusiness(&TranferBanker{}) BankBusiness(&SharesBanker{}) } 开闭原则:一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。简单的说就是在修改需求的时候,应该尽量通过扩展来实现变化,而不是通过修改已有代码来实现变化。 2.4 依赖倒转原则面向抽象层依赖倒转 如上图所示,如果我们在设计一个系统的时候,将模块分为3个层次,抽象层、实现层、业务逻辑层。那么,我们首先将抽象层的模块和接口定义出来,这里就需要了interface接口的设计,然后我们依照抽象层,依次实现每个实现层的模块,在我们写实现层代码的时候,实际上我们只需要参考对应的抽象层实现就好了,实现每个模块,也和其他的实现的模块没有关系,这样也符合了上面介绍的开闭原则。这样实现起来每个模块只依赖对象的接口,而和其他模块没关系,依赖关系单一。系统容易扩展和维护。 我们在指定业务逻辑也是一样,只需要参考抽象层的接口来业务就好了,抽象层暴露出来的接口就是我们业务层可以使用的方法,然后可以通过多态的线下,接口指针指向哪个实现模块,调用了就是具体的实现方法,这样我们业务逻辑层也是依赖抽象成编程。 我们就将这种的设计原则叫做依赖倒转原则。 package main import \"fmt\" // TODO 依赖倒转原则 // 依赖于抽象(接口),不要依赖具体的实现(类),针对接口编程 // <---- 抽象层 ----> type Car interface { Run() } type Drive interface { Drive(car Car) } // <---- 实现层 ----> type Banz struct{} func (b *Banz) Run() { fmt.Println(\"Banz is running\") } type BMW struct{} func (b *BMW) Run() { fmt.Println(\"BMW is running\") } type Zhangsan struct{} func (Zs *Zhangsan) Drive(car Car) { fmt.Println(\"zhangsan drive car\") car.Run() } type Lisi struct{} func (Ls *Lisi) Drive(car Car) { fmt.Println(\"lisi drive car\") car.Run() } // <---- 业务逻辑层 ----> func main() { var banz Car banz = new(Banz) var bm Car bm = new(BMW) // 张三开奔驰 var zs Drive zs = new(Zhangsan) zs.Drive(banz) //李四开宝马 var ls Drive ls = new(Lisi) ls.Drive(bm) } 2.5 合成复用原则如果使用继承,会导致父类的任何变换都可能影响到子类的行为。如果使用对象组合,就降低了这种依赖关系。对于继承和组合,优先使用组合。 package main import \"fmt\" type Cat struct {} func (c *Cat) Eat() { fmt.Println(\"小猫吃饭\") } //给小猫添加一个 可以睡觉的方法 (使用继承来实现) type CatB struct { Cat } func (cb *CatB) Sleep() { fmt.Println(\"小猫睡觉\") } //给小猫添加一个 可以睡觉的方法 (使用组合的方式) type CatC struct { C *Cat } func (cc *CatC) Sleep() { fmt.Println(\"小猫睡觉\") } func main() { //通过继承增加的功能,可以正常使用 cb := new(CatB) cb.Eat() cb.Sleep() //通过组合增加的功能,可以正常使用 cc := new(CatC) cc.C = new(Cat) cc.C.Eat() cc.Sleep() }","categories":[{"name":"Go设计模式","slug":"Go设计模式","permalink":"http://coderedeng.github.io/categories/Go%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/"}],"tags":[{"name":"Go设计模式","slug":"Go设计模式","permalink":"http://coderedeng.github.io/tags/Go%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/"}]},{"title":"Go设计模式 - 概述","slug":"Go设计模式 - 概述","date":"2022-05-01T13:50:21.000Z","updated":"2024-01-22T12:58:59.000Z","comments":true,"path":"2022/05/01/Go设计模式 - 概述/","link":"","permalink":"http://coderedeng.github.io/2022/05/01/Go%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%20-%20%E6%A6%82%E8%BF%B0/","excerpt":"","text":"1. Go设计模式概述如果把修习软件开发当做武功修炼的话,那么可以分为招式和内功。 招式: ●Java、C#、C++、Golang、Rust等编程语言; ● Eclipse、Visual Studio、Goland、Vim等开发工具; ● Struts、Hibernate、JBPM、Gin、Istio、gRPC等框架技术; 内功: ●数据结构 ●算法 ●设计模式 ●架构设计 ●软件工程 注意:招式可以很快学会,但是内功的修炼需要更长的时间 1.1 设计模式从何而来 上图是“模式之父”,Christopher Alexander(克里斯托弗.亚历山大)———哈佛大学建筑学博士、美国加州大学伯克利分校建筑学教授、加州大学伯克利分校环境结构研究所所长、美国艺术和科学院院士。 克里斯托弗.亚历山大在作品《建筑的永恒之道》中对“模式”的描述是: “每个模式都描述了一个在我们的环境中不断出现的问题,然后描述了该问题的解决方案的核心,通过这种方式,我们可以无数次地重用那些已有的成功的解决方案,无须再重复相同的工作。” 他给出了设计模式的定义。 我们也可以用下面这句话来理解“设计模式”的定义: “设计模式是在特定环境下人们解决某类重复出现问题的一套成功或有效的解决方案。” 1.2 软件设计模式又从何而来 左到右依次是:Ralph Johnson,Richard Helm,Erich Gamma,John Vlissides。 GoF将模式的概念引入软件工程领域,这标志着软件模式的诞生。软件模式(Software Patterns)是将模式的一般概念应用于软件开发领域,即软件开发的总体指导思路或参照样板。软件模式并非仅限于设计模式,还包括架构模式、分析模式和过程模式等,实际上,在软件开发生命周期的每一个阶段都存在着一些被认同的模式。 软件模式与具体的应用领域无关,也就是说无论你从事的是移动应用开发、桌面应用开发、Web应用开发还是嵌入式软件的开发,都可以使用软件模式。无论你是使用Java、C#、Objective-C、VB.net、Smalltalk等纯面向对象编程语言,还是使用C++、PHP、Delphi、JavaScript等可支持面向对象编程的语言,你都需要了解软件设计模式! GoF给软件设计模式提供了定义,如下: “软件设计模式(Design Pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结,使用设计模式是为了可重用代码、让代码更容易被他人理解并且保证代码可靠性。” 一句大白话可以总结:“在一定环境下,用固定套路解决问题。” 1.3 软件设计模式的种类GoF提出的设计模式有23个,包括: (1)创建型(Creational)模式:如何创建对象; (2)结构型(Structural )模式:如何实现类或对象的组合; (3)行为型(Behavioral)模式:类或对象怎样交互以及怎样分配职责。 有一个“简单工厂模式”不属于GoF 23种设计模式,但大部分的设计模式书籍都会对它进行专门的介绍。 设计模式目前种类: GoF的23种 + “简单工厂模式” = 24种。 1.4 软件设计模式的作用那么对于初学者来说,学习设计模式将有助于更加深入地理解面向对象思想, 让你知道: 如何将代码分散在几个不同的类中? 为什么要有“接口”? 何谓针对抽象编程? 何时不应该使用继承? 如果不修改源代码增加新功能? 更好地阅读和理解现有类库与其他系统中的源代码。 学习设计模式会让你早点脱离面向对象编程的“菜鸟期”。 1.5 如何学好设计模式设计模式的基础是:多态。 初学者:积累案例,不要盲目的背类图。 初级开发人员:多思考,多梳理,归纳总结,尊重事物的认知规律,注意临界点的突破,不要浮躁。 中级开发人员:合适的开发环境,寻找合适的设计模式来解决问题。 多应用,对经典则组合设计模式的大量,自由的运用。要不断的追求。 1.6 设计模式总览表 模式名称 模式名称 作用 创建型模式 Creational Pattern(6) 单例模式★★★★☆ 是保证一个类仅有一个实例,并提供一个访问它的全局访问点。 简单工厂模式★★★☆☆ 通过专门定义一个类来负责创建其他类的实例,被创建的实例通常都具有共同的父类。 工厂方法模式★★★★★ 定义一个创建产品对象的工厂接口,将实际创建工作推迟到子类中。 抽象工厂模式★★★★★ 提供一个创建一系列相关或者相互依赖的接口,而无需指定它们具体的类。 原型模式★★★☆☆ 用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。 建造者模式★★☆☆☆ 将一个复杂的构建与其表示相分离,使得同样的构建过程可以创建不同的表示。 结构型模式 Structural Pattern(7) 适配器模式★★★★☆ 将一个类的接口转换成客户希望的另外一个接口。使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。 桥接模式★★★☆☆ 将抽象部分与实际部分分离,使它们都可以独立的变化。 组合模式★★☆☆☆ 将对象组合成树形结构以表示“部分–整体”的层次结构。使得用户对单个对象和组合对象的使用具有一致性。 装饰模式★★★☆☆ 动态的给一个对象添加一些额外的职责。就增加功能来说,此模式比生成子类更为灵活。 外观模式★★★★★ 为子系统中的一组接口提供一个一致的界面,此模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。 享元模式★☆☆☆☆ 以共享的方式高效的支持大量的细粒度的对象。 代理模式★★★★☆ 为其他对象提供一种代理以控制对这个对象的访问。 行为型模式 Behavioral Pattern(11) 职责链模式★★☆☆☆ 在该模式里,很多对象由每一个对象对其下家的引用而连接起来形成一条链。请求在这个链上传递,直到链上的某一个对象决定处理此请求,这使得系统可以在不影响客户端的情况下动态地重新组织链和分配责任。 命令模式★★★★☆ 将一个请求封装为一个对象,从而使你可用不同的请求对客户端进行参数化;对请求排队或记录请求日志,以及支持可撤销的操作。 解释器模式★☆☆☆☆ 如何为简单的语言定义一个语法,如何在该语言中表示一个句子,以及如何解释这些句子。 迭代器模式★☆☆☆☆ 提供了一种方法顺序来访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。 中介者模式★★☆☆☆ 定义一个中介对象来封装系列对象之间的交互。终结者使各个对象不需要显示的相互调用 ,从而使其耦合性松散,而且可以独立的改变他们之间的交互。 备忘录模式★★☆☆☆ 是在不破坏封装的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。 观察者模式★★★★★ 定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。 状态模式★★☆☆☆ 对象的行为,依赖于它所处的状态。 策略模式★★★★☆ 准备一组算法,并将每一个算法封装起来,使得它们可以互换。 模板方法模式★★★☆☆ 得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。 访问者模式★☆☆☆☆ 表示一个作用于某对象结构中的各元素的操作,它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。","categories":[{"name":"Go设计模式","slug":"Go设计模式","permalink":"http://coderedeng.github.io/categories/Go%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/"}],"tags":[{"name":"Go设计模式","slug":"Go设计模式","permalink":"http://coderedeng.github.io/tags/Go%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/"}]},{"title":"常见坑1~10","slug":"Go进阶 - 常见坑1~10","date":"2022-04-21T14:20:11.000Z","updated":"2024-01-22T12:58:59.000Z","comments":true,"path":"2022/04/21/Go进阶 - 常见坑1~10/","link":"","permalink":"http://coderedeng.github.io/2022/04/21/Go%E8%BF%9B%E9%98%B6%20-%20%E5%B8%B8%E8%A7%81%E5%9D%911~10/","excerpt":"","text":"1.常见坑1~1001.nil slice & empty slice1.1 nil切片与空切片底层 nil切片:var nilSlice []string nil slice 的长度len和容量cap都是0 nil slice == nil nil slice的pointer 是nil, 空切片:emptySlice0 := make([]int, 0) empty slice的长度是0, 容量是由指向底层数组决定 empty slice != nil empty slice的pointer是底层数组的地址 nil切片和空切片最大的区别在于指向的数组引用地址是不一样的。 nil空切片引用数组指针地址为0(无指向任何实际地址) 空切片的引用数组指针地址是有的,且固定为一个值,所有的空切片指向的数组引用地址都是一样的 1.2 创建nil slice和empty slicepackage main import \"fmt\" func main() { var nilSlice []string // 创建一个 nil 切片 emptySlice0 := make([]int, 0) // 方法1:创建一个空切片(零切片) var emptySlice1 = []string{} // 方法2:创建一个空切片 fmt.Printf(\"\\nnilSlice---> Nil:%v Len:%d Capacity:%d\", nilSlice == nil, len(nilSlice), cap(nilSlice)) fmt.Printf(\"\\nemptySlice0---> nil:%v Len:%d Capacity:%d\", emptySlice0 == nil, len(emptySlice0), cap(emptySlice0)) fmt.Printf(\"\\nemptySlice1---> nil:%v Len:%d Capacity:%d\", emptySlice1 == nil, len(emptySlice1), cap(emptySlice1)) // nil切片和空切片都可以正常 append数据 nilSlice = append(nilSlice, \"sss\") } /* Nil:true Len:0 Capacity:0 nil:false Len:0 Capacity:0 nil:false Len:0 Capacity:0[sss] */ 02.类型强转产生内存拷贝1.1 字符串转数组发送内存拷贝 字符串转成byte数组,会发生内存拷贝吗? 字符串转成切片,会产生拷贝。 严格来说,只要是发生类型强转都会发生内存拷贝。 那么问题来了,频繁的内存拷贝操作听起来对性能不大友好。 有没有什么办法可以在字符串转成切片的时候不用发生拷贝呢? 1.2 字符串转数组不内存拷贝方法 那么如果想要在底层转换二者,只需要把 StringHeader 的地址强转成 SliceHeader 就行。那么go有个很强的包叫 unsafe 。 unsafe.Pointer(&a) 方法可以得到变量 a 的地址。 2.(*reflect.StringHeader)(unsafe.Pointer(&a)) 可以把字符串a转成底层结构的形式。 3.(*[]byte)(unsafe.Pointer(&ssh)) 可以把ssh底层结构体转成byte的切片的指针。 4.再通过 *转为指针指向的实际内容。 package main import ( \"fmt\" \"reflect\" \"unsafe\" ) func main() { a :=\"aaa\" ssh := *(*reflect.StringHeader)(unsafe.Pointer(&a)) b := *(*[]byte)(unsafe.Pointer(&ssh)) fmt.Printf(\"%v---%T\",b,b) // [97 97 97]---[]uint8 } 1.3 解释 StringHeader 是字符串在go的底层结构。 type StringHeader struct { Data uintptr Len int } SliceHeader 是切片在go的底层结构。 type SliceHeader struct { Data uintptr Len int Cap int } 那么如果想要在底层转换二者,只需要把 StringHeader 的地址强转成 SliceHeader 就行。 那么go有个很强的包叫 unsafe 。 unsafe.Pointer(&a) 方法可以得到变量 a 的地址。 2.(*reflect.StringHeader)(unsafe.Pointer(&a)) 可以把字符串a转成底层结构的形式。 3.(*[]byte)(unsafe.Pointer(&ssh)) 可以把ssh底层结构体转成byte的切片的指针。 4.再通过 *转为指针指向的实际内容。 03.拷贝大切片一定代价大吗拷贝大切片一定比小切片代价大吗? SliceHeader 是 切片 在go的底层结构。 - 第一个字是指向切片`底层数组的指针`,这是切片的存储空间 - 第二个字段是`切片的长度` - 第三个字段是`容量` ```go type SliceHeader struct { Data uintptr Len int Cap int } 大切片跟小切片的区别无非就是 Len 和 Cap的值比小切片的这两个值大一些,如果发生拷贝,本质上就是拷贝上面的三个字段。 所以 拷贝大切片跟小切片的代价应该是一样的。 04.map不初始化使用会怎么样 空map和nil map结果是一样的,都为map[]。 所以,这个时候别断定map是空还是nil,而应该通过map == nil来判断。 package main func main() { var m1 map[string]string // 创建一个 nil map println(\"m1为nil: \", m1==nil) // 报错 => panic: assignment to entry in nil map //m1[\"name\"] = \"tom\" var m2 = make(map[string]string) // 创建一个空map m2[\"name\"] = \"jack\" // 空map可以正常 println(\"m2为nil: \", m2==nil) } 05.map遍历删除安全吗 map 并不是一个线程安全的数据结构。 同时读写一个 map 是未定义的行为,如果被检测到,会直接 panic。 上面说的是发生在多个协程同时读写同一个 map 的情况下。 如果在同一个协程内边遍历边删除,并不会检测到同时读写,理论上是可以这样做的。 sync.Map可以解决多线程读写map问题 一般而言,这可以通过读写锁来解决:sync.RWMutex。 读之前调用 RLock() 函数,读完之后调用 RUnlock() 函数解锁; 写之前调用 Lock() 函数,写完之后,调用 Unlock() 解锁。 另外,sync.Map 是线程安全的 map,也可以使用。 06.for循环append坑6.1 坑1:添加元素变覆盖 不会死循环,for range其实是golang的语法糖,在循环开始前会获取切片的长度 len(切片),然后再执行len(切片)次数的循环。 package main import \"fmt\" func main() { s := []int{1,2,3,4,5} for _, v:=range s { s =append(s, v) fmt.Printf(\"len(s)=%v\\n\",len(s)) } } /* len(s)=6 len(s)=7 len(s)=8 len(s)=9 len(s)=10 */ 6.2 坑2:值全部一样 每次循转中num的值是正常的,但是由append构造的res中,全是nums的最后一个值。 最终总结出原因是在for range语句中,创建了变量num且只被创建了一次。 即num有自己的空间内存且地址在for循环过程中不变 循环过程中每次将nums中对应的值和num进行值传递。 package main import \"fmt\" func main() { var nums = []int{1, 2, 3, 4, 5} var res []*int for _, num := range nums { res = append(res, &num) //fmt.Println(\"num:\", num) } for _, r := range res { fmt.Println(\"res:\", *r) } } /* res: 5 res: 5 res: 5 res: 5 res: 5 */ 6.3 解决方法 方法1 - 不使用for range的形式,直接用索引来对nums取值 ```go package main import "fmt" func main() { var nums = []int{1, 2, 3, 4, 5} var res []*int for i := 0; i < len(nums); i++ { res = append(res, &nums[i]) } fmt.Println("res:", res) for _, r := range res { fmt.Println("res:", *r) } } 方法2 - 在for循环中每次再定义一个新的变量num_temp,将num的值传给num_temp,之后append该变量即可。 ```text package main import "fmt" func main() { var nums = []int{1, 2, 3, 4, 5} var res []*int for _, num := range nums { numTemp := num // 创建一个新的临时变量 res = append(res, &numTemp) } for _, r := range res { fmt.Println("res:", *r) } } /* res: 1 res: 2 res: 3 res: 4 res: 5 */","categories":[{"name":"Go进阶","slug":"Go进阶","permalink":"http://coderedeng.github.io/categories/Go%E8%BF%9B%E9%98%B6/"}],"tags":[{"name":"Go进阶","slug":"Go进阶","permalink":"http://coderedeng.github.io/tags/Go%E8%BF%9B%E9%98%B6/"}]},{"title":"Golang常犯错误","slug":"Go进阶 - Golang常犯错误","date":"2022-04-20T14:35:42.000Z","updated":"2024-01-22T12:58:59.000Z","comments":true,"path":"2022/04/20/Go进阶 - Golang常犯错误/","link":"","permalink":"http://coderedeng.github.io/2022/04/20/Go%E8%BF%9B%E9%98%B6%20-%20Golang%E5%B8%B8%E7%8A%AF%E9%94%99%E8%AF%AF/","excerpt":"","text":"1.Golang常犯错误01.01~1001.nil的slice和map 允许对值为 nil 的 slice 添加元素,但对值为 nil 的 map 添加元素,则会造成运行时 panic。 // map 错误示例 func main() { var m map[string]int m[\"one\"] = 1 // error: panic: assignment to entry in nil map // m := make(map[string]int)// map 的正确声明,分配了实际的内存 } // slice 正确示例 func main() { var s []int s = append(s, 1) } 02.判断map中key是否存在 当访问 map 中不存在的 key 时,Go 则会返回元素对应数据类型的零值,比如 nil、’’ 、false 和 0 取值操作总有值返回,故不能通过取出来的值,来判断 key 是不是在 map 中。 检查 key 是否存在可以用 map 直接访问,检查返回的第二个参数即可。 // 错误的 key 检测方式 func main() { x := map[string]string{\"one\": \"2\", \"two\": \"\", \"three\": \"3\"} if v := x[\"two\"]; v == \"\" { fmt.Println(\"key two is no entry\") // 键 two 存不存在都会返回的空字符串 } } // 正确示例 func main() { x := map[string]string{\"one\": \"2\", \"two\": \"\", \"three\": \"3\"} if _, ok := x[\"two\"]; !ok { fmt.Println(\"key two is no entry\") } } 03.string值修改 不能,尝试使用索引遍历字符串,来更新字符串中的个别字符,是不允许的。 string 类型的值是只读的二进制 byte slice,如果真要修改字符串中的字符 将 string 转为 []byte 修改后,再转为 string 即可。 // 修改字符串的错误示例 func main() { x := \"text\" x[0] = \"T\" // error: cannot assign to x[0] fmt.Println(x) } // 修改示例 func main() { x := \"text\" xBytes := []byte(x) xBytes[0] = 'T' // 注意此时的 T 是 rune 类型 x = string(xBytes) fmt.Println(x) // Text } 04.解析JSON数字转成float64 在 encode/decode JSON 数据时,Go 默认会将数值当做 float64 处理。 package main import ( \"encoding/json\" \"fmt\" \"log\" ) func main() { var data = []byte(`{\"status\": 200}`) var result map[string]interface{} if err := json.Unmarshal(data, &result); err != nil { log.Fatalln(err) } fmt.Printf(\"%v--%T\",result[\"status\"],result[\"status\"]) // 200--float64 } 解析出来的 200 是 float 类型。 05.如何从 panic 中恢复 在一个 defer 延迟执行的函数中调用 recover ,它便能捕捉/中断 panic。 // 错误的 recover 调用示例 func main() { recover() // 什么都不会捕捉 panic(\"not good\") // 发生 panic,主程序退出 recover() // 不会被执行 println(\"ok\") } // 正确的 recover 调用示例 func main() { defer func() { fmt.Println(\"recovered: \", recover()) }() panic(\"not good\") } 06.避免Goroutine泄露 可以通过 context 包来避免内存泄漏。 func main() { ctx, cancel := context.WithCancel(context.Background()) ch := func(ctx context.Context) <-chan int { ch := make(chan int) go func() { for i := 0; ; i++ { select { case <- ctx.Done(): return case ch <- i: } } } () return ch }(ctx) for v := range ch { fmt.Println(v) if v == 5 { cancel() break } } } 下面的 for 循环停止取数据时,就用 cancel 函数,让另一个协程停止写数据。 如果下面 for 已停止读取数据,上面 for 循环还在写入,就会造成内存泄漏。 07.跳出for select 循环 通常在for循环中,使用break可以跳出循环 但是注意在go语言中,for select配合时,break 并不能跳出循环。 func testSelectFor2(chExit chan bool){ EXIT: for { select { case v, ok := <-chExit: if !ok { fmt.Println(\"close channel 2\", v) break EXIT//goto EXIT2 } fmt.Println(\"ch2 val =\", v) } } //EXIT2: fmt.Println(\"exit testSelectFor2\") } 08.嵌套结构初始化 go 的哲学是组合优于继承,使用 struct 嵌套即可完成组合,内嵌的结构体属性就像外层结构的属性即可,可以直接调用。 注意初始化外层结构体时,必须指定内嵌结构体名称的结构体初始化,如下看到 s1方式报错,s2 方式正确。 type stPeople struct { Gender bool Name string } type stStudent struct { stPeople Class int } //尝试4 嵌套结构的初始化表达式 //var s1 = stStudent{false, \"JimWen\", 3} var s2 = stStudent{stPeople{false, \"JimWen\"}, 3} fmt.Println(s2.Gender, s2.Name, s2.Class) 09.defer触发顺序func main() { defer_call() } func defer_call() { defer func() { fmt.Println(\"打印前\") }() defer func() { fmt.Println(\"打印中\") }() defer func() { fmt.Println(\"打印后\") }() panic(\"触发异常\") } 看下答案,输出: 打印后 打印中 打印前 panic: 触发异常 参考解析:defer 的执行顺序是后进先出。当出现 panic 语句的时候,会先按照 defer 的后进先出的顺序执行,最后才会执行panic 10.for循环&val取值错误func main() { slice := []int{0,1,2,3} m := make(map[int]*int) for key,val := range slice { m[key] = &val } for k,v := range m { fmt.Println(k,\"->\",*v) } } 直接给答案: 0 -> 3 1 -> 3 2 -> 3 3 -> 3 参考解析: 这是新手常会犯的错误写法,for range 循环的时候会创建每个元素的副本,而不是元素的引用 所以 m[key] = &val 取的都是变量 val 的地址,所以最后 map 中的所有元素的值都是变量 val 的地址 因为最后 val 被赋值为3,所有输出都是3 正确的写法: func main() { slice := []int{0,1,2,3} m := make(map[int]*int) for key,val := range slice { value := val m[key] = &value } for k,v := range m { fmt.Println(k,\"===>\",*v) } } 11.切片append错误// 1. func main() { s := make([]int, 5) s = append(s, 1, 2, 3) fmt.Println(s) } // 2. func main() { s := make([]int,0) s = append(s,1,2,3,4) fmt.Println(s) } 两段代码分别输出: [0 0 0 0 0 1 2 3] [1 2 3 4] 参考解析:这道题考的是使用 append 向 slice 添加元素,第一段代码常见的错误是 [1 2 3],需要注意。 02.11~2012.new()返回指针func main() { list := new([]int) list = append(list, 1) fmt.Println(list) } 参考答案及解析: 不能通过编译,new([]int) 之后的 list 是一个 *[]int 类型的指针,不能对指针执行 append 操作。 可以使用 make() 初始化之后再用。 同样的,map 和 channel 建议使用 make() 或字面量的方式初始化,不要用 new() 。 13. :=赋值只能在函数内部使用package main // myvar := 1 // error var myvar = 1 // ok func main() { } :=赋值不会影响外层函数值 package main import \"fmt\" func main() { x := 1 fmt.Println(x) //prints 1 { fmt.Println(x) //prints 1 x := 2 // 不会影响到外部x变量的值 fmt.Println(x) //prints 2 //x = 5 // 要想修改x的值,必须使用这种语法赋值 } fmt.Println(x) //prints 1 } 14.数组用于函数传参时是值复制 注意:方法或函数调用时,传入参数都是值复制(跟赋值一致) 除非是map、slice、channel、指针类型 这些特殊类型是引用传递 package main import \"fmt\" func main() { x := [3]int{1,2,3} test01(x) fmt.Println(x) // [1 2 3] } // 数组在函数中传参是值复制 func test01(arr [3]int) { arr[0] = 7 fmt.Println(arr) //prints [7 2 3] } 15.defer打印顺序package main import ( \"fmt\" ) func main() { defer_call() } func defer_call() { defer func() { fmt.Println(\"打印前\") }() defer func() { fmt.Println(\"打印中\") }() defer func() { fmt.Println(\"打印后\") }() panic(\"触发异常\") } /* 打印后 打印中 打印前 panic: 触发异常 */ 当出现 panic 语句的时候,会先按照 defer 的后进先出的顺序执行,最后才会执行panic","categories":[{"name":"Go进阶","slug":"Go进阶","permalink":"http://coderedeng.github.io/categories/Go%E8%BF%9B%E9%98%B6/"}],"tags":[{"name":"Go进阶","slug":"Go进阶","permalink":"http://coderedeng.github.io/tags/Go%E8%BF%9B%E9%98%B6/"}]},{"title":"mutex锁原理","slug":"Go进阶 - mutex锁原理","date":"2021-02-23T14:14:30.000Z","updated":"2024-01-22T12:58:59.000Z","comments":true,"path":"2021/02/23/Go进阶 - mutex锁原理/","link":"","permalink":"http://coderedeng.github.io/2021/02/23/Go%E8%BF%9B%E9%98%B6%20-%20mutex%E9%94%81%E5%8E%9F%E7%90%86/","excerpt":"","text":"1.mutex锁原理01.Mutex1.1 mutex结构体 源码包src/sync/mutex.go:Mutex定义了互斥锁的数据结构 type Mutex struct { state int32 // 表示互斥锁的状态,比如是否被锁定等 sema uint32 // 表示信号量,协程阻塞等待该信号量,解锁的协程释放信号量从而唤醒等待信号量的协程 } 我们看到Mutex.state是32位的整型变量,内部实现时把该变量分成四份,用于记录Mutex的四种状态。 下图展示Mutex的内存布局 Locked: 表示该Mutex是否已被锁定,0:没有锁定 1:已被锁定。 Woken: 表示是否有协程已被唤醒,0:没有协程唤醒 1:已有协程唤醒,正在加锁过程中。 Starving:表示该Mutex是否处理饥饿状态, 0:没有饥饿 1:饥饿状态,说明有协程阻塞了超过1ms。 Waiter: 表示阻塞等待锁的协程个数,协程解锁时根据此值来判断是否需要释放信号量。 1.2 简单加锁 假定当前只有一个协程在加锁,没有其他协程干扰,那么过程如下图所示 加锁过程会去判断Locked标志位是否为0,如果是0则把Locked位置1,代表加锁成功 从上图可见,加锁成功后,只是Locked位置1,其他状态位没发生变化 1.3 加锁被阻塞 假定加锁时,锁已被其他协程占用了,此时加锁过程如下图所示 当协程B对一个已被占用的锁再次加锁时,Waiter计数器增加了1 此时协程B将被阻塞,直到Locked值变为0后才会被唤醒。 1.4 解锁并唤醒协程 假定解锁时,有1个或多个协程阻塞,此时解锁过程如下图所示: 协程A解锁过程分为两个步骤,一是把Locked位置0,二是查看到Waiter>0 所以释放一个信号量,唤醒一个阻塞的协程,被唤醒的协程B把Locked位置1,于是协程B获得锁。 1.5 自旋过程 加锁时,如果当前Locked位为1,说明该锁当前由其他协程持有,尝试加锁的协程并不是马上转入阻塞 而是会持续的探测Locked位是否变为0,这个过程即为自旋过程。 自旋时间很短,但如果在自旋过程中发现锁已被释放,那么协程可以立即获取锁。此时即便有协程被唤醒也无法获取锁,只能再次阻塞。 自旋的好处是,当加锁失败时不必立即转入阻塞,有一定机会获取到锁,这样可以避免协程的切换。 自旋条件 自旋次数要足够小,通常为4,即自旋最多4次 CPU核数要大于1,否则自旋没有意义,因为此时不可能有其他协程释放锁 协程调度机制中的Process数量要大于1,比如使用GOMAXPROCS()将处理器设置为1就不能启用自旋 协程调度机制中的可运行队列必须为空,否则会延迟协程调度 02.原子操作与锁2.1 什么是原子操作 一个或者多个操作在 CPU 执行的过程中不被中断的特性,称为原子性(atomicity) 。 这些操作对外表现成一个不可分割的整体,他们要么都执行,要么都不执行,外界不会看到他们只执行到一半的状态。 atomic包中的原子操作则由底层硬件指令直接提供支持,这些指令在执行的过程中是不允许中断的 因此原子操作可以在lock-free的情况下保证并发安全,并且它的性能也能做到随CPU个数的增多而线性扩展。 2.2 互斥锁跟原子操作 使用目的:互斥锁是用来保护一段逻辑,原子操作用于对一个变量的更新保护。 底层实现: Mutex由操作系统的调度器实现,而atomic包中的原子操作则由**底层硬件指令**直接提供支持,这些指令在执行的过程中是不允许中断的 因此原子操作可以在lock-free的情况下保证并发安全,并且它的性能也能做到随CPU个数的增多而线性扩展。 2.3 Mutex实现机制 CAS (Compare And Swap) 的做法类似操作数据库时常见的乐观锁机制 该操作在进行交换前首先确保被操作数的值未被更改,满足此前提条件下才进行交换操作。 其实Mutex的底层实现也是依赖原子操作中的CAS实现的,原子操作的atomic包相当于是sync包里的那些同步原语的实现依赖。 比如互斥锁Mutex的结构里有一个state字段,其是表示锁状态的状态位。 type Mutex struct { state int32 sema uint32 }","categories":[{"name":"Go进阶","slug":"Go进阶","permalink":"http://coderedeng.github.io/categories/Go%E8%BF%9B%E9%98%B6/"}],"tags":[{"name":"Go进阶","slug":"Go进阶","permalink":"http://coderedeng.github.io/tags/Go%E8%BF%9B%E9%98%B6/"}]},{"title":"sync.Map","slug":"Go进阶 - sync_map","date":"2021-02-22T13:54:21.000Z","updated":"2024-01-22T12:58:59.000Z","comments":true,"path":"2021/02/22/Go进阶 - sync_map/","link":"","permalink":"http://coderedeng.github.io/2021/02/22/Go%E8%BF%9B%E9%98%B6%20-%20sync_map/","excerpt":"","text":"1.sync.map01.sync.Map介绍1.1 sync.Map介绍 简单说:空间换时间+读写分离+原子操作(快路径) sync.Map 的主要思想就是读写分离,空间换时间 。 sync.Map底层使用了两个原生map,一个叫read,仅用于读; 一个叫dirty,用于在特定情况下存储最新写入的key-value数据 1.2 sync.Map特点 1、空间换时间:通过冗余的两个Map数据结构(read、dirty),实现加锁对性能的影响。 2、使用只读数据(read),避免读写冲突。 3、动态调整,miss次数多了之后,将dirty数据迁移到read中。 4、double-checking。 5、迟删除。 删除一个键值只是打标记,只有在迁移dirty数据的时候才清理删除的数据。 6、优先从read读取、更新、删除,因为对read的读取不需要锁。 1.3 sync.Map结构体type Map struct { // 当涉及到脏数据(dirty)操作时候,需要使用这个锁 mu Mutex // 后面是readOnly结构体,依靠map实现,仅仅只用来读 read atomic.Value // readOnly // 这个map主要用来写的,部分时候也承担读的能力 dirty map[interface{}]*entry // 记录自从上次更新了read之后,从read读取key失败的次数 misses int } readOnly // readOnly is an immutable struct stored atomically in the Map.read field. type readOnly struct { // m包含所有只读数据,不会进行任何的数据增加和删除操作 // 但是可以修改entry的指针因为这个不会导致map的元素移动 m map[interface{}]*entry // 标志位,如果为true则表明当前read只读map的数据不完整,dirty map中包含部分数据 amended bool // true if the dirty map contains some key not in m. } 02.sync.Map操作 2.1 新增数据 1、key原先就存在于read中,获取key所对应内存地址,原子性修改 2、key存在,但是key所对应的值被标记为 expunged,解锁,解除标记,并更新dirty中的key,与read中进行同步,然后修改key对应的值 3、read中没有key,但是dirty中存在这个key,直接修改dirty中key的值 4、read和dirty中都没有值,先判断自从read上次同步dirty的内容后有没有再修改过dirty的内容,没有的话,先同步read和dirty的值,然后添加新的key value到dirty上面 2.2 删除数据 1、read中没有,且Map存在修改,则尝试删除dirty中的map中的key 2、read中没有,且Map不存在修改,那就是没有这个key,无需操作 3、read中有,尝试将key对应的值设置为nil,后面读取的时候就知道被删了, 因为dirty中map的值跟read的map中的值指向的都是同一个地址空间,所以,修改了read也就是修改了dirty 2.3 遍历(Range) 遍历的逻辑就比较简单了,Map只有两种状态,被修改过和没有修改过 修改过:将dirty的指针交给read,read就是最新的数据了,然后遍历read的map 没有修改过:遍历read的map就好了","categories":[{"name":"Go进阶","slug":"Go进阶","permalink":"http://coderedeng.github.io/categories/Go%E8%BF%9B%E9%98%B6/"}],"tags":[{"name":"Go进阶","slug":"Go进阶","permalink":"http://coderedeng.github.io/tags/Go%E8%BF%9B%E9%98%B6/"}]},{"title":"sync.Pool","slug":"Go进阶 - sync_Pool ","date":"2021-02-21T13:19:10.000Z","updated":"2024-05-26T03:15:39.159Z","comments":true,"path":"2021/02/21/Go进阶 - sync_Pool /","link":"","permalink":"http://coderedeng.github.io/2021/02/21/Go%E8%BF%9B%E9%98%B6%20-%20sync_Pool%20/","excerpt":"","text":"1.sync.Pool01.sync.Pool介绍1.1 是什么 sync.Pool 是 sync 包下的一个组件,可以作为保存临时取还对象的一个“池子”。 个人觉得它的名字有一定的误导性,因为 Pool 里装的对象可以被无通知地被回收,可能 sync.Cache 是一个更合适的名字。 Pool 结构体的定义为: Pool 中有两个定义的公共方法,分别是 Put - 向池中添加元素; Get 从池中获取元素,如果没有,则调用 New 生成元素,如果 New 未设置,则返回 nil。 type Pool struct { noCopy noCopy local unsafe.Pointer // 本地P缓存池指针 localSize uintptr // 本地P缓存池大小 // 当池中没有可能对象时 // 会调用 New 函数构造构造一个对象 New func() interface{} } 1.2 有什么用 对于很多需要重复分配、回收内存的地方,sync.Pool 是一个很好的选择。 频繁地分配、回收内存会给 GC 带来一定的负担,严重的时候会引起 CPU 的毛刺 而 sync.Pool 可以将暂时不用的对象缓存起来,待下次需要的时候直接使用,不用再次经过内存分配 复用对象的内存,减轻 GC 的压力,提升系统的性能。 1.3 怎么用 首先,sync.Pool 是协程安全的,这对于使用者来说是极其方便的。 使用前,设置好对象的 New 函数,用于在 Pool 里没有缓存的对象时,创建一个。 之后,在程序的任何地方、任何时候仅通过 Get()、Put() 方法就可以取、还对象了。 package main import ( \"fmt\" \"sync\" ) var pool *sync.Pool type Person struct { Name string } func initPool() { pool = &sync.Pool { New: func() interface{} { fmt.Println(\"Creating a new Person\") return new(Person) }, } } func main() { initPool() p := pool.Get().(*Person) // get获取不到就会调用方法,创建一个 p.Name = \"first\" pool.Put(p) // 使用 Put方法将对象放回 pool池子中 fmt.Println(\"Pool 里已有一个对象:&{first},调用 Get: \", pool.Get().(*Person)) fmt.Println(\"Pool 没有对象了,调用 Get: \", pool.Get().(*Person)) // 获取后再次获取就没有了,会再次创建 } /* Creating a new Person Pool 里已有一个对象:&{first},调用 Get: &{first} Creating a new Person Pool 没有对象了,调用 Get: &{} */ 1.4 Get Pool 会为每个 P 维护一个本地池,P 的本地池分为 私有池 private 和共享池 shared。 私有池中的元素只能本地 P 使用,共享池中的元素可能会被其他 P 偷走 所以使用私有池 private 时不用加锁,而使用共享池 shared 时需加锁。 Get 会优先查找本地 private,再查找本地 shared,最后查找其他 P 的 shared 如果以上全部没有可用元素,最后会调用 New 函数获取新元素。 1.5 PUT Put 优先把元素放在 private 池中; 如果 private 不为空,则放在 shared 池中 有趣的是,在入池之前,该元素有 1/4 可能被丢掉。 02.gin中的Context pool 在 web 应用中,后台在处理用户的每条请求时都会为当前请求创建一个上下文环境 Context,用于存储请求信息及相应信息等。 Context 满足长生命周期的特点,且用户请求也是属于并发环境,所以对于线程安全的 Pool 非常适合用来维护 Context 的临时对象池。 Gin 在结构体 Engine 中定义了一个 pool: type Engine struct { // ... 省略了其他字段 pool sync.Pool } 初始化 engine 时定义了 pool 的 New 函数: engine.pool.New = func() interface{} { return engine.allocateContext() } // allocateContext func (engine *Engine) allocateContext() *Context { // 构造新的上下文对象 return &Context{engine: engine} } ServeHttp // 从 pool 中获取,并转化为 *Context c := engine.pool.Get().(*Context) c.writermem.reset(w) c.Request = req c.reset() // reset engine.handleHTTPRequest(c) // 再扔回 pool 中 engine.pool.Put(c)","categories":[{"name":"Go进阶","slug":"Go进阶","permalink":"http://coderedeng.github.io/categories/Go%E8%BF%9B%E9%98%B6/"}],"tags":[{"name":"Go进阶","slug":"Go进阶","permalink":"http://coderedeng.github.io/tags/Go%E8%BF%9B%E9%98%B6/"}]},{"title":"协程调度GMP模型","slug":"Go进阶 - 协程调度GMP模型","date":"2021-02-20T14:15:12.000Z","updated":"2024-01-22T12:58:59.000Z","comments":true,"path":"2021/02/20/Go进阶 - 协程调度GMP模型/","link":"","permalink":"http://coderedeng.github.io/2021/02/20/Go%E8%BF%9B%E9%98%B6%20-%20%E5%8D%8F%E7%A8%8B%E8%B0%83%E5%BA%A6GMP%E6%A8%A1%E5%9E%8B/","excerpt":"","text":"1.协程调度GMP模型01.线程调度1.1 早期单线程操作系统 一切的软件都是跑在操作系统上,真正用来干活(计算)的是CPU。 早期的操作系统每个程序就是一个进程,知道一个程序运行完,才能进行下一个进程,就是“单进程时代” 一切的程序只能串行发生。 1.2 多进程/线程时代 在多进程/多线程的操作系统中,就解决了阻塞的问题,因为一个进程阻塞cpu可以立刻切换到其他进程中去执行 而且调度cpu的算法可以保证在运行的进程都可以被分配到cpu的运行时间片 这样从宏观来看,似乎多个进程是在同时被运行。 但新的问题就又出现了,进程拥有太多的资源,进程的创建、切换、销毁,都会占用很长的时间 CPU虽然利用起来了,但如果进程过多,CPU有很大的一部分都被用来进行进程调度了 大量的进程/线程出现了新的问题 高内存占用 调度的高消耗CPU 进程虚拟内存会占用4GB[32位操作系统], 而线程也要大约4MB 1.3 Go协程goroutine Go中,协程被称为goroutine,它非常轻量,一个goroutine只占几KB,并且这几KB就足够goroutine运行完 这就能在有限的内存空间内支持大量goroutine,支持了更多的并发 虽然一个goroutine的栈只占几KB,但实际是可伸缩的,如果需要更多内容,runtime会自动为goroutine分配。 Goroutine特点: 占用内存更小(几kb) 调度更灵活(runtime调度) 1.4 协程与线程区别 协程跟线程是有区别的,线程由CPU调度是抢占式的 协程由用户态调度是协作式的,一个协程让出CPU后,才执行下一个协程 02.调度器GMP模型 G:goroutine(协程) M:thread(内核线程,不是用户态线程) P:processer(调度器) 2.1 GM模型 G(协程),通常在代码里用 go 关键字执行一个方法,那么就等于起了一个G。 M(内核线程),操作系统内核其实看不见G和P,只知道自己在执行一个线程。 G和P都是在用户层上的实现。 并发量小的时候还好,当并发量大了,这把大锁,就成为了性能瓶颈。 GPM由来 基于没有什么是加一个中间层不能解决的思路,golang在原有的GM模型的基础上加入了一个调度器P 可以简单理解为是在G和M中间加了个中间层 于是就有了现在的GMP模型里的P 2.2 GMP模型 03.GPM流程分析 我们通过 go func()来创建一个goroutine; 3.1 P本地队列获取G M想要运行G,就得先获取P,然后从P的本地队列获取G 3.2 本地队列中G移动到全局队列 新建 G 时,新G会优先加入到 P 的本地队列; 如果本地队列满了,则会把本地队列中一半的 G 移动到全局队列 3.3 从其他P本地队列的G放到自己P队列 如果全局队列为空时,M 会从其他 P 的本地队列偷(stealing)一半G放到自己 P 的本地队列。 3.4 M从P获取下一个G,不断重复 M 运行 G,G 执行之后,M 会从 P 获取下一个 G,不断重复下去 04.goroutine调度器 参考(opens new window) 4.1 普通线程与goroutine1、普通线程缺点 1)创建和切换太重 操作系统线程的创建和切换都需要进入内核,而进入内核所消耗的性能代价比较高,开销较大; 2)内存使用太重 一方面,为了尽量避免极端情况下操作系统线程栈的溢出, 内核在创建操作系统线程时默认会为其分配一个较大的栈内存 虚拟地址空间,内核并不会一开始就分配这么多的物理内存 然而在绝大多数情况下,系统线程远远用不了这么多内存,这导致了浪费; 另一方面,栈内存空间一旦创建和初始化完成之后其大小就不能再有变化,这决定了在某些特殊场景下系统线程栈还是有溢出的风险。 2、goroutine为什么轻量 goroutine是用户态线程,其创建和切换都在用户代码中完成而无需进入操作系统内核,所以其开销要远远小于系统线程的创建和切换; goroutine启动时默认栈大小只有2k,这在多数情况下已经够用了,即使不够用,goroutine的栈也会自动扩大 同时,如果栈太大了过于浪费它还能自动收缩,这样既没有栈溢出的风险,也不会造成栈内存空间的大量浪费。 4.2 线程模型与调度器1、调度器理论 goroutine建立在操作系统线程基础之上,它与操作系统线程之间实现了一个多对多(M:N)的两级线程模型 这里的 M:N 是指M个goroutine运行在N个操作系统线程之上 内核负责对这N个操作系统线程进行调度,而这N个系统线程又负责对这M个goroutine进行调度和运行 如何调度 所谓的对goroutine的调度,是指程序代码按照一定的算法在适当的时候挑选出合适的goroutine并放到CPU上去运行的过程 这些负责对goroutine进行调度的程序代码我们称之为goroutine调度器 2、调度器伪代码理解 所谓的对goroutine的调度,是指程序代码按照一定的算法在适当的时候挑选出合适的goroutine并放到CPU上去运行的过程 这些负责对goroutine进行调度的程序代码我们称之为goroutine调度器 // 程序启动时的初始化代码 ...... for i := 0; i < N; i++ { // 创建N个操作系统线程执行schedule函数 create_os_thread(schedule) // 创建一个操作系统线程执行schedule函数 } //schedule函数实现调度逻辑 func schedule() { for { //调度循环 // 根据某种算法从M个goroutine中找出一个需要运行的goroutine g := find_a_runnable_goroutine_from_M_goroutines() run_g(g) // CPU运行该goroutine,直到需要调度其它goroutine才返回 save_status_of_g(g) // 保存goroutine的状态,主要是寄存器的值 } } 程序运行起来之后创建了N个由内核调度的操作系统线程(工作线程)去执行shedule函数 而schedule函数在一个调度循环中反复从M个goroutine中挑选出一个需要运行的goroutine并跳转到该goroutine去运行 直到需要调度其它goroutine时才返回到schedule函数中 通过save_status_of_g保存刚刚正在运行的goroutine的状态然后再次去寻找下一个goroutine 4.3 调度器数据结构操作系统线程及其调度 在执行操作系统代码时,内核调度器按照一定的算法挑选出一个线程 并把该线程保存在内存之中的寄存器的值放入CPU对应的寄存器从而恢复该线程的运行 1、g结构体存放goroutine状态信息:g的结构体 系统线程对goroutine的调度与内核对系统线程的调度原理是一样的 实质都是通过保存和修改CPU寄存器的值来达到切换线程/goroutine的目的 为了实现对goroutine的调度,需要引入一个数据结构来保存CPU寄存器的值以及goroutine的其它一些状态信息 在Go语言调度器源代码中,这个数据结构是一个名叫g的结构体,它保存了goroutine的所有信息 该结构体的每一个实例对象都代表了一个goroutine,调度器代码可以通过g对象来对goroutine进行调度 当goroutine被调离CPU时,调度器代码负责把CPU寄存器的值保存在g对象的成员变量之中 当goroutine被调度起来运行时,调度器代码又负责把g对象的成员变量所保存的寄存器的值恢复到CPU的寄存器 2、全局队列goroutine全局队列:schedt结构体 要实现对goroutine的调度,仅仅有g结构体对象是不够的,至少还需要一个存放所有(可运行)goroutine的容器 一方面用来保存调度器自身的状态信息,另一方面它还拥有一个用来保存goroutine的运行队列 在每个Go程序中schedt结构体只有一个实例对象,该实例对象在源代码中被定义成了一个共享的全局变量 这样每个工作线程都可以访问它以及它所拥有的goroutine运行队列,我们称这个运行队列为全局运行队列 3、局部队列goroutine局部队列:p结构体 因为全局运行队列是每个工作线程都可以读写的,因此访问它需要加锁,加锁会导致严重的性能问题。 于是,调度器又为每个工作线程引入了一个私有的局部goroutine运行队列 工作线程优先使用自己的局部运行队列,只有必要时才会去访问全局运行队列,这大大减少了锁冲突,提高了工作线程的并发性 在Go调度器源代码中,局部运行队列被包含在p结构体的实例对象之中 每一个运行着go代码的工作线程都会与一个p结构体的实例对象关联在一起 4、m结构体属于工作线程的m结构体 Go调度器源代码中还有一个用来代表工作线程的m结构体 每个工作线程都有唯一的一个m结构体的实例对象与之对应,m结构体对象除了记录着 1)工作线程的诸如栈的起止位置 2)当前正在执行的goroutine以及是否空闲等等状态信息之外 3)还通过指针维持着与p结构体的实例对象之间的绑定关系 于是,通过m既可以找到与之对应的工作线程正在运行的goroutine,又可以找到工作线程的局部运行队列等资源 5、全局私有变量全局私有变量 工作线程与工作线程结构体对应关系 工作线程执行的代码是如何找到属于自己的那个m结构体实例对象的呢? 每个工作线程在刚刚被创建出来进入调度循环之前就利用线程本地存储机制为该工作线程实现了一个指向m结构体实例对象的私有全局变量 这样在之后的代码中就使用该全局变量来访问自己的m结构体对象以及与m相关联的p和g对象 4.4 重要的结构体1、g结构体 g结构体用于代表一个goroutine,该结构体保存了goroutine的所有信息 包括栈,gobuf结构体和其它的一些状态信息 // 前文所说的g结构体,它代表了一个goroutine type g struct { stack stack // 记录该goroutine使用的栈 m *m // 此goroutine正在被哪个工作线程执行 sched gobuf // 保存调度信息,主要是几个寄存器的值 // schedlink字段指向全局运行队列中的下一个g,所有位于全局运行队列中的g形成一个链表 schedlink guintptr preempt bool // 抢占调度标志,如果需要抢占调度,设置preempt为true } 2、m结构体 m结构体用来代表工作线程,它保存了m自身使用的栈信息 当前正在运行的goroutine以及与m绑定的p等信息 type m struct { // g0主要用来记录工作线程使用的栈信息,在执行调度代码时需要使用这个栈 // 执行用户goroutine代码时,使用用户goroutine自己的栈,调度时会发生栈的切换 g0 *g // 通过TLS实现m结构体对象与工作线程之间的绑定 tls [6]uintptr // thread-local storage (for x86 extern register) curg *g // 指向工作线程正在运行的goroutine的g结构体对象 p puintptr // 记录与当前工作线程绑定的p结构体对象 nextp puintptr oldp puintptr // the p that was attached before executing a syscall // spinning状态:表示当前工作线程正在试图从其它工作线程的本地运行队列偷取goroutine spinning bool // m is out of work and is actively looking for work blocked bool // m is blocked on a note // 没有goroutine需要运行时,工作线程睡眠在这个park成员上, // 其它线程通过这个park唤醒该工作线程 park note // 记录所有工作线程的一个链表 alllink *m // on allm schedlink muintptr // Linux平台thread的值就是操作系统线程ID thread uintptr // thread handle freelink *m // on sched.freem } 3、p结构体 p结构体用于保存工作线程执行go代码时所必需的资源,比如goroutine的运行队列,内存分配用到的缓存等等 type p struct { lock mutex status uint32 // one of pidle/prunning/... link puintptr schedtick uint32 // incremented on every scheduler call syscalltick uint32 // incremented on every system call sysmontick sysmontick // last tick observed by sysmon m muintptr // back-link to associated m (nil if idle) //本地goroutine运行队列 runqhead uint32 // 队列头 runqtail uint32 // 队列尾 runq [256]guintptr //使用数组实现的循环队列 runnext guintptr gFree struct { gList n int32 } } 4、schedt结构体 schedt结构体用来保存调度器的状态信息和goroutine的全局运行队列: type schedt struct { // accessed atomically. keep at top to ensure alignment on 32-bit systems. goidgen uint64 lastpoll uint64 lock mutex // When increasing nmidle, nmidlelocked, nmsys, or nmfreed, be // sure to call checkdead(). // 由空闲的工作线程组成链表 midle muintptr // idle m's waiting for work // 空闲的工作线程的数量 nmidle int32 // number of idle m's waiting for work nmidlelocked int32 // number of locked m's waiting for work mnext int64 // number of m's that have been created and next M ID // 最多只能创建maxmcount个工作线程 maxmcount int32 // maximum number of m's allowed (or die) nmsys int32 // number of system m's not counted for deadlock nmfreed int64 // cumulative number of freed m's ngsys uint32 // number of system goroutines; updated atomically // 由空闲的p结构体对象组成的链表 pidle puintptr // idle p's // 空闲的p结构体对象的数量 npidle uint32 nmspinning uint32 // See \"Worker thread parking/unparking\" comment in proc.go. // Global runnable queue. // goroutine全局运行队列 runq gQueue runqsize int32 ...... // Global cache of dead G's. // gFree是所有已经退出的goroutine对应的g结构体对象组成的链表 // 用于缓存g结构体对象,避免每次创建goroutine时都重新分配内存 gFree struct { lock mutex stack gList // Gs with stacks noStack gList // Gs without stacks n int32 } ...... } 5、重要的全局变量allgs []*g // 保存所有的g allm *m // 所有的m构成的一个链表,包括下面的m0 allp []*p // 保存所有的p,len(allp) == gomaxprocs ncpu int32 // 系统中cpu核的数量,程序启动时由runtime代码初始化 gomaxprocs int32 // p的最大值,默认等于ncpu,但可以通过GOMAXPROCS修改 sched schedt // 调度器结构体对象,记录了调度器的工作状态 m0 m // 代表进程的主线程 g0 g // m0的g0,也就是m0.g0 = &g0","categories":[{"name":"Go进阶","slug":"Go进阶","permalink":"http://coderedeng.github.io/categories/Go%E8%BF%9B%E9%98%B6/"}],"tags":[{"name":"Go进阶","slug":"Go进阶","permalink":"http://coderedeng.github.io/tags/Go%E8%BF%9B%E9%98%B6/"}]},{"title":"chan读写问题","slug":"Go进阶 - chan读写问题","date":"2021-02-15T14:38:41.000Z","updated":"2024-01-22T12:58:59.000Z","comments":true,"path":"2021/02/15/Go进阶 - chan读写问题/","link":"","permalink":"http://coderedeng.github.io/2021/02/15/Go%E8%BF%9B%E9%98%B6%20-%20chan%E8%AF%BB%E5%86%99%E9%97%AE%E9%A2%98/","excerpt":"","text":"1.chan读写问题01.对关闭chan读写 golang面试题:对已经关闭的的chan进行读写,会怎么样?为什么? 读已经关闭的 chan 能一直读到东西,但是读到的内容根据通道内关闭前是否有元素而不同。 1)读取有元素,且关闭的chan 会正确读到 chan 内的值,且返回的第二个 bool 值(是否读成功)为 true。 2)读取无元素,且关闭的chan chan 内无值,接下来所有接收的值都会非阻塞直接成功 返回 channel 元素的零值,但是第二个 bool 值一直为 false。 3)写已经关闭的 chan 会 panic 02.未初始化的的chan读写 对未初始化的的chan进行读写,会怎么样?为什么? 2.1 对于写的情况 未初始化的 chan 此时是等于 nil,当它不能阻塞的情况下,直接返回 false,表示写 chan 失败 当 chan 能阻塞的情况下,则直接阻塞 gopark(nil, nil, waitReasonChanSendNilChan, traceEvGoStop, 2) 然后调用 throw(s string) 抛出错误,其中 waitReasonChanSendNilChan 就是刚刚提到的报错 "chan send (nil chan)" 2.2 对于读的情况 未初始化的 chan 此时是等于 nil,当它不能阻塞的情况下,直接返回 false,表示读 chan 失败 当 chan 能阻塞的情况下,则直接阻塞 gopark(nil, nil, waitReasonChanReceiveNilChan, traceEvGoStop, 2) 然后调用 throw(s string) 抛出错误,其中 waitReasonChanReceiveNilChan 就是刚刚提到的报错 "chan receive (nil chan)"","categories":[{"name":"Go进阶","slug":"Go进阶","permalink":"http://coderedeng.github.io/categories/Go%E8%BF%9B%E9%98%B6/"}],"tags":[{"name":"Go进阶","slug":"Go进阶","permalink":"http://coderedeng.github.io/tags/Go%E8%BF%9B%E9%98%B6/"}]},{"title":"深浅拷贝","slug":"Go进阶 - 深浅拷贝","date":"2021-02-12T14:24:10.000Z","updated":"2024-01-22T12:58:59.000Z","comments":true,"path":"2021/02/12/Go进阶 - 深浅拷贝/","link":"","permalink":"http://coderedeng.github.io/2021/02/12/Go%E8%BF%9B%E9%98%B6%20-%20%E6%B7%B1%E6%B5%85%E6%8B%B7%E8%B4%9D/","excerpt":"","text":"1.深浅拷贝01.深浅拷贝1.1 深浅拷贝定义 浅拷贝就是只拷贝指针的值,指针指向的内容只有一份。 而深拷贝是把指针指向的值拷贝一份。 golang里面也有浅拷贝和深拷贝。 slice的浅拷贝就是指slice变量的赋值操作。 slice的深拷贝就是指使用内置的copy函数来拷贝两个slice。 1.2 深浅拷贝代码举例package main import \"fmt\" func main() { SliceShallowCopy() SliceDeepCopy() } func SliceShallowCopy() { src := []byte {1,2,3,4,5,6} dst := src fmt.Println(\"浅拷贝原始数据\",src) // [1 2 3 4 5 6] dst[0]=10 // 修改拷贝数据,原始数据会以前跟着改变 fmt.Println(\"after modify[src]:\",src) // [10 2 3 4 5 6] } func SliceDeepCopy() { src := []byte {1,2,3,4,5,6} var dst = make([]byte, len(src)) copy(dst[:], src) fmt.Println(\"深拷贝前:\",src) // [1 2 3 4 5 6] dst[0]=10 fmt.Println(\"深拷贝修改拷贝数据值后:\",src) // [1 2 3 4 5 6] }","categories":[{"name":"Go进阶","slug":"Go进阶","permalink":"http://coderedeng.github.io/categories/Go%E8%BF%9B%E9%98%B6/"}],"tags":[{"name":"Go进阶","slug":"Go进阶","permalink":"http://coderedeng.github.io/tags/Go%E8%BF%9B%E9%98%B6/"}]},{"title":"垃圾回收","slug":"Go进阶 - 垃圾回收","date":"2021-02-11T13:24:31.000Z","updated":"2024-01-22T12:58:59.000Z","comments":true,"path":"2021/02/11/Go进阶 - 垃圾回收/","link":"","permalink":"http://coderedeng.github.io/2021/02/11/Go%E8%BF%9B%E9%98%B6%20-%20%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6/","excerpt":"","text":"1.垃圾回收01.三种常见垃圾回收机制1.0 垃圾回收是什么 传统的系统级编程语言(主要指C/C++)中,程序员必须对内存小心的进行管理操作,控制内存的申请及释放。 稍有不慎,就可能产生内存泄露问题,这种问题不易发现并且难以定位 后来开发出来的几乎所有新语言(java,python,php等等)都引入了语言层面的自动内存管理 也就是语言的使用者只用关注内存的申请而不必关心内存的释放 内存释放由虚拟机(virtual machine)或运行时(runtime)来自动进行管理 而这种对不再使用的内存资源进行自动回收的行为就被称为垃圾回收。 1.1 引计数 原理 当一个对象的引用被创建或者复制时,对象的引用计数加1;当一个对象的引用被销毁时,对象的引用计数减1. 当对象的引用计数减少为0时,就意味着对象已经再没有被使用了,可以将其内存释放掉。 优点 引用计数有一个很大的优点,即实时性,任何内存,一旦没有指向它的引用,就会被立即回收,而其他的垃圾收集技术必须在某种特殊条件下才能进行无效内存的回收。 缺点 引用计数机制所带来的维护引用计数的额外操作与Python运行中所进行的内存分配和释放,引用赋值的次数是成正比的, 显然比其它那些垃圾收集技术所带来的额外操作只是与待回收的内存数量有关的效率要低。 同时,因为对象之间相互引用,每个对象的引用都不会为0,所以这些对象所占用的内存始终都不会被释放掉。 1.2 标记-清除 它分为两个阶段:第一阶段是标记阶段,GC会把所有的活动对象打上标记,第二阶段是把那些没有标记的对象非活动对象进行回收。 对象之间通过引用(指针)连在一起,构成一个有向图 从根对象(root object)出发,沿着有向边遍历对象,可达的(reachable)对象标记为活动对象,不可达的对象就是要被清除的非活动对象。 根对象就是全局变量、调用栈、寄存器。 在上图中,可以从程序变量直接访问块1,并且可以间接访问块2和3,程序无法访问块4和5 第一步将标记块1,并记住块2和3以供稍后处理。 第二步将标记块2,第三步将标记块3,但不记得块2,因为它已被标记。 扫描阶段将忽略块1,2和3,因为它们已被标记,但会回收块4和5。 1.3 分代回收 分代回收是建立在标记清除技术基础之上的,是一种以空间换时间的操作方式。 Python将内存分为了3“代”,分别为年轻代(第0代)、中年代(第1代)、老年代(第2代) 他们对应的是3个链表,它们的垃圾收集频率与对象的存活时间的增大而减小。 新创建的对象都会分配在年轻代,年轻代链表的总数达到上限时,Python垃圾收集机制就会被触发 把那些可以被回收的对象回收掉,而那些不会回收的对象就会被移到中年代去,依此类推 老年代中的对象是存活时间最久的对象,甚至是存活于整个系统的生命周期内。 02.Golang-三色标记法2.1 三色标记法介绍 三色标记法只是为了叙述方便而抽象出来的一种说法,实际上的对象是没有三色之分的。 这里的三色,对应了垃圾回收过程中对象的三种状态: 1)灰色(可能指向其他白色) :对象还在标记队列中等待 - 已被回收器访问到的对象,不会被回收 - 但回收器需要对其中的一个或多个指针进行扫描,因为他们可能还指向白色对象 - ``` 2)黑色(不指向其他白色) :对象已被标记, gcmarkBits 对应位为 1 -- 该对象不会在本次 GC 中被回收 - 已被回收器访问到的对象,其中所有字段都已被扫描 - 黑色对象中任何一个指针都不可能直接指向白色对象 3)白色(要被清除的):对象未被标记,gcmarkBits 对应位为 0 – 该对象将会在本次 GC 中被清理 2.2 具体流程如下图 就是标记内存中那些还在使用中(即被引用了)的部分 而内存中不再使用(即未被引用)的部分,就是要回收的垃圾,需要将其回收 上图中的 A、B、D 就是被引用正在使用的内存 而 C、F、E 曾经被使用过,但现在没有任何对象引用,就需要被回收掉。 而 Root 区域主要是程序运行到当前时刻的栈和全局数据区域,是实时正在使用到的内存,当然应该优先标记。 而考虑到内存块中存放的可能是指针,所以还需要递归的进行标记,待全部标记完后,就会对未被标记的内存进行回收。 2.3 STW弊端和优化 STW弊端 golang 的垃圾回收算法属于 标记-清除,是需要 STW 的 STW 就是 Stop The World 的意思,在 golang 中就是要停掉所有的 goroutine,专心进行垃圾回收,待垃圾回收结束后再恢复 goroutine 而 STW 时间的长短直接影响了应用的执行,如果时间过长,那将是灾难性的。 为了缩短 STW 时间,golang 不对优化垃圾回收算法 其中写屏障(Write Barrier)和辅助 GC(Mutator Assist)就是两种优化垃圾回收的方法 1)写屏障(Write Barrier) 而写屏障就是让 goroutine 与 GC 同时运行的手段,虽然不能完全消除 STW,但是可以大大减少 STW 的时间。 写屏障在 GC 的特定时间开启,开启后指针传递时会把指针标记,即本轮不回收,下次 GC 时再确定。 2)辅助 GC(Mutator Assist) 为了防止内存分配过快,在 GC 执行过程中 GC 过程中 mutator 线程会并发运行,而 mutator assist 机制会协助 GC 做一部分的工作 2.4 写屏障1、STW解决的问题 标记过程需的要STW,因为对象引用关系如果在标记阶段做了修改,会影响标记结果的正确性。 例如下图(假设没有STW) 1)灰色对象B引用白色对象C(此时C尚未被扫描) 2)当遍历完A对象后,A变成黑色 如果有其他程序断开了B对C的引用 同时添加了A对C的引用(由于A是黑色,所以C会一直是白色,被回收) 2、屏障技术 1)强三色不变式:强三色不变式很好理解,强制性的不允许黑色对象引用白色对象即可 插入屏障:插入屏障拦截将白色指针插入黑色对象的操作,标记其对应对象为灰色状态 2)弱三色不变式:所有被黑色对象引用的白色对象都处于灰色保护状态 删除屏障:也是拦截写操作的,但是是通过保护灰色对象到白色对象的路径不会断来实现的 被灰色引用的对象,被删除了最后一个指向它的指针,也依旧可以活过这一轮,在下一轮GC中被清理掉 3、混合写屏障 插入屏障 优缺点 插入写屏障在标记开始时无需STW,可直接开始,并发进行 但结束时需要STW来重新扫描栈,标记栈上引用的白色对象的存活 删除屏障 优缺点 删除写屏障则需要在GC开始时STW扫描堆栈来记录初始快照 这个过程会保护开始时刻的所有存活对象,但结束时无需STW Go1.8版本引入的混合写屏障 同样允许黑色对象引用白色对象,白色对象处于灰色保护状态,但是只由堆上的灰色对象保护。 只需要在开始时并发扫描各个goroutine的栈,使其变黑并一直保持,这个过程不需要STW 而标记结束后,因为栈在扫描后始终是黑色的,也无需再进行re-scan操作了,减少了STW的时间。 混合写屏障两种情况 灰色对象B在堆上 - 一个堆上的灰色对象B,引用白色对象C,在GC并发运行的过程中 - 如果栈已扫描置黑,而赋值器`将指向C的唯一指针从B中删除`,`并让栈上其他对象引用它` - 这时,写屏障会在删除指向白色对象C的指针的时候`就将C对象置灰`,就可以保护下来了 - ``` 灰色对象B在栈上 - 灰色对象B在栈上,引用堆上的白色对象C,将其引用关系删除,且新增一个黑色对象到对象C的引用 - 那么就需要通过shade(ptr)来保护了,在指针插入黑色对象时会触发对对象C的置灰操作。 - 如果栈已经被扫描过了,那么栈上引用的对象都是灰色或受灰色保护的白色对象了,所以就没有必要再进行这步操作。 2.5 垃圾回收触发机制 1、内存分配量达到阈值 每次内存分配都会检查当前内存分配量是否达到阈值,如果达到阈值则触发 GC。 阈值 = 上次 GC 内存分配量 * 内存增长率 内存增长率由环境变量 GOGC 控制,默认为 100,即每当内存扩大一倍时启动 GC。 2、定时触发 GC 默认情况下,2 分钟触发一次 GC,该间隔由 src/runtime/proc.go 中的 forcegcperiod 声明。 3、手动触发 GC 在代码中,可通过使用 runtime.GC() 手动触发 GC。 2.6 GC 优化建议 由上文可知,GC 性能是与对象数量有关的,对象越多 GC 性能越差,对程序的影响也越大。 所以在开发中要尽量减少对象分配个数,采用对象复用、将小对象组合成大对象或采用小数据类型(如使用 int8 代替 int)等。 2.7 结语 一门编程语言的垃圾回收机制会直接影响使用其开发应用的性能。 在日常开发工作中也因注意到其作用,有助于开发出高性能的应用,这也是 GC 常常在面试中被问到的原因。 同时,了解 GC 对了解内存管理也很有帮助。","categories":[{"name":"Go进阶","slug":"Go进阶","permalink":"http://coderedeng.github.io/categories/Go%E8%BF%9B%E9%98%B6/"}],"tags":[{"name":"Go进阶","slug":"Go进阶","permalink":"http://coderedeng.github.io/tags/Go%E8%BF%9B%E9%98%B6/"}]},{"title":"内存泄露","slug":"Go进阶 - 内存泄漏","date":"2021-02-10T13:10:32.000Z","updated":"2024-01-22T12:58:59.000Z","comments":true,"path":"2021/02/10/Go进阶 - 内存泄漏/","link":"","permalink":"http://coderedeng.github.io/2021/02/10/Go%E8%BF%9B%E9%98%B6%20-%20%E5%86%85%E5%AD%98%E6%B3%84%E6%BC%8F/","excerpt":"","text":"1.内存泄漏01.内存泄漏概念1.1 内存泄漏定义 定义:由于疏忽或错误造成程序未能释放已经不再使用的内存。 1.2 go内存泄漏两种情况 情况1:僵尸进程 - 有goroutine泄漏,goroutine“飞”了,zombie goroutine没有结束 - 这个时候在这个goroutine上分配的内存对象将一直被这个僵尸goroutine引用着 - 进而导致gc无法回收这类对象,内存泄漏 - ``` 情况2:全局数据结构挂住了本该释放的对象 - 有一些全局(或者生命周期和程序本身运行周期一样长的)的数据结构意外的挂住了本该释放的对象 - 虽然goroutine已经退出了,但是这些对象并没有从这类数据结构中删除,导致对象一直被引用,无法被回收。 02.内存泄漏排查2.1 排除掉goroutine泄漏 首先,我利用压测工具对server进行100个websocket连接,模拟用户浏览行为,然后关闭连接。 打开浏览器查看goroutine数量,发现新起的goroutine全部已经销毁,没有观察到有泄漏的goroutine,因此排除此情况。 2.2 确定是全局变量无回收 再次用压测工具进行压测然后关闭,使用观察内存情况。 使用go tool pprof -inuse_space http://127.0.0.1:9999/debug/pprof/heap 输入png导出(在这种情况下,需要等程序gc完再导出,建议等10分钟左右) 发现问题所在 每次都会遗留这么大概0.5M的内存空间出来 就奇怪,明明整个goroutine退出为什么还有会内存占用?相应的全局变量也会删除该地方的引用。 等一下,全局变量,难道是删除的时候没做好配对导致没有真正删除该引用吗? 去查了下代码,果然是没有删除引用导致的,至此问题解决。 这里面有个项目的坑,上报日志的key不是根据这个len(map)计算出,导致上报日志的时候以为删除了该key。 03.goroutine 泄露的场景 goroutine泄露一般是因为channel操作阻塞而导致整个routine一直阻塞等待或者 goroutine 里有死循环的时候 可以细分为下面五种情况 3.1 情况1:从channel里读但没有写 leak 是一个有 bug 程序。它启动了一个 goroutine 阻塞接收 channel。 当 Goroutine 正在等待时,leak 函数会结束返回。 此时,程序的其他任何部分都不能通过 channel 发送数据,那个 channel 永远不会关闭 fmt.Println 调用永远不会发生, 那个 goroutine 会被永远锁死 func leak() { ch := make(chan int) go func() { val := <-ch fmt.Println(\"We received a value:\", val) }() } 3.2 情况2:向已满的 buffered channel 写,但是没有读3.3 情况3: select操作在所有case上阻塞 实现一个 fibonacci 数列生成器,并在独立的 goroutine 中运行 在读取完需要长度的数列后,如果 用于 退出生成器的 quit 忘了被 close (或写入数据) select 将一直被阻塞造成 该 goroutine 泄露。 package main import \"fmt\" func fibonacci(c, quit chan int) { x, y := 0, 1 for{ select { case c <- x: x, y = y, x+y case <-quit: fmt.Println(\"quit\") return } } } func main() { c := make(chan int) quit := make(chan int) go fibonacci(c, quit) for i := 0; i < 10; i++{ fmt.Println(<- c) } // close(quit) } 3.4 goroutine进入死循环// 粗暴的示例 func foo() { for{ fmt.Println(\"fooo\") } }","categories":[{"name":"Go进阶","slug":"Go进阶","permalink":"http://coderedeng.github.io/categories/Go%E8%BF%9B%E9%98%B6/"}],"tags":[{"name":"Go进阶","slug":"Go进阶","permalink":"http://coderedeng.github.io/tags/Go%E8%BF%9B%E9%98%B6/"}]},{"title":"内存逃逸","slug":"Go进阶 - 内存逃逸","date":"2021-02-09T14:01:35.000Z","updated":"2024-01-22T12:58:59.000Z","comments":true,"path":"2021/02/09/Go进阶 - 内存逃逸/","link":"","permalink":"http://coderedeng.github.io/2021/02/09/Go%E8%BF%9B%E9%98%B6%20-%20%E5%86%85%E5%AD%98%E9%80%83%E9%80%B8/","excerpt":"","text":"1.内存逃逸01.内存逃逸1.1 其他语言内存回收机制 在C/C++开发中,动态分配内存(new/malloc)需要我们手动释放资源。 这样做的好处是,需要申请多少内存空间可以很好的掌握怎么分配。 但是这有个缺点,如果忘记释放内存,则会导致内存泄漏。 在很多高级语言中(python/Go/java)都加上了垃圾回收机制。 1.2 什么是内存逃逸 函数内部申请的临时变量,正常会分配到栈里,栈中的内存分配非常快,自动回收,无需垃圾回收 但是若果申请的临时变量作为了函数返回值,编译器会认为在退出函数之后还有其他地方在引用 在编译的时候就会将变量存储到堆中,堆中的数据不会自动回收,必须使用垃圾回收机制清楚 我们将这种 由于某些原因,数据没有分配到栈中而是分配到堆中的现象叫做 内存逃逸 1.2 golang的内存分配之堆和栈1.2.1 内存分片概述 Go的垃圾回收,让堆和栈堆程序员保持透明。 真正解放了程序员的双手,让他们可以专注于业务,“高效”地完成代码编写。 把那些内存管理的复杂机制交给编译器。 栈 可以简单得理解成一次函数调用内部申请到的内存,它们会随着函数的返回把内存还给系统。 1.2.2 分配到栈里 下面的例子,函数内部申请的临时变量,即使你是用make申请到的内存 如果发现在退出函数后没有用了,那么就把丢到栈上,毕竟栈上的内存分配比堆上快很多 func F() { temp := make([]int, 0, 20) ... } 1.2.3 分配到堆里 申请的代码和上面的一模一样,但是申请后作为返回值返回了 编译器会认为在退出函数之后还有其他地方在引用,当函数返回之后并不会将其内存归还 那么就申请到堆里。 func F() { temp := make([]int, 0, 20) ... return temp } 1.3 分配到栈里和堆里区别 分片到堆的坏处 如果变量都分配到堆上,堆不像栈可以自动清理。 它会引起Go频繁地进行垃圾回收,而垃圾回收会占用比较大的系统开销。 放到堆还是栈的情况 堆适合不可预知的大小的内存分配。 但是为此付出的代价是分配速度较慢,而且会形成内存碎片。 栈内存分配则会非常快,栈分配内存只需要两个CPU指令:“PUSH”和“RELEASE”分配和释放; 而堆分配内存首先需要去找到一块大小合适的内存块。之后要通过垃圾回收才能释放。 02.逃逸的几种情况2.0 逃逸分析 逃逸分析是分析在程序的哪些地方可以访问到该指针。 简单来说,编译器会根据变量是否被外部引用来决定是否逃逸 1、如果函数外部没有引用,则优先放到栈中; 2、如果函数外部存在引用,则必定放到堆中; 对此你可以理解为,逃逸分析是编译器用于决定变量分配到堆上还是栈上的一种行为。 注意:go 在编译阶段确立逃逸,并不是在运行时。 2.1 指针逃逸方法内把局部变量指针返回 提问:函数传递指针真的比传值效率高吗? 我们知道传递指针可以减少底层值的拷贝,可以提高效率 但是如果拷贝的数据量小,由于指针传递会产生逃逸,逃逸可能存储到堆中 存储到堆可能会增加GC的负担,所以传递指针不一定是高效的。 2.2 栈空间不足逃逸 当我们创建一个切片长度为10000时就会逃逸。 实际上当栈空间不足以存放当前对象时或无法判断当前切片长度时会将对象分配到堆中。 slice 的背后数组被重新分配了,因为 append 时可能会超出其容量( cap )。 slice 初始化的地方在编译时是可以知道的,它最开始会在栈上分配。 如果切片背后的存储要基于运行时的数据进行扩充,就会在堆上分配。 go build -gcflags=-m package main import \"fmt\" func main() { s := make([]int, 10000, 10000) fmt.Println(s) } 2.3 动态类型逃逸 很多函数参数为interface类型,比如 Println函数 func Println(a ...interface{}) (n int, err error) 编译期间很难确定其参数的具体类型,也能产生逃逸。 03.如何避免 1、不要盲目使用变量指针作为参数,虽然减少了复制,但变量逃逸的开销更大。 2、预先设定好slice长度,避免频繁超出容量,重新分配。 3、如果对于性能要求比较高且访问频次比较高的函数调用,应该尽量避免使用接口类型 在 interface 类型上调用方法都是动态调度的 —— 方法的真正实现只能在运行时知道 04.逃逸分析作用4.1 逃逸分析作用 1、逃逸分析的好处是为了减少gc的压力,不逃逸的对象分配在栈上,当函数返回时就回收了资源,不需要gc标记清除。 2、逃逸分析完后可以确定哪些变量可以分配在栈上,栈的分配比堆快,性能好 3、同步消除,如果你定义的对象的方法上有同步锁,但在运行时,却只有一个线程在访问,此时逃逸分析后的机器码,会去掉同步锁运行。 4.2 总结 1、堆上动态分配内存比栈上静态分配内存,开销大很多。 2、变量分配在栈上需要能在编译期确定它的作用域,否则会分配到堆上。 3、Go编译器会在编译期对考察变量的作用域,并作一系列检查 如果它的作用域在运行期间对编译器一直是可知的,那么就会分配到栈上。 简单来说,编译器会根据变量是否被外部引用来决定是否逃逸。 4、不要盲目使用变量的指针作为函数参数,虽然它会减少复制操作。 但其实当参数为变量自身的时候,复制是在栈上完成的操作 开销远比变量逃逸后动态地在堆上分配内存少的多。 5、逃逸分析在编译阶段完成的。","categories":[{"name":"Go进阶","slug":"Go进阶","permalink":"http://coderedeng.github.io/categories/Go%E8%BF%9B%E9%98%B6/"}],"tags":[{"name":"Go进阶","slug":"Go进阶","permalink":"http://coderedeng.github.io/tags/Go%E8%BF%9B%E9%98%B6/"}]},{"title":"make和new","slug":"Go进阶 - make和new","date":"2021-02-08T13:21:28.000Z","updated":"2024-01-22T12:58:59.000Z","comments":true,"path":"2021/02/08/Go进阶 - make和new/","link":"","permalink":"http://coderedeng.github.io/2021/02/08/Go%E8%BF%9B%E9%98%B6%20-%20make%E5%92%8Cnew/","excerpt":"","text":"1.make和new01.make和new1.1 make和new比较 new 和 make 是两个内置函数,主要用来创建并分配类型的内存。 make和new区别 make 关键字的作用是创建于 slice、map 和 channel 等内置的数据结构 new 的作用是为类型申请一片内存空间,并返回指向这片内存的指针 package main import \"fmt\" func main() { a := make([]int, 3, 10) // 切片长度为 1,预留空间长度为 10 a = append(a,1) fmt.Printf(\"%v--%T \\n\",a,a) // [0 0 0]--[]int 值----切片本身 var b = new([]int) //b = b.append(b,2) // 返回的是内存指针,所以不能直接 append *b = append(*b, 3) // 必须通过 * 指针取值,才能进行 append 添加 fmt.Printf(\"%v--%T\",b,b) // &[]--*[]string 内存的指针---内存指针 } 1.2 new函数 一:系统默认的数据类型,分配空间 package main import \"fmt\" func main() { // 1.new实例化int age := new(int) *age = 1 // 2.new实例化切片 li := new([]int) *li = append(*li, 1) // 3.实例化map userinfo := new(map[string]string) *userinfo = map[string]string{} (*userinfo)[\"username\"] = \"张三\" fmt.Println(userinfo) // &map[username:张三] } 二:自定义类型使用 new 函数来分配空间 package main import \"fmt\" func main() { var s *Student s = new(Student) //分配空间 s.name =\"zhangsan\" fmt.Println(s) // &{zhangsan 0} } type Student struct { name string age int } 1.3 make函数 make 也是用于内存分配的,但是和 new 不同,它只用于 chan、map 以及 slice 的内存创建 而且它返回的类型就是这三个类型本身,而不是他们的指针类型 因为这三种类型就是引用类型,所以就没有必要返回他们的指针了 package main import \"fmt\" func main() { a := make([]int, 3, 10) // 切片长度为 1,预留空间长度为 10 b := make(map[string]string) c := make(chan int, 1) fmt.Println(a,b,c) // [0 0 0] map[] 0xc0000180e0 } 当我们为slice分配内存的时候,应当尽量预估到slice可能的最大长度 通过给make传第三个参数的方式来给slice预留好内存空间 这样可以避免二次分配内存带来的开销,大大提高程序的性能。","categories":[{"name":"Go进阶","slug":"Go进阶","permalink":"http://coderedeng.github.io/categories/Go%E8%BF%9B%E9%98%B6/"}],"tags":[{"name":"Go进阶","slug":"Go进阶","permalink":"http://coderedeng.github.io/tags/Go%E8%BF%9B%E9%98%B6/"}]},{"title":"pointer","slug":"Go进阶 - pointer","date":"2021-02-07T14:28:36.000Z","updated":"2024-05-21T14:45:57.792Z","comments":true,"path":"2021/02/07/Go进阶 - pointer/","link":"","permalink":"http://coderedeng.github.io/2021/02/07/Go%E8%BF%9B%E9%98%B6%20-%20pointer/","excerpt":"","text":"01.pointer01.pointer1.1 什么是pointer 在Go里面pointer就是1种可以把内存地址存储起来的数据类型。 我们使用pointer数据类型的变量可以记录下另1个变量的内存地址,方便我们修改这变量的值。 只需要记住以下几点: &变量名: 获取变量的内存地址 *pointor:通过指针类型的变量,获取该指针指向的值 package main import \"fmt\" func main() { name := \"张三\" p1 := &name // &变量名: 获取变量的内存地址 p2 := *&name // *pointor:通过指针类型的变量,获取该指针指向的值 fmt.Println(name,p1,p2) // 张三 0xc000088230 张三 } 1.2 为什么Go中使用了指针? 因为指针可以帮助我们节省内存,我们知道在程序运行时值类型的变量被赋值之后会对值进行重新拷贝 如果我们每次拷贝的是1个指针类型的变量呢? 值类型变量:在函数中传递无法修改变量的值 还有Go函数里面传递的参数都是副本也就是重新copy一份,我们如何在函数中修该1个外部变量。 我们可以通过记录下值类型变量的内存地址,来达到修改值类型变量的目的。","categories":[{"name":"Go进阶","slug":"Go进阶","permalink":"http://coderedeng.github.io/categories/Go%E8%BF%9B%E9%98%B6/"}],"tags":[{"name":"Go进阶","slug":"Go进阶","permalink":"http://coderedeng.github.io/tags/Go%E8%BF%9B%E9%98%B6/"}]},{"title":"interface","slug":"Go进阶 - interface","date":"2021-02-05T14:15:27.000Z","updated":"2024-01-22T12:58:59.000Z","comments":true,"path":"2021/02/05/Go进阶 - interface/","link":"","permalink":"http://coderedeng.github.io/2021/02/05/Go%E8%BF%9B%E9%98%B6%20-%20interface/","excerpt":"","text":"1.interface01.interface1.1 interface作用 接口是 Go 语言的重要组成部分,它在 Go 语言中通过一组方法指定了一个对象的行为 接口 interface 的引入能够让我们在 Go 语言更好地组织并写出易于测试的代码 golang中的接口分为 带方法的接口和空接口 iface:表示带方法的接口 eface:表示空接口 1.2 eface空接口 空接口eface结构比较简单,由两个属性构成 一个是类型信息_type,一个是数据信息 其数据结构声明如下: type eface struct { _type *_type data unsafe.Pointer } 其中_type是GO语言中所有类型的公共描述,Go语言几乎所有的数据结构都可以抽象成 _type,是所有类型的公共描述 type负责决定data应该如何解释和操作 type的结构代码如下: type _type struct { size uintptr ptrdata uintptr // size of memory prefix holding all pointers hash uint32 // 类型哈希 tflag tflag align uint8 // _type作为整体变量存放时的对齐字节数 fieldalign uint8 kind uint8 alg *typeAlg // gcdata stores the GC type data for the garbage collector. // If the KindGCProg bit is set in kind, gcdata is a GC program. // Otherwise it is a ptrmask bitmap. See mbitmap.go for details. gcdata *byte str nameOff ptrToThis typeOff // type for pointer to this type, may be zero } data表示指向具体的实例数据,由于Go的参数传递规则为值传递 如果希望可以通过interface对实例数据修改,则需要传入指针 此时data指向的是指针的副本,但指针指向的实例地址不变,仍然可以对实例数据产生修改。 1.3 iface带方法的接口 iface 表示带方法的数据结构,非空接口初始化的过程就是初始化一个iface类型的结构 其中data的作用同eface的相同,这里不再多加描述。 type iface struct { tab *itab data unsafe.Pointer } iface结构中最重要的是itab结构(结构如下),每一个 itab 都占 32 字节的空间 itab可以理解为pair<interface type, concrete type> itab里面包含了interface的一些关键信息,比如method的具体实现 type itab struct { inter *interfacetype // 接口自身的元信息 _type *_type // 具体类型的元信息 link *itab bad int32 hash int32 // _type里也有一个同样的hash,此处多放一个是为了方便运行接口断言 fun [1]uintptr // 函数指针,指向具体类型所实现的方法 } // interface type包含了一些关于interface本身的信息,比如package path,包含的method type interfacetype struct { typ _type pkgpath name mhdr []imethod } type imethod struct { //这里的 method 只是一种函数声明的抽象,比如 func Print() error name nameOff ityp typeOff } 1.4 interface设计的优缺点 优点,非侵入式设计,写起来更自由,无需显式实现,只要实现了与 interface 所包含的所有函数签名相同的方法即可。 缺点,duck-typing风格并不关注接口的规则和含义,也没法检查,不确定某个struct具体实现了哪些interface。 只能通过goru工具查看。","categories":[{"name":"Go进阶","slug":"Go进阶","permalink":"http://coderedeng.github.io/categories/Go%E8%BF%9B%E9%98%B6/"}],"tags":[{"name":"Go进阶","slug":"Go进阶","permalink":"http://coderedeng.github.io/tags/Go%E8%BF%9B%E9%98%B6/"}]},{"title":"Channel","slug":"Go进阶 - Channel","date":"2021-01-30T13:26:15.000Z","updated":"2024-01-22T12:58:59.000Z","comments":true,"path":"2021/01/30/Go进阶 - Channel/","link":"","permalink":"http://coderedeng.github.io/2021/01/30/Go%E8%BF%9B%E9%98%B6%20-%20Channel/","excerpt":"","text":"1.channel01.channel的整体结构图1.1 channel结构图 channel本质是一个hchan这个结构体 type hchan struct { buf unsafe.Pointer // points to an array of dataqsiz elements sendx uint // send index recvx uint // receive index recvq waitq // list of recv waiters sendq waitq // list of send waiters lock mutex } 简单说明: buf是有缓冲的channel所特有的结构,用来存储缓存数据,是个循环链表 sendx 和 recvx 用于记录 buf 这个循环链表中的~发送或者接收的~index - `recvx`和`sendx`是根据循环链表`buf`的变动而改变的 - `lock`是个互斥锁,发送或接收前都需要加锁 - ``` recvq 和 sendq —> 是个双向链表 - 分别是接收或者发送的goroutine抽象出来的结构体(sudog)的队列 1.2 创建channel 创建channel实际上就是在内存中实例化了一个hchan的结构体,并返回一个ch指针 我们使用过程中channel在函数之间的传递都是用的这个指针 这就是为什么函数传递中无需使用channel的指针,而直接用channel就行了,因为channel本身就是一个指针 1.3 channel中队列如何实现 channel中有个缓存buf,是用来缓存数据的(假如实例化了带缓存的channel的话)队列。 当使用send (ch <- xx)或者recv ( <-ch)的时候,首先要锁住hchan这个结构体 然后开始send (ch <- xx)数据,这时候满了,队列塞不进去了 然后是取recv ( <-ch)的过程,是个逆向的操作,也是需要加锁 1.4 channel缓存满发生什么 使用的时候,我们都知道,当channel缓存满了,或者没有缓存的时候 我们继续send(ch <- xxx)或者recv(<- ch)会阻塞当前goroutine,但是,是如何实现的呢? Go的goroutine是用户态的线程(user-space threads),用户态的线程是需要自己去调度的 Go有运行时的scheduler去帮我们完成调度这件事情","categories":[{"name":"Go进阶","slug":"Go进阶","permalink":"http://coderedeng.github.io/categories/Go%E8%BF%9B%E9%98%B6/"}],"tags":[{"name":"Go进阶","slug":"Go进阶","permalink":"http://coderedeng.github.io/tags/Go%E8%BF%9B%E9%98%B6/"}]},{"title":"数组与切片","slug":"Go进阶 - 数组与切片","date":"2021-01-25T14:10:18.000Z","updated":"2024-01-22T12:58:59.000Z","comments":true,"path":"2021/01/25/Go进阶 - 数组与切片/","link":"","permalink":"http://coderedeng.github.io/2021/01/25/Go%E8%BF%9B%E9%98%B6%20-%20%E6%95%B0%E7%BB%84%E4%B8%8E%E5%88%87%E7%89%87/","excerpt":"","text":"1.数组与切片01.数组1.1 数组 数组是一种非常有用的数据结构,因为其占用的内存是连续分配的。 由于内存连续,CPU能把正在使用的数据缓存更久的时间。 而且内存连续很容易计算索引,可以快速迭代数组里的所有元素。 golang中声明数组需要告诉数组长度,以及存放数据类型 一旦初始化成功,那么存储的数据类型和数组长度就都不能改变了 xxxxxxxxxx package mainimport “fmt”func main() { SliceShallowCopy() SliceDeepCopy()}func SliceShallowCopy() { src := []byte {1,2,3,4,5,6} dst := src fmt.Println(“浅拷贝原始数据”,src) // [1 2 3 4 5 6] dst[0]=10 // 修改拷贝数据,原始数据会以前跟着改变 fmt.Println(“after modify[src]:”,src) // [10 2 3 4 5 6]}func SliceDeepCopy() { src := []byte {1,2,3,4,5,6} var dst = make([]byte, len(src)) copy(dst[:], src) fmt.Println(“深拷贝前:”,src) // [1 2 3 4 5 6] dst[0]=10 fmt.Println(“深拷贝修改拷贝数据值后:”,src) // [1 2 3 4 5 6]}go 1.2 引用类型 golang 的引用类型包括 slice、map、channel、function、pointer 等. 它们在进行赋值时拷贝的是指针值,但拷贝后指针指向的地址是相同的. 02.切片的内部实现 切片是一种数据结构,这种数据结构便于使用和管理数据集合。 切片是围绕动态数组的概念构建的,可以按需自动增长和缩小。 切片的动态增长是通过内置函数 append 来实现的,这个函数可以快速且高效地增长切片。 还可以通过对切片再次切片来缩小一个切片的大小。 因为切片的底层内存也是在连续块中分配的,所以切片还能获得索引、迭代以及为垃圾回收优化的好处。","categories":[{"name":"Go进阶","slug":"Go进阶","permalink":"http://coderedeng.github.io/categories/Go%E8%BF%9B%E9%98%B6/"}],"tags":[{"name":"Go进阶","slug":"Go进阶","permalink":"http://coderedeng.github.io/tags/Go%E8%BF%9B%E9%98%B6/"}]},{"title":"Map","slug":"Go进阶 - Map","date":"2021-01-23T14:45:18.000Z","updated":"2024-01-22T12:58:59.000Z","comments":true,"path":"2021/01/23/Go进阶 - Map/","link":"","permalink":"http://coderedeng.github.io/2021/01/23/Go%E8%BF%9B%E9%98%B6%20-%20Map/","excerpt":"","text":"01.Map01.map底层 [参考(opens new window)](https://www.bookstack.cn/read/qcrao-Go-Questions/map-map 的扩容过程是怎样的.md) 1.1 map底层浅析 笼统的来说,go的map底层是一个hash表,通过键值对进行映射 键通过哈希函数生成哈希值,然后go底层的map数据结构就存储相应的hash值,进行索引,最终是在底层使用的数组存储key,和value 稍微详细的说,就设计到go map 的结构:hmap 和bmap 1.2 Hash函数 哈希函数会将传入的key值进行哈希运算,得到一个唯一的值。 go语言把生成的哈希值一分为二,比如一个key经过哈希函数,生成的哈希值为:8423452987653321,go语言会这它拆分为84234529,和87653321。 那么,前半部分就叫做 高位哈希值 ,后半部分就叫做 低位哈希值 。 高位哈希值:是用来确定当前的bucket(桶)有没有所存储的数据的。 低位哈希值:是用来确定,当前的数据存在了哪个bucket(桶) 02.map源码2.1 hmap(a header of map) hmap是map的最外层的一个数据结构,包括了map的各种基础信息、如大小、bucket。 首先说一下,buckets这个参数,它存储的是指向buckets数组的一个指针,当bucket(桶为0时)为nil。 我们可以理解为,hmap指向了一个空bucket数组,并且当bucket数组需要扩容时,它会开辟一倍的内存空间 并且会渐进式的把原数组拷贝,即用到旧数组的时候就拷贝到新数组。 // Go map的一个header结构 type hmap struct { count int // map的大小. len()函数就取的这个值 flags uint8 //map状态标识 B uint8 // 可以最多容纳 6.5 * 2 ^ B 个元素,6.5为装载因子即:map长度=6.5*2^B //B可以理解为buckets已扩容的次数 noverflow uint16 // 溢出buckets的数量 hash0 uint32 // hash 种子 buckets unsafe.Pointer //指向最大2^B个Buckets数组的指针. count==0时为nil. oldbuckets unsafe.Pointer //指向扩容之前的buckets数组,并且容量是现在一半.不增长就为nil nevacuate uintptr // 搬迁进度,小于nevacuate的已经搬迁 extra *mapextra // 可选字段,额外信息 } 2.2 bmap(a bucket of map) bucket(桶),每一个bucket最多放8个key和value,最后由一个overflow字段指向下一个bmap 注意key、value、overflow字段都不显示定义,而是通过maptype计算偏移获取的。 // Go map 的 buckets结构 type bmap struct { // 每个元素hash值的高8位,如果tophash[0] < minTopHash,表示这个桶的搬迁状态 tophash [bucketCnt]uint8 // 第二个是8个key、8个value,但是我们不能直接看到;为了优化对齐,go采用了key放在一起,value放在一起的存储方式, // 第三个是溢出时,下一个溢出桶的地址 } bucket这三部分内容决定了它是怎么工作的 第一部分:tophash 它的tophash 存储的是哈希函数算出的哈希值的高八位是用来加快索引的。 因为把高八位存储起来,这样不用完整比较key就能过滤掉不符合的key,加快查询速度 当一个哈希值的高8位和存储的高8位相符合,再去比较完整的key值,进而取出value。 第二部分:存储的是key 和value 就是我们传入的key和value,注意,它的底层排列方式是,key全部放在一起,value全部放在一起。 当key大于128字节时,bucket的key字段存储的会是指针,指向key的实际内容;value也是一样。 这样排列好处是在key和value的长度不同的时候,可以消除padding带来的空间浪费。 并且每个bucket最多存放8个键值对 第三部分:存储的是当bucket溢出时,指向的下一个bucket的指针 bucket的设计细节: 在golang map中出现冲突时,不是每一个key都申请一个结构通过链表串起来 **而是以bmap为最小粒度挂载,一个bmap可以放8个key和value。 这样减少对象数量,减轻管理内存的负担,利于gc。 如果插入时,bmap中key超过8,那么就会申请一个新的bmap(overflow bucket)挂在这个bmap的后面形成链表 优先用预分配的overflow bucket,如果预分配的用完了,那么就malloc一个挂上去。 注意golang的map不会shrink,内存只会越用越多,overflow bucket中的key全删了也不会释放 2.3 map图解 hmap存储了一个指向底层bucket数组的指针。 我们存入的key和value是存储在bucket里面中,如果key和value大于128字节,那么bucket里面存储的是指向我们key和value的指针,如果不是则存储的是值。 每个bucket 存储8个key和value,如果超过就重新创建一个bucket挂在在元bucket上,持续挂接形成链表。 高位哈希值:是用来确定当前的bucket(桶)有没有所存储的数据的。 低位哈希值:是用来确定,当前的数据存在了哪个bucket(桶) 2.4 查找流程 查找或者操作map时,首先key经过hash函数生成hash值,通过哈希值的低8位来判断当前数据属于哪个桶(bucket) 找到bucket以后,通过哈希值的高八位与bucket存储的高位哈希值循环比对 如果相同就比较刚才找到的底层数组的key值,如果key相同,取出value。 如果高八位hash值在此bucket没有,或者有,但是key不相同,就去链表中下一个溢出bucket中查找,直到查找到链表的末尾 碰撞冲突: 如果不同的key定位到了统一bucket或者生成了同一hash,就产生冲突。 go是通过链表法来解决冲突的。 比如一个高八位的hash值和已经存入的hash值相同,并且此bucket存的8个键值对已经满了,或者后面已经挂了好几个bucket了。 那么这时候要存这个值就先比对key,key肯定不相同啊,那就从此位置一直沿着链表往后找,找到一个空位置,存入它。 所以这种情况,两个相同的hash值高8位是存在不同bucket中的。 03.map增删改查原理3.1 创建 map的创建比较简单,在参数校验之后,需要找到合适的B来申请桶的内存空间 接着便是穿件hmap这个结构,以及对它的初始化。 3.2 访问 - mapaccess 3.3 分配 - mapassign 为一个key分配空间的逻辑,大致与查找类似,但增加了写保护和扩容的操作; 注意,分配过程和删除过程都没有在oldbuckets中查找,这是因为首先要进行扩容判断和操作; 3.4 删除 - mapdelete 删除某个key的操作与分配类似,由于hashmap的存储结构是数组+链表, 所以真正删除key仅仅是将对应的slot设置为empty,并没有减少内存; 04.map扩容策略是什么4.1 map简述 使用哈希表的目的就是要快速查找到目标 key,然而,随着向 map 中添加的 key 越来越多,key 发生碰撞的概率也越来越大。 bucket 中的 8 个 cell 会被逐渐塞满,查找、插入、删除 key 的效率也会越来越低。 最理想的情况是一个 bucket 只装一个 key,这样,就能达到 O(1) 的效率,但这样空间消耗太大,用空间换时间的代价太高。 Go 语言采用一个 bucket 里装载 8 个 key,定位到某个 bucket 后,还需要再定位到具体的 key,这实际上又用了时间换空间。 当然,这样做,要有一个度,不然所有的 key 都落在了同一个 bucket 里,直接退化成了链表,各种操作的效率直接降为 O(n),是不行的。 装载因子 需要有一个指标来衡量前面描述的情况,这就是装载因子 loadFactor := count /(2^B) 1 - count 就是 map 的元素个数,2^B 表示 bucket 数量 ### 4.2 触发 map 扩容的时机 - 在向 map 插入新 key 的时候,会进行条件检测,符合下面这 2 个条件,就会触发扩容 - 1)装载因子超过阈值,源码里定义的阈值是 6.5。 - 2)overflow 的 bucket 数量过多: - 当 B 小于 15,也就是 bucket 总数 2^B 小于 2^15 时,如果 overflow 的 bucket 数量超过 2^B; - 当 B >= 15,也就是 bucket 总数 2^B 大于等于 2^15,如果 overflow 的 bucket 数量超过 2^15。 #### 4.2.1 第 1 点扩容 - 我们知道,每个 bucket 有 8 个空位,在没有溢出,且所有的桶都装满了的情况下,装载因子算出来的结果是 8。 - 因此当装载因子超过 6.5 时,表明很多 bucket 都快要装满了,查找效率和插入效率都变低了。在这个时候进行扩容是有必要的。 - ``` 对于条件 1,扩容方案 元素太多,而 bucket 数量太少,很简单:将 B 加 1,bucket 最大数量(2^B)直接变成原来 bucket 数量的 2 倍。 于是,就有新老 bucket 了 注意,这时候元素都在老 bucket 里,还没迁移到新的 bucket 来。 而且,新 bucket 只是最大数量变为原来最大数量(2^B)的 2 倍(2^B * 2)。 4.2.2 第2点扩容是对第1点的补充 就是说在装载因子比较小的情况下,这时候 map 的查找和插入效率也很低,而第 1 点识别不出来这种情况。 表面现象就是计算装载因子的分子比较小,即 map 里元素总数少,但是 bucket 数量多(真实分配的 bucket 数量多,包括大量的 overflow bucket)。 不难想像造成这种情况的原因:不停地插入、删除元素。 先插入很多元素,导致创建了很多 bucket,但是装载因子达不到第 1 点的临界值,未触发扩容来缓解这种情况。 之后,删除元素降低元素总数量,再插入很多元素,导致创建很多的 overflow bucket,但就是不会触犯第 1 点的规定 overflow bucket 数量太多,导致 key 会很分散,查找插入效率低得吓人,因此出台第 2 点规定。 这就像是一座空城,房子很多,但是住户很少,都分散了,找起人来很困难。 对于条件 2,扩容方案 其实元素没那么多,但是 overflow bucket 数特别多,说明很多 bucket 都没装满。 解决办法就是开辟一个新 bucket 空间,将老 bucket 中的元素移动到新 bucket,使得同一个 bucket 中的 key 排列地更紧密。 这样,原来,在 overflow bucket 中的 key 可以移动到 bucket 中来。 结果是节省空间,提高 bucket 利用率,map 的查找和插入效率自然就会提升。 05.map值无法地址取值5.1 map指针取地址报错问题 当通过key获取到value时,这个value是不可寻址的,因为map 会进行动静扩容 当进行扩大后,map的value就会进行内存迁徙,其地址发生变化,所以无奈对这个value进行寻址 package main type UserInfo struct { UserName string `json:\"user_name\"` } func main() { user := make(map[string]UserInfo) user[\"0001\"] = UserInfo{ UserName: \"jack\", } // 因为map会进行动静扩容,当进行扩大后,map的value就会进行内存迁徙,其地址发生变化 // 所以 user[\"0001\"] 返回值不是固定的地址,所以无法获取地址 p1 := &user[\"0001\"] // Cannot take the address of 'user[\"0001\"]' // 如果非要获取地址可以先赋值给一个变量 u := user[\"0001\"] p2 := &u } 5.2 map使用指针valuepackage main import \"fmt\" type UserInfo struct { UserName string `json:\"user_name\"` } func main() { user := make(map[string]*UserInfo) user[\"0001\"] = &UserInfo{ UserName: \"jack\", } // 上面是指针 *UserInfo 才能这样操作,否则报错 user[\"0001\"].UserName = \"tom\" fmt.Println(user[\"0001\"]) }","categories":[{"name":"Go进阶","slug":"Go进阶","permalink":"http://coderedeng.github.io/categories/Go%E8%BF%9B%E9%98%B6/"}],"tags":[{"name":"Go进阶","slug":"Go进阶","permalink":"http://coderedeng.github.io/tags/Go%E8%BF%9B%E9%98%B6/"}]},{"title":"字符串","slug":"Go进阶 - 字符串","date":"2021-01-20T13:25:14.000Z","updated":"2024-01-22T12:58:59.000Z","comments":true,"path":"2021/01/20/Go进阶 - 字符串/","link":"","permalink":"http://coderedeng.github.io/2021/01/20/Go%E8%BF%9B%E9%98%B6%20-%20%E5%AD%97%E7%AC%A6%E4%B8%B2/","excerpt":"","text":"1.字符串01.字符串底层1.1 字符串底层结构 一个字符串是一个不可改变的字节序列,字符串通常是用来包含人类可读的文本数据。 和数组不同的是,字符串的元素不可修改,是一个只读的字节数组。 每个字符串的长度虽然也是固定的,但是字符串的长度并不是字符串类型的一部分。 Go语言字符串的底层结构在 reflect.StringHeader 中定义 type StringHeader struct { Data uintptr Len int } 字符串结构由两个信息组成 第一个是字符串指向的底层字节数组 第二个是字符串的字节的长度 而slice 包含一个数据指针、一个长度和一个容量,当容量不够时会重新申请新的内存,Data 指针将指向新的地址,原来的地址空间将被释放 字符串其实是一个结构体,因此字符串的赋值操作也就是 reflect.StringHeader 结构体的复制过程,并不会涉及底层字节数组的复制。 我们可以看看字符串“Hello, world”本身对应的内存结构: 字符串虽然不是切片,但是支持切片操作,不同位置的切片底层也访问的同一块内存数据 因为字符串是只读的,相同的字符串通常是对应同一个字符串常量 1.2 reflect.StringHeader 字符串和数组类似,内置的 len 函数返回字符串的长度。 也可以通过 reflect.StringHeader 结构访问字符串的长度 package main import ( \"fmt\" \"reflect\" \"unsafe\" ) func main() { a :=\"aaa\" // 1.unsafe.Pointer(&a)方法可以得到变量a的地址 fmt.Println(unsafe.Pointer(&a)) // 0xc000054240 // 2.(*reflect.StringHeader)(unsafe.Pointer(&a)) 可以把字符串a转成底层结构的形式。 b := (*reflect.StringHeader)(unsafe.Pointer(&a)).Len fmt.Println(b) // 3 // 3.(*[]byte)(unsafe.Pointer(&ssh)) 可以把ssh底层结构体转成byte的切片的指针 // 4.再通过 *转为指针指向的实际内容 ssh := *(*reflect.StringHeader)(unsafe.Pointer(&a)) c := *(*[]byte)(unsafe.Pointer(&ssh)) fmt.Printf(\"%v\",c) // [97 97 97] } 1.3 修改字符串 要修改字符串,需要先将其转换成[]rune 或[]byte,完成后再转换为 string。 无论哪种转换,都会重新分配内存,并复制字节数组。 package main import \"fmt\" func main() { s1 := \"big\" // 强制类型转换 byteS1 := []byte(s1) byteS1[0] = 'p' fmt.Println(string(byteS1)) // pig s2 := \"白萝卜\" runeS2 := []rune(s2) runeS2[0] = '红' fmt.Println(string(runeS2)) // 红萝卜 } 1.4 字符串反转 rune关键字,从golang源码中看出,它是int32的别名(-2^31 ~ 2^31-1),比起byte(-128~127),可表示更多的字符。 由于rune可表示的范围更大,所以能处理一切字符,当然也包括中文字符。在平时计算中文字符,可用rune。 因此将字符串转为rune的切片,再进行翻转,完美解决。 package main import\"fmt\" func main() { src := \"2012年的第一场雪!\" fmt.Println([]rune(src)) // [50 48 49 50 24180 30340 31532 19968 22330 38634 65281] fmt.Println(string([]rune(src))) // []rune(src) dst := reverse([]rune(src)) fmt.Printf(\"%v\\n\", string(dst)) // !雪场一第的年2102 } func reverse(s []rune) []rune { for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 { s[i], s[j] = s[j], s[i] } return s }","categories":[{"name":"Go进阶","slug":"Go进阶","permalink":"http://coderedeng.github.io/categories/Go%E8%BF%9B%E9%98%B6/"}],"tags":[{"name":"Go进阶","slug":"Go进阶","permalink":"http://coderedeng.github.io/tags/Go%E8%BF%9B%E9%98%B6/"}]},{"title":"go mod包管理工具","slug":"9.go mod包管理工具","date":"2021-01-13T14:27:34.000Z","updated":"2024-01-22T12:58:59.000Z","comments":true,"path":"2021/01/13/9.go mod包管理工具/","link":"","permalink":"http://coderedeng.github.io/2021/01/13/9.go%20mod%E5%8C%85%E7%AE%A1%E7%90%86%E5%B7%A5%E5%85%B7/","excerpt":"","text":"1.go mod包管理工具01.Golang 中包的介绍和定义 包(package)是多个 Go 源码的集合,是一种高级的代码复用方案 Go 语言为我们提供了很多内置包,如 fmt、strconv、strings、sort、errors、time、encoding/json、os、io 等。 Golang 中的包可以分为三种:1、系统内置包 2、自定义包 3、第三方包 1、系统内置包 fmt、strconv、strings、sort、errors、time、encoding/json、os、io 等 2、自定义包 开发者自己写的包 3、第三方包 属于自定义包的一种,需要下载安装到本地后才可以使用 如前面给大家介绍的”github.com/shopspring/decimal”包解决 float 精度丢失问题 02.Golang包管理工具go mod 在 Golang1.11 版本之前如果我们要自定义包的话必须把项目放在 GOPATH 目录。 Go1.11 版本之后无需手动配置环境变量,使用 go mod 管理项目 也不需要非得把项目放到 GOPATH指定目录下,你可以在你磁盘的任何位置新建一个项目 Go1.13 以后可以彻底不要 GOPATH了。 2.1 go mod init 初始化项目 实际项目开发中我们首先要在我们项目目录中用 go mod 命令生成一个 go.mod 文件管理我们项目的依赖。 比如我们的 golang 项目文件要放在了 itying 这个文件夹,这个时候我们需要在 itying 文件夹 里面使用 go mod 命令生成一个 go.mod 文件 go.mod文件一旦创建后,它的内容将会被go toolchain全面掌控。 go toolchain会在各类命令执行时,比如go get、go build、go mod等修改和维护go.mod文件。 go.mod 提供了module, require、replace和exclude 四个命令 module 语句指定包的名字(路径) require 语句指定的依赖项模块 replace 语句可以替换依赖项模块 exclude 语句可以忽略依赖项模块 2.2 go mod常用命令 命令 说明 download download modules to local cache(下载依赖包) edit edit go.mod from tools or scripts(编辑go.mod) graph print module requirement graph (打印模块依赖图) verify initialize new module in current directory(在当前目录初始化mod) tidy add missing and remove unused modules(拉取缺少的模块,移除不用的模块) vendor make vendored copy of dependencies(将依赖复制到vendor下) verify verify dependencies have expected content (验证依赖是否正确) why explain why packages or modules are needed(解释为什么需要依赖) 03.Golang 中自定义包 包(package)是多个 Go 源码的集合,一个包可以简单理解为一个存放多个.go 文件的文件夹。 该文件夹下面的所有 go 文件都要在代码的第一行添加如下代码,声明该文件归属的包。 package 包名 3.1 初始化项目mkdir Demo cd Demo go mod init Demo 3.2 Demo/calc/calc.go 如果想在一个包中引用另外一个包里的标识符(如变量、常量、类型、函数等)时,该标识符必须是对外可见的(public)。 在 Go 语言中只需要将标识符的首字母大写就可以让标识符对外可见了。 package calc var aaa = \"私有变量\" //首字母小写表示私有 var Age = 20 func Add(x, y int) int { //首字母大写表示 公有方法 return x + y } func Sub(x, y int) int { //公有方法 return x - y } 3.3 Demo/main.gopackage main import ( c \"Demo/calc\" // c是取的别名 \"fmt\" ) func main() { sum := c.Add(10, 2) fmt.Println(sum) //12 sub := c.Sub(10, 2) fmt.Println(sub) // 8 } func init() { //main包中init函数 优先于 main函数 fmt.Println(\"main init...\") } /* main init... 12 8 */ 04.init()初始化函数4.1 init()函数介绍 在Go语言程序执行时导入包语句会自动触发包内部 init()函数的调用。 需要注意的是:init()函数没有参数也没有返回值。 init()函数在程序运行时自动被调用执行,不能在代码中主动调用它。 4.2 init()函数执行顺序 Go 语言包会从 main 包开始检查其导入的所有包,每个包中又可能导入了其他的包。 Go 编译器由此构建出一个树状的包引用关系,再根据引用顺序决定编译顺序,依次编译这些包的代码。 在运行时,被最后导入的包会最先初始化并调用其 init()函数, 如下图示 05.Golang中使用第三方包5.1 查找golang的第三方包 我们可以在 https://pkg.go.dev/[ (opens new window)](https://pkg.go.dev/)查找看常见的 golang 第三方包 5.2 安装这个包 第一种方法:go get 包名称 (全局) go get github.com/shopspring/decimal 第二种方法:go mod download (全局) 依赖包会自动下载到$GOPATH/pkg/mod,多个项目可以共享缓存的 mod 注意使用 go mod download 的时候首先需要在你的项目里面引入第三方包 go mod download 第三种方法:go mod vendor 将依赖复制到当前项目的 vendor 下 (本项目) 将依赖复制到当前项目的 vendor 下 注意:使用 go mod vendor 的时候首先需要在你的项目里面引入第三方包 go mod vendor","categories":[{"name":"Go基础","slug":"Go基础","permalink":"http://coderedeng.github.io/categories/Go%E5%9F%BA%E7%A1%80/"}],"tags":[{"name":"Go基础","slug":"Go基础","permalink":"http://coderedeng.github.io/tags/Go%E5%9F%BA%E7%A1%80/"}]},{"title":"流程控制","slug":"8.流程控制","date":"2021-01-12T14:11:24.000Z","updated":"2024-01-22T12:58:59.000Z","comments":true,"path":"2021/01/12/8.流程控制/","link":"","permalink":"http://coderedeng.github.io/2021/01/12/8.%E6%B5%81%E7%A8%8B%E6%8E%A7%E5%88%B6/","excerpt":"","text":"1.流程控制01.if else(分支结构)1.1 if 条件判断基本写法package main import ( \"fmt\" ) func main() { score := 65 if score >= 90 { fmt.Println(\"A\") } else if score > 75 { fmt.Println(\"B\") } else { fmt.Println(\"C\") } } 1.2 if 条件判断特殊写法 if 条件判断还有一种特殊的写法,可以在 if 表达式之前添加一个执行语句,再根据变量值进行判断 package main import \"fmt\" func main() { //这里的 score 是局部作用域 if score := 56; score >= 90 { fmt.Println(\"A\") } else if score > 75 { fmt.Println(\"B\") }else { fmt.Println(\"C\") fmt.Println(score) // 只能在函数内部打印 score } // fmt.Println(score) //undefined: score } 02.for(循环结构)2.1 for循环 1)普通for循环 package main import \"fmt\" func main() { // 打印: 0 ~ 9 的数字 for i := 0; i < 10; i++ { fmt.Println(i) } } 2)外部定义 i package main import \"fmt\" func main() { i := 0 for i < 10 { fmt.Println(i) i++ } } 3)省略初始语句 package main import \"fmt\" func main() { i := 0 for ; i < 10; i++ { fmt.Println(i) } } 2.2 打印 0-10 所有的偶数package main import \"fmt\" func main() { // 0 2 4 6 8 for i := 0; i < 10; i++ { if i%2 == 0 { fmt.Println(i) } } } 2.3 嵌套循环package main import \"fmt\" func main() { for i := 1; i <= 9; i++ { for j := 1; j <= i; j++ { fmt.Printf(\"%vx%v=%v \\t\", i, j, i*j) } fmt.Println() } } /* 1x1=1 2x1=2 2x2=4 3x1=3 3x2=6 3x3=9 4x1=4 4x2=8 4x3=12 4x4=16 5x1=5 5x2=10 5x3=15 5x4=20 5x5=25 6x1=6 6x2=12 6x3=18 6x4=24 6x5=30 6x6=36 7x1=7 7x2=14 7x3=21 7x4=28 7x5=35 7x6=42 7x7=49 8x1=8 8x2=16 8x3=24 8x4=32 8x5=40 8x6=48 8x7=56 8x8=64 9x1=9 9x2=18 9x3=27 9x4=36 9x5=45 9x6=54 9x7=63 9x8=72 9x9=81 */ 2.4 模拟while循环 Go 语言中是没有 while 语句的,我们可以通过 for 代替 package main import \"fmt\" func main() { k := 1 for { // 这里也等价 for ; ; { if k <= 10 { fmt.Println(\"ok~~\", k) } else { break //break 就是跳出这个 for 循环 } k++ } } 2.5 for range(键值循环)package main import \"fmt\" func main() { str := \"abc上海\" for index, val := range str { fmt.Printf(\"索引=%d, 值=%c \\n\", index, val) } } /* 索引=0, 值=a 索引=1, 值=b 索引=2, 值=c 索引=3, 值=上 索引=6, 值=海 */ 03.switch case 使用 switch 语句可方便地对大量的值进行条件判断 3.1 case一般用法package main import \"fmt\" func main() { score := \"B\" switch score { case \"A\": fmt.Println(\"非常棒\") case \"B\": fmt.Println(\"优秀\") case \"C\": fmt.Println(\"及格\") default: fmt.Println(\"不及格\") } } 3.2 case语句多个值package main import \"fmt\" func main() { n := 2 switch n { case 1, 3, 5, 7, 9: fmt.Println(\"奇数\") case 2, 4, 6, 8: fmt.Println(\"偶数\") default: fmt.Println(n) } } 3.3 fallthrough 语法 fallthrough 语法可以执行满足条件的 case 的下一个 case,是为了兼容 C 语言中的 case 设计 package main import \"fmt\" func main() { s := \"a\" switch { case s == \"a\": fmt.Println(\"a\") fallthrough case s == \"b\": fmt.Println(\"b\") case s == \"c\": fmt.Println(\"c\") default: fmt.Println(\"...\") } } /* a b */ 04.break、continue、goto4.1 break跳出单循环package main import \"fmt\" func main() { k := 1 for { // 这里也等价 for ; ; { if k <= 10 { fmt.Println(\"ok~~\", k) } else { break //break 就是跳出这个 for 循环 } k++ } } 4.2 跳出多重循环 在多重循环中,可以用标号 label 标出想 break 的循环 package main import \"fmt\" func main() { lable2: for i := 0; i < 2; i++ { for j := 0; j < 10; j++ { if j == 2 { break lable2 } fmt.Println(\"i j 的值:\", i, \"-\", j) } } } /* i j 的值: 0 - 0 i j 的值: 0 - 1 */ 4.3 continue(继续下次循环)package main import \"fmt\" func main() { for i := 0; i < 2; i++ { for j := 0; j < 4; j++ { if j == 2 { continue } fmt.Println(\"i j 的值\", i, \"-\", j) } } } /* i j 的值 0 - 0 i j 的值 0 - 1 i j 的值 0 - 3 i j 的值 1 - 0 i j 的值 1 - 1 i j 的值 1 - 3 */ 4.4 goto(跳转到指定标签)package main import \"fmt\" func main() { for i := 0; i < 10; i++ { for j := 0; j < 10; j++ { if j == 2 { goto breakTag // // 设置退出标签 } fmt.Printf(\"%v-%v\\n\", i, j) } } return breakTag: // 标签 fmt.Println(\"结束 for 循环\") } /* 0-0 0-1 结束 for 循环 */","categories":[{"name":"Go基础","slug":"Go基础","permalink":"http://coderedeng.github.io/categories/Go%E5%9F%BA%E7%A1%80/"}],"tags":[{"name":"Go基础","slug":"Go基础","permalink":"http://coderedeng.github.io/tags/Go%E5%9F%BA%E7%A1%80/"}]},{"title":"interface接口","slug":"7.interface接口","date":"2021-01-11T13:14:52.000Z","updated":"2024-01-22T12:58:59.000Z","comments":true,"path":"2021/01/11/7.interface接口/","link":"","permalink":"http://coderedeng.github.io/2021/01/11/7.interface%E6%8E%A5%E5%8F%A3/","excerpt":"","text":"1.interface接口01.Golang接口的定义1.1 Golang 中的接口 在Go语言中接口(interface)是一种类型,一种抽象的类型。 接口(interface)定义了一个对象的行为规范,只定义规范不实现,由具体的对象来实现规范的细节。 实现接口的条件 一个对象只要全部实现了接口中的方法,那么就实现了这个接口。 换句话说,接口就是一个需要实现的方法列表。 1.2 定义一个Usber接口 定义一个 Usber 接口让 Phone 和 Camera 结构体实现这个接口 package main import \"fmt\" //1.接口是一个规范 type Usber interface { start() stop() } //2.如果接口里面有方法的话,必要要通过结构体或者通过自定义类型实现这个接口 type Phone struct { Name string } //3.手机要实现usb接口的话必须得实现usb接口中的所有方法 func (p Phone) start() { fmt.Println(p.Name, \"启动\") } func (p Phone) stop() { fmt.Println(p.Name, \"关机\") } func main() { p := Phone{ Name: \"华为手机\", } var p1 Usber //golang中接口就是一个数据类型 p1 = p //表示手机实现Usb接口 p1.start() p1.stop() } /* 华为手机 启动 华为手机 关机 */ 02.空接口2.1 空接口说明 golang中空接口也可以直接当做类型来使用,可以表示任意类型 Golang 中的接口可以不定义任何方法,没有定义任何方法的接口就是空接口。 空接口表示没有任何约束,因此任何类型变量都可以实现空接口。 空接口在实际项目中用的是非常多的,用空接口可以表示任意数据类型。 2.2 空接口作为函数的参数package main import \"fmt\" //空接口作为函数的参数 func show(a interface{}) { fmt.Printf(\"值:%v 类型:%T\\n\", a, a) } func main() { show(20) // 值:20 类型:int show(\"你好golang\") // 值:你好golang 类型:string slice := []int{1, 2, 34, 4} show(slice) // 值:[1 2 34 4] 类型:[]int } 2.3 切片实现空接口package main import \"fmt\" func main() { var slice = []interface{}{\"张三\", 20, true, 32.2} fmt.Println(slice) // [张三 20 true 32.2] } 2.4 map 的值实现空接口package main import \"fmt\" func main() { // 空接口作为 map 值 var studentInfo = make(map[string]interface{}) studentInfo[\"name\"] = \"张三\" studentInfo[\"age\"] = 18 studentInfo[\"married\"] = false fmt.Println(studentInfo) // [age:18 married:false name:张三] } 03.类型断言 一个接口的值(简称接口值)是由一个具体类型和具体类型的值两部分组成的。 这两部分分别称为接口的动态类型和动态值。 如果我们想要判断空接口中值的类型,那么这个时候就可以使用类型断言 其语法格式:x.(T) x : 表示类型为 interface{}的变量 T : 表示断言 x 可能是的类型 package main import \"fmt\" func main() { var x interface{} x = \"Hello golnag\" v, ok := x.(string) if ok { fmt.Println(v) }else { fmt.Println(\"非字符串类型\") } } 04.值接收者和指针接收者4.1 值接收者 如果结构体中的方法是值接收者,那么实例化后的结构体值类型和结构体指针类型都可以赋值给接口变量 package main import \"fmt\" type Usb interface { Start() Stop() } type Phone struct { Name string } func (p Phone) Start() { fmt.Println(p.Name, \"开始工作\") } func (p Phone) Stop() { fmt.Println(\"phone 停止\") } func main() { phone1 := Phone{ Name: \"小米手机\", } var p1 Usb = phone1 //phone1 实现了 Usb 接口 phone1 是 Phone 类型 p1.Start() phone2 := &Phone{ //小米手机 开始工作 Name: \"苹果手机\", } var p2 Usb = phone2 //phone2 实现了 Usb 接口 phone2 是 *Phone 类型 p2.Start() //苹果手机 开始工作 } 4.2 指针接收者 如果结构体中的方法是指针接收者,那么实例化后结构体指针类型都可以赋值给接口变量,结构体值类型没法赋值给接口变量。 package main import \"fmt\" type Usb interface { Start() Stop() } type Phone struct { Name string } func (p *Phone) Start() { fmt.Println(p.Name, \"开始工作\") } func (p *Phone) Stop() { fmt.Println(\"phone 停止\") } func main() { /*错误写法 phone1 := Phone{ Name: \"小米手机\", } var p1 Usb = phone1 p1.Start() */ //正确写法 phone2 := &Phone{ Name: \"苹果手机\", } var p2 Usb = phone2 //phone2 实现了 Usb 接口 phone2 是 *Phone 类型 p2.Start() //苹果手机 开始工作 } 05.一个结构体实现多个接口 Golang 中一个结构体也可以实现多个接口 package main import \"fmt\" type AInterface interface { GetInfo() string } type BInterface interface { SetInfo(string, int) } type People struct { Name string Age int } func (p People) GetInfo() string { return fmt.Sprintf(\"姓名:%v 年龄:%d\", p.Name, p.Age) } func (p *People) SetInfo(name string, age int) { p.Name = name p.Age = age } func main() { var people = &People{ Name: \"张三\", Age: 20, } // people 实现了 AInterface 和 BInterface var p1 AInterface = people var p2 BInterface = people fmt.Println(p1.GetInfo()) p2.SetInfo(\"李四\", 30) // 姓名:张三 年龄:20 fmt.Println(p1.GetInfo()) // 姓名:李四 年龄:30 } 06.接口嵌套 接口与接口间可以通过嵌套创造出新的接口。 package main import \"fmt\" type SayInterface interface { say() } type MoveInterface interface { move() } // 接口嵌套 type Animal interface { SayInterface MoveInterface } type Cat struct { name string } func (c Cat) say() { fmt.Println(\"喵喵喵\") } func (c Cat) move() { fmt.Println(\"猫会动\") } func main() { var x Animal x = Cat{name: \"花花\"} x.move() // 猫会动 x.say() // 喵喵喵 }","categories":[{"name":"Go基础","slug":"Go基础","permalink":"http://coderedeng.github.io/categories/Go%E5%9F%BA%E7%A1%80/"}],"tags":[{"name":"Go基础","slug":"Go基础","permalink":"http://coderedeng.github.io/tags/Go%E5%9F%BA%E7%A1%80/"}]},{"title":"结构体","slug":"6.结构体","date":"2021-01-10T13:14:17.000Z","updated":"2024-01-22T12:58:59.000Z","comments":true,"path":"2021/01/10/6.结构体/","link":"","permalink":"http://coderedeng.github.io/2021/01/10/6.%E7%BB%93%E6%9E%84%E4%BD%93/","excerpt":"","text":"1.结构体01.结构体基础1.1 什么是结构体 Go语言中没有“类”的概念,也不支持“类”的继承等面向对象的概念。 Go语言中通过结构体的内嵌再配合接口比面向对象具有更高的扩展性和灵活性。 1.2 自定义类型 在 Go 语言中有一些基本的数据类型,如 string、整型、浮点型、布尔等数据类型 Go 语言中可以使用 type 关键字来定义自定义类型。 将 myInt 定义为 int 类型,通过 type 关键字的定义,myInt 就是一种新的类型,它具有 int 的特性 type myInt int 1.3 类型别名 Golang1.9 版本以后添加的新功能。 类型别名规定:TypeAlias 只是 Type 的别名,本质上 TypeAlias 与 Type 是同一个类型 就像 一个孩子小时候有大名、小名、英文名,但这些名字都指的是他本人。 type TypeAlias = Type 1.4 自定义类型和类型别名的区别 类型别名与自定义类型表面上看只有一个等号的差异 结果显示 a 的类型是 main.newInt,表 示 main 包下定义的 newInt 类型。 b 的类型是 int 类型。 package main import \"fmt\" type newInt int //类型定义 type myInt = int //类型别名 func main() { var a newInt var b myInt fmt.Printf(\"type of a:%T\\n\", a) //type of a:main.newInt fmt.Printf(\"type of b:%T\\n\", b) //type of b:int } 02.结构体定义2.1 基本实例化(法1) 只有当结构体实例化时,才会真正地分配内存,也就是必须实例化后才能使用结构体的字段。 结构体本身也是一种类型,我们可以像声明内置类型一样使用 var 关键字声明结构体类型 package main import \"fmt\" type person struct { name string city string age int } func main() { var p1 person p1.name = \"张三\" p1.city = \"北京\" p1.age = 18 fmt.Printf(\"p1=%v\\n\", p1) // p1={张三 北京 18} fmt.Printf(\"p1=%#v\\n\", p1) // p1=main.person{name:\"张三\", city:\"北京\", age:18} } 2.2 new实例化(法2) 我们还可以通过使用 new 关键字对结构体进行实例化,得到的是结构体的地址 从打印的结果中我们可以看出 p2 是一个结构体指针。 注意:在 Golang 中支持对结构体指针直接使用.来访问结构体的成员。 p2.name = "张三" 其实在底层是 (*p2).name = "张三" package main import \"fmt\" type person struct { name string city string age int } func main() { var p2 = new(person) p2.name = \"张三\" p2.age = 20 p2.city = \"北京\" fmt.Printf(\"%T\\n\", p2) //*main.person fmt.Printf(\"p2=%#v\\n\", p2) //p2=&main.person{name:\"张三\", city:\"北京\", age:20} } xxxxxxxxxx package mainimport ( “fmt” “sort”)func main() { // 第一:生成字典,scoreMap var scoreMap = make(map[string]int, 200) for i := 0; i < 10; i++ { key := fmt.Sprintf(“stu%02d”, i) //生成 stu 开头的字符串 scoreMap[key] = i } // 第二:取出 map 中的所有 key 存入切片 keys var keys = make([]string, 0, 200) for key := range scoreMap { keys = append(keys, key) } // 第三:对切片进行排序 sort.Strings(keys) // 第四:按照排序后的 key 遍历 map for _, key := range keys { fmt.Println(key, scoreMap[key]) }}/*stu00 0stu01 1stu02 2stu03 3stu04 4stu05 5stu06 6stu07 7stu08 8stu09 9 */go 使用&对结构体进行取地址操作相当于对该结构体类型进行了一次 new 实例化操作 package main import \"fmt\" type person struct { name string city string age int } func main() { p3 := &person{} fmt.Printf(\"%T\\n\", p3) //*main.person fmt.Printf(\"p3=%#v\\n\", p3) //p3=&main.person{name:\"\", city:\"\", age:0} p3.name = \"zhangsan\" p3.age = 30 p3.city = \"深圳\" (*p3).age = 40 //这样也是可以的 fmt.Printf(\"p3=%#v\\n\", p3) //p3=&main.person{name:\"zhangsan\", city:\"深圳\", age:30} } 2.4 键值对初始化(法4) 注意:最后一个属性的,要加上 package main import \"fmt\" type person struct { name string city string age int } func main() { p4 := person{ name: \"zhangsan\", city: \"北京\", age: 18, } // p4=main.person{name:\"zhangsan\", city:\"北京\", age:18} fmt.Printf(\"p4=%#v\\n\", p4) } 2.5 值列表初始化(法5) 初始化结构体的时候可以简写,也就是初始化的时候不写键,直接写值 必须初始化结构体的所有字段。 初始值的填充顺序必须与字段在结构体中的声明顺序一致。 该方式不能和键值初始化方式混用 package main import \"fmt\" type person struct { name string city string age int } func main() { // 初始化结构体的时候可以简写,也就是初始化的时候不写键,直接写值 p7 := &person{ \"zhangsan\", \"北京\", 28, } // p7=&main.person{name:\"zhangsan\", city:\"北京\", age:28} fmt.Printf(\"p7=%#v\\n\", p7) } 2.6 结构体的匿名字段 结构体允许其成员字段在声明时没有字段名而只有类型,这种没有名字的字段就称为匿名字段。 匿名字段默认采用类型名作为字段名,结构体要求字段名称必须唯一,因此一个结构体中同种类型的匿名字段只能有一个。 package main import \"fmt\" type Person struct { //Person 结构体 Person 类型 string int } func main() { p1 := Person{ \"小王子\", 18, } fmt.Printf(\"%#v\\n\", p1) //main.Person{string:\"北京\", int:18} fmt.Println(p1.string, p1.int) //北京 18 } 03.嵌套结构体3.1 普通嵌套结构体 一个结构体中可以嵌套包含另一个结构体或结构体指针。 package main import \"fmt\" type Address struct { //Address 地址结构体 Province string City string } type User struct { //User 用户结构体 Name string Gender string Address Address } func main() { user1 := User{ Name: \"张三\", Gender: \"男\", Address: Address{ Province: \"广东\", City: \"深圳\", }, } fmt.Printf(\"user1=%#v\\n\", user1) //user1=main.User{Name:\" 张 三 \", Gender:\" 男 \", Address:main.Address{Province:\"广东\", City:\"深圳\"}} } 3.2 嵌套匿名结构体 注意:当访问结构体成员时会先在结构体中查找该字段,找不到再去匿名结构体中查找。 package main import \"fmt\" type Address struct { //Address 地址结构体 Province string City string } type User struct { //User 用户结构体 Name string Gender string Address } func main() { var user2 User user2.Name = \"张三\" user2.Gender = \"男\" user2.Address.Province = \"广东\" //通过匿名结构体.字段名访问 user2.City = \"深圳\" //直接访问匿名结构体的字段名 fmt.Printf(\"user2=%#v\\n\", user2) //user2=main.User{Name:\"张三\", Gender:\"男\", Address:main.Address{Province:\"广东\", City:\"深圳\"}} } 04.结构体方法和接收者4.1 结构体说明 在 go 语言中,没有类的概念但是可以给类型(结构体,自定义类型)定义方法。 所谓方法就是定义了接收者的函数。 Go语言中的方法(Method)是一种作用于特定类型变量的函数。 这种特定类型变量叫做接收者(Receiver)。 接收者的概念就类似于其他语言中的this或者 self。 方法的定义格式如下: func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) { 函数体 } 给结构体 Person 定义一个方法打印 Person 的信息 4.2 结构体方法和接收者package main import \"fmt\" type Person struct { name string age int8 } func (p Person) printInfo() { fmt.Printf(\"姓名:%v 年龄:%v\", p.name, p.age) // 姓名:小王子 年龄:25 } func main() { p1 := Person{ name: \"小王子\", age: 25, } p1.printInfo() // 姓名:小王子 年龄:25 } 4.3 值类型和指针类型接收者 实例1:给结构体 Person 定义一个方法打印 Person 的信息 1、值类型的接收者 当方法作用于值类型接收者时,Go 语言会在代码运行时将接收者的值复制一份。 在值类型接收者的方法中可以获取接收者的成员值,但修改操作只是针对副本,无法修改接收者变量本身。 2、指针类型的接收者 指针类型的接收者由一个结构体的指针组成 由于指针的特性,调用方法时修改接收者指针的任意成员变量,在方法结束后,修改都是有效的。 这种方式就十分接近于其他语言中面向对象中的 this 或者 self。 package main import \"fmt\" type Person struct { name string age int } //值类型接受者 func (p Person) printInfo() { fmt.Printf(\"姓名:%v 年龄:%v\\n\", p.name, p.age) // 姓名:小王子 年龄:25 } //指针类型接收者 func (p *Person) setInfo(name string, age int) { p.name = name p.age = age } func main() { p1 := Person{ name: \"小王子\", age: 25, } p1.printInfo() // 姓名:小王子 年龄:25 p1.setInfo(\"张三\", 20) p1.printInfo() // 姓名:张三 年龄:20 } 05.结构体继承 Go 语言中使用结构体也可以实现其他编程语言中的继承 5.1 普通传值package main import \"fmt\" type Animal struct { //Animal 动物 name string } func (a *Animal) run() { fmt.Printf(\"%s 会运动!\\n\", a.name) } type Dog struct { //Dog狗 Age int8 Animal // 通过嵌套匿名结构体实现继承 } func (d Dog) wang() { fmt.Printf(\"%s 会汪汪汪~\\n\", d.name) } func main() { d1 := Dog{ Age: 4, Animal: Animal{ //注意嵌套的是结构体指针 name: \"阿奇\", }, } d1.wang() //乐乐会汪汪汪~ d1.run() //乐乐会动! } 5.2 指针传值package main import \"fmt\" type Animal struct { //Animal 动物 name string } func (a *Animal) run() { fmt.Printf(\"%s 会运动!\\n\", a.name) } type Dog struct { //Dog狗 Age int8 *Animal //通过嵌套匿名结构体实现继承 } func (d *Dog) wang() { fmt.Printf(\"%s 会汪汪汪~\\n\", d.name) } func main() { d1 := &Dog{ Age: 4, Animal: &Animal{ //注意嵌套的是结构体指针 name: \"阿奇\", }, } d1.wang() //乐乐会汪汪汪~ d1.run() //乐乐会动! } 06.给任意类型添加方法 在 Go 语言中,接收者的类型可以是任何类型,不仅仅是结构体,任何类型都可以拥有方法。 举个例子,我们基于内置的 int 类型使用 type 关键字可以定义新的自定义类型,然后为我们的自定义类型添加方法。 注意事项: 非本地类型不能定义方法,也就是说我们不能给别的包的类型定义方法。 package main import \"fmt\" type myInt int func (m myInt) SayHello() { fmt.Println(\"Hello, 我是一个 int。\") } func main() { var m1 myInt m1.SayHello() //Hello, 我是一个 int。 m1 = 100 fmt.Printf(\"%#v %T\\n\", m1, m1) //100 main.MyInt }","categories":[{"name":"Go基础","slug":"Go基础","permalink":"http://coderedeng.github.io/categories/Go%E5%9F%BA%E7%A1%80/"}],"tags":[{"name":"Go基础","slug":"Go基础","permalink":"http://coderedeng.github.io/tags/Go%E5%9F%BA%E7%A1%80/"}]},{"title":"指针","slug":"5.指针","date":"2021-01-09T12:32:14.000Z","updated":"2024-01-22T12:58:59.000Z","comments":true,"path":"2021/01/09/5.指针/","link":"","permalink":"http://coderedeng.github.io/2021/01/09/5.%E6%8C%87%E9%92%88/","excerpt":"","text":"1.指针01.关于指针要搞明白 Go 语言中的指针需要先知道 3 个概念:指针地址、指针类型、指针取值 指针地址(&a) 指针取值(*&a) 指针类型(&a) —> *int 改变数据传指针 变量的本质是给存储数据的内存地址起了一个好记的别名。 比如我们定义了一个变量 a := 10 ,这个时候可以直接通过 a 这个变量来读取内存中保存的 10 这个值。 在计算机底层 a 这个变量其实对应了一个内存地址。 指针也是一个变量,但它是一种特殊的变量,它存储的数据不是一个普通的值,而是另一个变量的内存地址。 Go 语言中的指针操作非常简单,我们只需要记住两个符号:&(取地址)和 *(根据地址取值) package main import \"fmt\" func main() { var a = 10 fmt.Printf(\"%d \\n\",&a) // &a 指针地址 (824633761976) fmt.Printf(\"%d \\n\",*&a) // *&a 指针取值 (10) fmt.Printf(\"%T \\n\",&a) // %T 指针类型 (*int ) } 02.&取变量地址2.1 &符号取地址操作 每个变量在运行时都拥有一个地址,这个地址代表变量在内存中的位置。 Go 语言中使用&字符放在变量前面对变量进行取地址操作。 Go 语言中的值类型(int、float、bool、string、array、struct)都有对应的指针类型 取变量指针的语法如下: ptr := &v // 比如 v 的类型为 T 1 v : 代表被取地址的变量,类型为 T ptr : 用于接收地址的变量,ptr 的类型就为T,称做 T 的指针类型。代表指针。 package main import \"fmt\" func main() { var a = 10 var b = &a fmt.Printf(\"a:%d ptr:%p\\n\", a, &a) // a:10 ptr:0xc0000100a8 fmt.Printf(\"b:%v type:%T\\n\", b, b) // b:0xc0000100a8 type:*int fmt.Println(\"取 b 的地址:\", &b) // 取 b 的地址: 0xc000006028 } 2.2 b := &a 的图示 03.指针修改数据3.1 *指针取值 在对普通变量使用&操作符取地址后会获得这个变量的指针,然后可以对指针使用操作,也就是指针取值 package main import \"fmt\" func main() { a := 10 b := &a // 取变量 a 的地址,将地址保存到指针 b 中 fmt.Printf(\"type of b:%T\\n\", b) // type of b:*int c := *b // 指针取值(根据指针的值去内存取值) fmt.Printf(\"type of c:%T\\n\", c) // type of c:int fmt.Printf(\"value of c:%v\\n\", c) // value of c:10 } 变量、指针地址、指针变量、取地址、取值的相互关系和特性如下: xxxxxxxxxx package mainimport ( “fmt” “sort”)func main() { // 第一:生成字典,scoreMap var scoreMap = make(map[string]int, 200) for i := 0; i < 10; i++ { key := fmt.Sprintf(“stu%02d”, i) //生成 stu 开头的字符串 scoreMap[key] = i } // 第二:取出 map 中的所有 key 存入切片 keys var keys = make([]string, 0, 200) for key := range scoreMap { keys = append(keys, key) } // 第三:对切片进行排序 sort.Strings(keys) // 第四:按照排序后的 key 遍历 map for _, key := range keys { fmt.Println(key, scoreMap[key]) }}/*stu00 0stu01 1stu02 2stu03 3stu04 4stu05 5stu06 6stu07 7stu08 8stu09 9 */go 指针变量的值是指针地址。 对指针变量进行取值(*)操作,可以获得指针变量指向的原变量的值。 3.2 指针传值示例package main import \"fmt\" func modify1(x int) { x = 100 } func modify2(x *int) { *x = 100 } func main() { a := 10 modify1(a) fmt.Println(a) // 10 modify2(&a) fmt.Println(a) // 100 } 04.new 和 make4.0 执行报错 执行下面的代码会引发 panic,为什么呢? 在 Go 语言中对于引用类型的变量,我们在使用的时候不仅要声明它,还要为它分配内存空间,否则我们的值就没办法存储。 而对于值类型的声明不需要分配内存空间,是因为它们在声明的时候已经默认分配好了内存空间。 要分配内存,就引出来今天的 new 和 make。 Go 语言中 new 和 make 是内建的两个函数,主要用来分配内存。 package main import \"fmt\" func main() { var userinfo map[string]string userinfo[\"username\"] = \"张三\" fmt.Println(userinfo) } /* panic: assignment to entry in nil map */ 4.1 make和new比较 new 和 make 是两个内置函数,主要用来创建并分配类型的内存。 make和new区别 make 关键字的作用是创建于 slice、map 和 channel 等内置的数据结构 new 的作用是为类型申请一片内存空间,并返回指向这片内存的指针 package main import \"fmt\" func main() { a := make([]int, 3, 10) // 切片长度为 1,预留空间长度为 10 a = append(a,1) fmt.Printf(\"%v--%T \\n\",a,a) // [0 0 0]--[]int 值----切片本身 var b = new([]int) //b = b.append(b,2) // 返回的是内存指针,所以不能直接 append *b = append(*b, 3) // 必须通过 * 指针取值,才能进行 append 添加 fmt.Printf(\"%v--%T\",b,b) // &[]--*[]string 内存的指针---内存指针 } 4.2 new函数 一:系统默认的数据类型,分配空间 package main import \"fmt\" func main() { // 1.new实例化int age := new(int) *age = 1 // 2.new实例化切片 li := new([]int) *li = append(*li, 1) // 3.实例化map userinfo := new(map[string]string) *userinfo = map[string]string{} (*userinfo)[\"username\"] = \"张三\" fmt.Println(userinfo) // &map[username:张三] } 二:自定义类型使用 new 函数来分配空间 package main import \"fmt\" func main() { var s *Student s = new(Student) //分配空间 s.name =\"zhangsan\" fmt.Println(s) // &{zhangsan 0} } type Student struct { name string age int } 4.3 make函数 make 也是用于内存分配的,但是和 new 不同,它只用于 chan、map 以及 slice 的内存创建 而且它返回的类型就是这三个类型本身,而不是他们的指针类型 因为这三种类型就是引用类型,所以就没有必要返回他们的指针了 package main import \"fmt\" func main() { a := make([]int, 3, 10) // 切片长度为 1,预留空间长度为 10 b := make(map[string]string) c := make(chan int, 1) fmt.Println(a,b,c) // [0 0 0] map[] 0xc0000180e0 } 当我们为slice分配内存的时候,应当尽量预估到slice可能的最大长度 通过给make传第三个参数的方式来给slice预留好内存空间 这样可以避免二次分配内存带来的开销,大大提高程序的性能。","categories":[{"name":"Go基础","slug":"Go基础","permalink":"http://coderedeng.github.io/categories/Go%E5%9F%BA%E7%A1%80/"}],"tags":[{"name":"Go基础","slug":"Go基础","permalink":"http://coderedeng.github.io/tags/Go%E5%9F%BA%E7%A1%80/"}]},{"title":"Map","slug":"4.Map","date":"2021-01-08T13:46:37.000Z","updated":"2024-01-22T12:58:59.000Z","comments":true,"path":"2021/01/08/4.Map/","link":"","permalink":"http://coderedeng.github.io/2021/01/08/4.Map/","excerpt":"","text":"1.map01.map介绍1.1 map说明 map 是一种无序的基于 key-value 的数据结构,Go 语言中的 map 是引用类型,必须初始化才能使用。 Go 语言中 map 的定义语法如下:map[KeyType]ValueType 其中: KeyType:表示键的类型。 ValueType:表示键对应的值的类型。 map 类型的变量默认初始值为 nil,需要使用 make()函数来分配内存。 其中 cap 表示 map 的容量,该参数虽然不是必须的。 注意:获取 map 的容量不能使用 cap, cap 返回的是数组切片分配的空间大小, 根本不能用于map。 要获取 map 的容量,可以用 len 函数。 02.定义map2.1 map定义:法1package main import ( \"fmt\" ) func main() { scoreMap := make(map[string]int, 8) scoreMap[\"张三\"] = 90 scoreMap[\"小明\"] = 100 fmt.Println(scoreMap) // map[小明:100 张三:90] fmt.Println(scoreMap[\"小明\"]) // 100 fmt.Printf(\"type of a:%T\\n\", scoreMap) // type of a:map[string]int } 2.2 map定义:法2package main import ( \"fmt\" ) func main() { userInfo := map[string]string{ \"username\": \"IT 营小王子\", \"password\": \"123456\", } fmt.Println(userInfo) // map[password:123456 username:IT 营小王子] } 2.3 map嵌套mappackage main import \"fmt\" func main() { var mapSlice = make([]map[string]string, 3) for index, value := range mapSlice { fmt.Printf(\"index:%d value:%v\\n\", index, value) } fmt.Println(\"#################### after init ################\") // 对切片中的 map 元素进行初始化 mapSlice[0] = make(map[string]string, 10) mapSlice[0][\"name\"] = \"小王子\" mapSlice[0][\"password\"] = \"123456\" mapSlice[0][\"address\"] = \"海淀区\" for index, value := range mapSlice { fmt.Printf(\"index:%d value:%v\\n\", index, value) } } /* index:0 value:map[] index:1 value:map[] index:2 value:map[] #################### after init ################ index:0 value:map[address:海淀区 name:小王子 password:123456] index:1 value:map[] index:2 value:map[] */ 03.map基本使用3.1 判断某个键是否存在package main import ( \"fmt\" ) func main() { userInfo := map[string]string{ \"username\": \"zhangsan\", \"password\": \"123456\", } v, ok := userInfo[\"username\"] if ok { fmt.Println(v) // zhangsan }else { fmt.Println(\"map中没有此元素\") } } 3.2 delete()函数 使用 delete()内建函数从 map 中删除一组键值对,delete()函数的格式如下:delete(map 对象, key) 其中, map 对象:表示要删除键值对的 map 对象 key:表示要删除的键值对的键 package main import ( \"fmt\" ) func main() { scoreMap := make(map[string]int) scoreMap[\"张三\"] = 90 scoreMap[\"小明\"] = 100 scoreMap[\"娜扎\"] = 60 delete(scoreMap, \"小明\") //将小明:100 从 map 中删除 for k,v := range scoreMap{ fmt.Println(k, v) } } /* 娜扎 60 张三 90 */ 04.map遍历4.1 遍历key和valuepackage main import ( \"fmt\" ) func main() { scoreMap := make(map[string]int) scoreMap[\"张三\"] = 90 scoreMap[\"小明\"] = 100 scoreMap[\"娜扎\"] = 60 for k, v := range scoreMap { fmt.Println(k, v) } } /* 张三 90 小明 100 娜扎 60 */ 4.2 只遍历Key 注意: 遍历 map 时的元素顺序与添加键值对的顺序无关 package main import ( \"fmt\" ) func main() { scoreMap := make(map[string]int) scoreMap[\"张三\"] = 90 scoreMap[\"小明\"] = 100 scoreMap[\"娜扎\"] = 60 for k := range scoreMap { fmt.Println(k) } } /* 张三 小明 娜扎 */ 4.3 顺序遍历mappackage main import ( \"fmt\" \"sort\" ) func main() { // 第一:生成字典,scoreMap var scoreMap = make(map[string]int, 200) for i := 0; i < 10; i++ { key := fmt.Sprintf(\"stu%02d\", i) //生成 stu 开头的字符串 scoreMap[key] = i } // 第二:取出 map 中的所有 key 存入切片 keys var keys = make([]string, 0, 200) for key := range scoreMap { keys = append(keys, key) } // 第三:对切片进行排序 sort.Strings(keys) // 第四:按照排序后的 key 遍历 map for _, key := range keys { fmt.Println(key, scoreMap[key]) } } /* stu00 0 stu01 1 stu02 2 stu03 3 stu04 4 stu05 5 stu06 6 stu07 7 stu08 8 stu09 9 */","categories":[{"name":"Go基础","slug":"Go基础","permalink":"http://coderedeng.github.io/categories/Go%E5%9F%BA%E7%A1%80/"}],"tags":[{"name":"Go基础","slug":"Go基础","permalink":"http://coderedeng.github.io/tags/Go%E5%9F%BA%E7%A1%80/"}]},{"title":"切片","slug":"3.切片","date":"2021-01-07T15:53:43.000Z","updated":"2024-01-22T12:58:59.000Z","comments":true,"path":"2021/01/07/3.切片/","link":"","permalink":"http://coderedeng.github.io/2021/01/07/3.%E5%88%87%E7%89%87/","excerpt":"","text":"1.切片01.切片基础1.1 切片的定义 切片(Slice)是一个拥有相同类型元素的可变长度的序列。 它是基于数组类型做的一层封装。 它非常灵活,支持自动扩容。 切片是一个引用类型,它的内部结构包含地址、长度和容量。 声明切片类型的基本语法如下: // var name []T // 1、name:表示变量名 // 2、T:表示切片中的元素类型 package main import \"fmt\" func main() { // 切片是引用类型,不支持直接比较,只能和 nil 比较 var a []string //声明一个字符串切片 fmt.Println(a) //[] fmt.Println(a == nil) //true var b = []int{} //声明一个整型切片并初始化 fmt.Println(b) //[] fmt.Println(b == nil) //false var c = []bool{false, true} //声明一个布尔切片并初始化 fmt.Println(c) //[false true] fmt.Println(c == nil) //false } 切片之间是不能比较的,我们不能使用==操作符来判断两个切片是否含有全部相等元素。 切片唯一合法的比较操作是和 nil 比较。 一个 nil 值的切片并没有底层数组,一个 nil 值的切片的长度和容量都是 0。 但是我们不能说一个长度和容量都是 0 的切片一定是 nil 例如下面的 1.2 关于 nil 的认识 当你声明了一个变量 , 但却还并没有赋值时 , golang 中会自动给你的变量赋值一个默认零值。 这是每种类型对应的零值 bool -> false numbers -> 0 string-> \"\" pointers -> nil slices -> nil maps -> nil channels -> nil functions -> nil interfaces -> nil 1.3 切片的本质 切片的本质就是对底层数组的封装,它包含了三个信息:底层数组的指针、切片的长度(len)和切片的容量(cap)。 举个例子,现在有一个数组 a := [8]int{0, 1, 2, 3, 4, 5, 6, 7},切片 s1 := a[:5],相应示意图如下。 切片 s2 := a[3:6],相应示意图如下 1.4 切片的扩容策略 1、首先判断,如果新申请容量(cap)大于 2 倍的旧容量(old.cap),最终容量(newcap)就是新申请的容量(cap)。 2、否则判断,如果旧切片的长度小于 1024,则最终容量(newcap)就是旧容量(old.cap)的两倍,即(newcap=doublecap) 3、否则判断,如果旧切片长度大于等于 1024,则最终容量(newcap)从旧容量(old.cap)开始循环增加原来的 1/4,即(newcap=old.cap,for{newcap+= newcap/4})直到最终容量newcap)大于等于新申请的容量(cap),即(newcap >= cap) 4、如果最终容量(cap)计算值溢出,则最终容量(cap)就是新申请容量(cap)。 1.5 切片的长度和容量 切片拥有自己的长度和容量,我们可以通过使用内置的 **len()**函数求长度,使用内置的 **cap()**函数求切片的容量。 切片的长度就是它所包含的元素个数。 切片的容量是从它的第一个元素开始数,到其底层数组元素末尾的个数。 切片 s 的长度和容量可通过表达式 len(s) 和 cap(s) 来获取。 import \"fmt\" func main() { s := []int{2, 3, 5, 7, 11, 13} fmt.Printf(\"长度:%v 容量 %v\\n\", len(s), cap(s)) // 长度:6 容量 6 c := s[:2] fmt.Println(c) // [2 3] fmt.Printf(\"长度:%v 容量 %v\\n\", len(c), cap(c)) // 长度:2 容量 6 d := s[1:3] fmt.Println(d) // [3 5] fmt.Printf(\"长度:%v 容量 %v\", len(d), cap(d)) // 长度:2 容量 5 } 02.切片循环 切片的循环遍历和数组的循环遍历是一样的 2.1 基本遍历package main import \"fmt\" func main() { var a = []string{\"北京\", \"上海\", \"深圳\"} for i := 0; i < len(a); i++ { fmt.Println(a[i]) } } /* 北京 上海 深圳 */ 2.2 k,v遍历package main import \"fmt\" func main() { var a = []string{\"北京\", \"上海\", \"深圳\"} for index, value := range a { fmt.Println(index, value) } } /* 0 北京 1 上海 2 深圳 */ 03.定义切片3.1 数组定义切片 由于切片的底层就是一个数组,所以我们可以基于数组定义切片。 package main import \"fmt\" func main() { a := [5]int{55, 56, 57, 58, 59} // 基于数组定义切片 b := a[1:4] fmt.Println(b) // [56 57 58] fmt.Printf(\"type of b:%T\\n\", b) // type of b:[]int c := b[0:2] fmt.Println(c) // [56 57] } 3.2 make()构造切片 我们上面都是基于数组来创建的切片,如果需要动态的创建一个切片,我们就需要使用内置的 make()函数 格式如下:make([]T, size, cap) T:切片的元素类型 size:切片中元素的数量 cap:切片的容量 package main import \"fmt\" func main() { a := make([]int, 2, 10) fmt.Println(a) //[0 0] fmt.Println(len(a)) //2 fmt.Println(cap(a)) //10 } 上面代码中 a 的内部存储空间已经分配了 10 个,但实际上只用了 2 个。 容量并不会影响当前元素的个数,所以 len(a)返回 2,cap(a)则返回该切片的容量。 04.append() Go 语言的内建函数 append()可以为切片动态添加元素,每个切片会指向一个底层数组 这个数组的容量够用就添加新增元素。 当底层数组不能容纳新增的元素时,切片就会自动按照一定的策略进行“扩容”,此时该切片指向的底层数组就会更换。 “扩容”操作往往发生在append()函数调用时,所以我们通常都需要用原变量接收 append 函数的返回值。 4.1 append添加package main import \"fmt\" func main() { // append()添加元素和切片扩容 var numSlice []int for i := 0; i < 10; i++ { numSlice = append(numSlice, i) fmt.Printf(\"%v len:%d cap:%d ptr:%p\\n\", numSlice, len(numSlice), cap(numSlice), numSlice) } } 4.2 append追加多个package main import \"fmt\" func main() { var citySlice []string citySlice = append(citySlice, \"北京\") // 追加一个元素 citySlice = append(citySlice, \"上海\", \"广州\", \"深圳\") // 追加多个元素 a := []string{\"成都\", \"重庆\"} citySlice = append(citySlice, a...) // 追加切片 fmt.Println(citySlice) //[北京 上海 广州 深圳 成都 重庆] } 4.3 切片中删除元素 Go 语言中并没有删除切片元素的专用方法,我们可以使用切片本身的特性来删除元素 package main import \"fmt\" func main() { a := []int{30, 31, 32, 33, 34, 35, 36, 37} a = append(a[:2], a[3:]...) // 要删除索引为 2 的元素 fmt.Println(a) //[30 31 33 34 35 36 37] } 4.4 切片合并package main import \"fmt\" func main() { arr1 := []int{2,7,1} arr2 := []int{5,9,3} fmt.Println(arr2,arr1) arr1 = append(arr1, arr2...) fmt.Println(arr1) // [2 7 1 5 9 3] } 05.copy()5.1 引用问题package main import \"fmt\" func main() { a := []int{1, 2, 3, 4, 5} b := a fmt.Println(a) //[1 2 3 4 5] fmt.Println(b) //[1 2 3 4 5] b[0] = 1000 fmt.Println(a) //[1000 2 3 4 5] fmt.Println(b) //[1000 2 3 4 5] } 5.2 copy()函数 Go 语言内建的 copy()函数可以迅速地将一个切片的数据复制到另外一个切片空间中 copy()函数的使用格式如下: copy(destSlice, srcSlice []T) 其中: srcSlice: 数据来源切片 destSlice: 目标切片 package main import \"fmt\" func main() { a := []int{1, 2, 3, 4, 5} c := make([]int, 5, 5) // [0 0 0 0 0] copy(c, a) //使用 copy()函数将切片 a 中的元素复制到切片 c fmt.Println(a) //[1 2 3 4 5] fmt.Println(c) //[1 2 3 4 5] c[0] = 1000 fmt.Println(a) //[1 2 3 4 5] fmt.Println(c) //[1000 2 3 4 5] } 06.sort()6.1 正序排序 对于 int 、 float64 和 string 数组或是切片的排序 go 分别提供了 sort.Ints() 、sort.Float64s() 和 sort.Strings() 函数, 默认都是从小到大排序 package main import ( \"fmt\" \"sort\" ) func main() { intList := []int{2, 4, 3, 5, 7, 6, 9, 8, 1, 0} sort.Ints(intList) fmt.Println(intList) // [0 1 2 3 4 5 6 7 8 9] stringList := []string{\"a\", \"c\", \"b\", \"z\", \"x\", \"w\", \"y\", \"d\", \"f\", \"i\"} sort.Strings(stringList) fmt.Println(stringList) // [a b c d f i w x y z] } 6.2 sort 降序排序 Golang的sort 包 可 以 使 用 sort.Reverse(slice) 来 调 换slice.Interface.Less 也就是比较函数,所以, int 、 float64 和 string的逆序排序函数可以这么写 package main import ( \"fmt\" \"sort\" ) func main() { intList := []int{2, 4, 3, 5, 7, 6, 9, 8, 1, 0} sort.Sort(sort.Reverse(sort.IntSlice(intList))) fmt.Println(intList) // [9 8 7 6 5 4 3 2 1 0] stringList := []string{\"a\", \"c\", \"b\", \"z\", \"x\", \"w\", \"y\", \"d\", \"f\", \"i\"} sort.Sort(sort.Reverse(sort.StringSlice(stringList))) fmt.Println(stringList) // [z y x w i f d c b a] }","categories":[{"name":"Go基础","slug":"Go基础","permalink":"http://coderedeng.github.io/categories/Go%E5%9F%BA%E7%A1%80/"}],"tags":[{"name":"Go基础","slug":"Go基础","permalink":"http://coderedeng.github.io/tags/Go%E5%9F%BA%E7%A1%80/"}]},{"title":"数组","slug":"2.数组","date":"2021-01-07T14:54:21.000Z","updated":"2024-01-22T12:58:59.000Z","comments":true,"path":"2021/01/07/2.数组/","link":"","permalink":"http://coderedeng.github.io/2021/01/07/2.%E6%95%B0%E7%BB%84/","excerpt":"","text":"1.数组01.切片基础1.1 Array介绍 数组是指一系列同一类型数据的集合。 数组中包含的每个数据被称为数组元素(element),这种类型可以是任意的原始类型,比如 int、string 等 一个数组包含的元素个数被称为数组的长度。 在 Golang 中数组是一个长度固定的数据类型,数组的长度是类型的一部分,也就是说 [5]int 和 [10]int 是两个不同的类型。 Golang中数组的另一个特点是占用内存的连续性,也就是说数组中的元素是被分配到连续的内存地址中的,因而索引数组元素的速度非常快。 和数组对应的类型是 Slice(切片),Slice 是可以增长和收缩的动态序列,功能也更灵活 但是想要理解 slice 工作原理的话需要先理解数组,所以本节主要为大家讲解数组的使用。 1.2 数组定义var 数组变量名 [元素数量]T 比如:var a [5]int, 数组的长度必须是常量,并且长度是数组类型的一部分 一旦定义,长度不能变。 [5]int 和[4]int 是不同的类型。 package main import \"fmt\" func main() { // 定义一个长度为 3 元素类型为 int 的数组 a var a [5]int // 定义一个长度为 3 元素类型为 int 的数组 b 并赋值 var b [3]int b[0] = 80 b[1] = 100 b[2] = 96 fmt.Println(a) // [0 0 0 0 0] fmt.Print(b) // [80 100 96] } 1.3 数组是值类型 数组是值类型,赋值和传参会复制整个数组。 因此改变副本的值,不会改变本身的值。 注意: 数组支持 “==“、”!=” 操作符,因为内存总是被初始化过的。 [n]*T表示指针数组,*[n]T 表示数组指针 package main import \"fmt\" func main() { a := [3]int{10, 20, 30} modifyArray(a) //在 modify 中修改的是 a 的副本 x fmt.Println(a) //[10 20 30] } func modifyArray(x [3]int) { x[0] = 100 } 02.创建数组2.1 自定义数组长度package main import \"fmt\" func main() { // 1) 数组会初始化为 int 类型的零值 var testArray [3]int fmt.Println(testArray) //[0 0 0] // 2) 使用指定的初始值完成初始化 var numArray = [3]int{1, 2} fmt.Println(numArray) //[1 2 0] var cityArray = [3]string{\"北京\", \"上海\", \"深圳\"} fmt.Println(cityArray) //[北京 上海 深圳] } 2.2 让编译器识别 按照上面的方法每次都要确保提供的初始值和数组长度一致 一般情况下我们可以让编译器根据初始值的个数自行推断数组的长度 package main import \"fmt\" func main() { var numArray = [...]int{1, 2} fmt.Println(numArray) //[1 2] fmt.Printf(\"type of numArray:%T\\n\", numArray) //type of numArray:[2]int var cityArray = [...]string{\"北京\", \"上海\", \"深圳\"} fmt.Println(cityArray) //[北京 上海 深圳] fmt.Printf(\"type of cityArray:%T\\n\", cityArray) //type of cityArray:[3]string } 2.3 指定索引值 我们还可以使用指定索引值的方式来初始化数组 package main import \"fmt\" func main() { // 初始化一个整数数组,在下标1号和3号位置写入: 1 5 a := [...]int{1: 1, 3: 5} fmt.Println(a) // [0 1 0 5] fmt.Printf(\"type of a:%T\\n\", a) //type of a:[4]int } 03.数组的遍历3.1 普通遍历数组package main import \"fmt\" func main() { var a = [...]string{\"北京\", \"上海\", \"深圳\"} for i := 0; i < len(a); i++ { fmt.Println(a[i]) } } /* 北京 上海 深圳 */ 3.2 k,v遍历数组package main import \"fmt\" func main() { var a = [...]string{\"北京\", \"上海\", \"深圳\"} for index, value := range a { fmt.Println(index, value) } } /* 0 北京 1 上海 2 深圳 */ 04.多维数组4.1 定义多维数组package main import \"fmt\" func main() { a := [3][2]string{ {\"北京\", \"上海\"}, {\"广州\", \"深圳\"}, {\"成都\", \"重庆\"}, } fmt.Println(a) //[[北京 上海] [广州 深圳] [成都 重庆]] fmt.Println(a[2][1]) //支持索引取值:重庆 } 4.2 遍历多维数组package main import \"fmt\" func main() { a := [3][2]string{ {\"北京\", \"上海\"}, {\"广州\", \"深圳\"}, {\"成都\", \"重庆\"}, } for _, v1 := range a { // v1 = [北京 上海] for _, v2 := range v1 { fmt.Println(v2) } } } /* 北京 上海 广州 深圳 成都 重庆 */ 05.数组练习5.1 数组求和package main import \"fmt\" func main() { var intArr2 [5]int = [...]int {1, -1, 9, 90, 12} sum := 0 for _, val := range intArr2 { //累计求和 sum += val } //如何让平均值保留到小数. fmt.Printf(\"sum=%v 平均值=%v \\n\\n\", sum, float64(sum) / float64(len(intArr2))) // sum=111 平均值=22.2 } 5.2 数组最大值 1、声明一个数组 var intArr[5] = […]int {1, -1, 12, 65, 11} 2、假定第一个元素就是最大值,下标就 0 3、然后从第二个元素开始循环比较,如果发现有更大,则交换 package main import \"fmt\" func main() { var intArr = [...]int{1, -1, 112, 65, 11} maxValue := intArr[0] maxIndex := 0 for i := 0; i < len(intArr); i++ { if maxValue < intArr[i] { maxValue = intArr[i] maxIndex = i } } fmt.Println(\"最大值\", maxValue, \"最大值索引值\", maxIndex) // 最大值 112 最大值索引值 2 }","categories":[{"name":"Go基础","slug":"Go基础","permalink":"http://coderedeng.github.io/categories/Go%E5%9F%BA%E7%A1%80/"}],"tags":[{"name":"Go基础","slug":"Go基础","permalink":"http://coderedeng.github.io/tags/Go%E5%9F%BA%E7%A1%80/"}]},{"title":"基本数据类型","slug":"1.基本数据类型","date":"2021-01-07T14:50:35.000Z","updated":"2024-01-22T12:58:59.000Z","comments":true,"path":"2021/01/07/1.基本数据类型/","link":"","permalink":"http://coderedeng.github.io/2021/01/07/1.%E5%9F%BA%E6%9C%AC%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B/","excerpt":"","text":"1.基本数据类型01.内置类型1.1 值类型:bool int(32 or 64), int8, int16, int32, int64 uint(32 or 64), uint8(byte), uint16, uint32, uint64 float32, float64 string complex64, complex128 array // 固定长度的数组 1.2 引用类型:(指针类型)slice // 序列数组(最常用) map // 映射 chan // 管道 02.内置函数 Go 语言拥有一些不需要进行导入操作就可以使用的内置函数。 它们有时可以针对不同的类型进行操作,例如:len、cap 和 append,或必须用于系统级的操作,例如:panic。 因此,它们需要直接获得编译器的支持。 append // 用来追加元素到数组、slice中,返回修改后的数组、slice close // 主要用来关闭channel delete // 从map中删除key对应的value panic // 停止常规的goroutine (panic和recover:用来做错误处理) recover // 允许程序定义goroutine的panic动作 real // 返回complex的实部 (complex、real imag:用于创建和操作复数) imag // 返回complex的虚部 make // 用来分配内存,返回Type本身(只能应用于slice, map, channel) new // 用来分配内存,主要用来分配值类型,比如int、struct。返回指向Type的指针 cap // capacity是容量的意思,用于返回某个类型的最大容量(只能用于切片和 map) copy // 用于复制和连接slice,返回复制的数目 len // 来求长度,比如string、array、slice、map、channel ,返回长度 print、println // 底层打印函数,在部署环境中建议使用 fmt 包 03.基本类型介绍 类型 长度(字节) 默认值 说明 bool 1 false byte 1 0 uint8 rune 4 0 Unicode Code Point, int32 int, uint 4或8 0 32 或 64 位 int8, uint8 1 0 -128 ~ 127, 0 ~ 255,byte是uint8 的别名 int16, uint16 2 0 -32768 ~ 32767, 0 ~ 65535 int32, uint32 4 0 -21亿~ 21亿, 0 ~ 42亿,rune是int32 的别名 int64, uint64 8 0 float32 4 0.0 float64 8 0.0 complex64 8 xxxxxxxxxx package mainimport “fmt”func main() { var intArr = […]int{1, -1, 112, 65, 11} maxValue := intArr[0] maxIndex := 0 for i := 0; i < len(intArr); i++ { if maxValue < intArr[i] { maxValue = intArr[i] maxIndex = i } } fmt.Println(“最大值”, maxValue, “最大值索引值”, maxIndex) // 最大值 112 最大值索引值 2}go complex128 16 uintptr 4或8 以存储指针的 uint32 或 uint64 整数 array 值类型 struct 值类型 string “” UTF-8 字符串 slice nil 引用类型 map nil 引用类型 channel nil 引用类型 interface nil 接口 function nil 函数 04.数字类型4.1 Golang数据类型介绍 Go 语言中数据类型分为:基本数据类型和复合数据类型 基本数据类型有: 整型、浮点型、布尔型、字符串 复合数据类型有: 数组、切片、结构体、函数、map、通道(channel)、接口 4.2 整型分为两大类 有符号整形按长度分为:int8、int16、int32、int64 对应的无符号整型:uint8、uint16、uint32、uint64 关于字节: 字节也叫 Byte,是计算机数据的基本存储单位。8bit(位)=1Byte(字节) 1024Byte(字节)=1KB 1024KB=1MB 1024MB=1GB 1024GB=1TB 。在电脑里一个中文字是占两个字节的。 4.3 unsafe.Sizeof unsafe.Sizeof(n1) 是 unsafe 包的一个函数,可以返回 n1 变量占用的字节数 package main import ( \"fmt\" \"unsafe\" ) func main() { var a int8 = 124 fmt.Printf(\"%T\\n\", a) // int8 fmt.Println(unsafe.Sizeof(a)) // 1 (表示占用1个字节,也就是8 byte) } 4.4 int不同长度直接的转换package main import ( \"fmt\" ) func main() { var num1 int8 num1 = 127 num2 := int32(num1) // 将num1类型转换成 int32 并赋值给num1 fmt.Printf(\"值:%v 类型%T\", num2, num2) //值:127 类型 int32 } 4.5 浮点型 Go 语言支持两种浮点型数:float32 和 float64 package main import ( \"fmt\" \"math\" ) func main() { fmt.Printf(\"%f\\n\", math.Pi) // 3.141593 (默认保留 6 位小数) fmt.Printf(\"%.2f\\n\", math.Pi) // 3.14 (保留 2 位小数) } 4.6 reflect.TypeOf查看数据类型package main import ( \"fmt\" \"reflect\" ) func main() { c := 10 fmt.Println( reflect.TypeOf(c) ) // int } 4.7 int常用转换package main import ( \"fmt\" \"strconv\" ) func main() { // string到int intV,_ := strconv.Atoi(\"123456\") // string到int64 int64V, _ := strconv.ParseInt(\"123456\", 10, 64) // int到string strS := strconv.Itoa(123) // int64到string var tmp int64 = 123 str64S:=strconv.FormatInt(tmp,10) fmt.Printf(\"%T--%T--%T--%T\", intV, int64V, strS, str64S) // int--int64--string--string } 4.8 int8转int16package main import \"fmt\" func main() { var a int8 = 20 var b int16 = 40 var c = int16(a) + b //要转换成相同类型才能运行 fmt.Printf(\"值:%v--类型%T\", c, c) //值:60--类型 int16 } 4.9 int16转float32package main import \"fmt\" func main() { var a float32 = 3.2 var b int16 = 6 var c = a + float32(b) fmt.Printf(\"值:%v--类型%T\", c, c) //值:9.2--类型 float32 } 4.10 math.Sqrt强转package main import ( \"fmt\" \"math\" ) func main() { var a, b = 3, 4 var c int // math.Sqrt()接收的参数是 float64 类型,需要强制转换 c = int(math.Sqrt(float64(a*a + b*b))) fmt.Println(c) // 5 } 4.11 int与str转换package main import ( \"fmt\" \"strconv\" ) func main() { //2.1 int64转str var num2 int64 = 123456 str2 := strconv.FormatInt(num2, 10) fmt.Printf(\"%v---%T \\n\",str2,str2) // 123456---string //2.2 str转int64 v1, _ := strconv.ParseFloat(str2, 64) fmt.Printf(\"%v---%T\\n\",v1,v1) // 123456---float64 } 4.12 str与int64转换package main import ( \"fmt\" \"strconv\" ) func main() { //1.1 int转sting num1 := 123456 str1 := strconv.Itoa(num1) fmt.Printf(\"%v---%T \\n\",str1,str1) // 123456---string // 1.2 sting转int _int, err := strconv.Atoi(str1) fmt.Println(_int,err) // 123456 <nil> fmt.Printf(\"%v---%T\\n\",_int,_int) // 123456---int //2.1 int64转str var num2 int64 = 123456 str2 := strconv.FormatInt(num2, 10) fmt.Printf(\"%v---%T \\n\",str2,str2) // 123456---string //2.2 str转int64 v1, _ := strconv.ParseFloat(str2, 64) fmt.Printf(\"%v---%T\\n\",v1,v1) // 123456---float64 } 05.布尔值 Go 语言中以 bool 类型进行声明布尔型数据,布尔型数据只有 true(真)和 false(假)两个值。 注意: 1.布尔类型变量的默认值为 false。 2.Go 语言中不允许将整型强制转换为布尔型. 3.布尔型无法参与数值运算,也无法与其他类型进行转换。 package main import ( \"fmt\" \"unsafe\" ) func main() { var b = true fmt.Println(b, \"占用字节:\", unsafe.Sizeof(b)) // true 占用字节: 1 } 06.字符串6.1 字符串 Go 语言里的字符串的内部实现使用 UTF-8 编码。 字符串的值为双引号(“)中的内容,可以在 Go 语言的源码中直接添加非 ASCII 码字符 s1 := \"hello\" s2 := \"你好\" 6.2 字符串转义符 Go 语言的字符串常见转义符包含回车、换行、单双引号、制表符等 package main import ( \"fmt\" ) func main() { fmt.Println(\"str := \\\"c:\\\\Code\\\\demo\\\\go.exe\\\"\") // str := \"c:\\Code\\demo\\go.exe\" } 6.3 多行字符串 反引号间换行将被作为字符串中的换行,但是所有的转义字符均无效,文本将会原样输出。 package main import ( \"fmt\" ) func main() { s1 := ` 第一行 第二行 第三行` fmt.Println(s1) } 6.4 byte和rune Go 语言的字符有以下两种 uint8类型,或者叫 byte 型:代表了ASCII码的一个字符。 rune类型:代表一个 UTF-8字符 字符串底层是一个byte数组,所以可以和[]byte类型相互转换。 字符串是不能修改的 字符串是由byte字节组成,所以字符串的长度是byte字节的长度。 rune类型用来表示utf8字符,一个rune字符由一个或多个byte组成。 package main import \"fmt\" func main() { // “美国第一” s := \"美国第一\" s_rune := []rune(s) fmt.Println( \"中国\" + string(s_rune[2:])) // 中国第一 } 07.字符串的常用操作 方法 介绍 len(str) 求长度 +或fmt.Sprintf 拼接字符串 strings.Split 分割 strings.Contains 判断是否包含 strings.HasPrefix,strings.HasSuffix 前缀/后缀判断 strings.Index(),strings.LastIndex() 子串出现的位置 strings.Join(a[]string, sep string) join操作 7.1 len(str)package main import ( \"fmt\" ) func main() { var str = \"this is str\" fmt.Println(len(str)) // 11 } 7.2 +(拼接)package main import ( \"fmt\" ) func main() { var str1 = \"你好\" var str2 = \"golang\" fmt.Println(str1 + str2) // 你好golang } 7.3 strings.Split()package main import ( \"fmt\" \"strings\" ) func main() { var s = \"123-456-789\" var arr = strings.Split(s, \"-\") fmt.Println(arr) // [123 456 789] } 7.4 strings.HasPrefix() 首字符尾字母包含指定字符 package main import ( \"fmt\" \"strings\" ) func main() { // 1.判断字符串 以 this 开头 var str = \"this is golang\" var flag = strings.HasPrefix(str, \"this\") fmt.Println(flag) // true // 2.判断字符串以 go 结尾 var flag2 = strings.HasSuffix(str, \"go\") fmt.Println(flag2) // false } 7.5 strings.Index() 判断字符串出现的位置 package main import ( \"fmt\" \"strings\" ) func main() { var str = \"this is golang\" var index = strings.Index(str, \"go\") //从前往后 fmt.Println(index) // 8 (判断字符串 go 出现的位置) } 7.6 strings.Join()package main import ( \"fmt\" \"strings\" ) func main() { var str = \"123-456-789\" var arr = strings.Split(str, \"-\") // [123 456 789] var str2 = strings.Join(arr, \"*\") // 123*456*789 fmt.Println(arr) fmt.Println(str2) } 7.7 单引号 组成每个字符串的元素叫做“字符”,可以通过遍历字符串元素获得字符,字符用单引号(’) uint8 类型,或者叫 byte 型,代表了 ASCII 码的一个字符 rune 类型,代表一个 UTF-8 字符 package main import \"fmt\" func main() { a := 'a' name := \"zhangsan\" //当我们直接输出 byte(字符)的时候输出的是这个字符对应的码值 fmt.Println(a) // 97 这里输出的是 a 字符串的 ASCII值 fmt.Println(name) // zhangsan //如果我们要输出这个字符,需要格式化输出 fmt.Printf(\"的值是%c\", a) // a的值是a } 08.字符串遍历8.1 遍历字符串package main import \"fmt\" func main() { s := \"hello 张三\" for i := 0; i < len(s); i++ { //byte fmt.Printf(\"%v(%c) \", s[i], s[i]) // 104(h) 101(e) 108(l) 108(l) 111(o) 32( ) 229(å) 188(¼) 160() 228(ä) 184(¸) 137() } fmt.Println() // 打印一个换行 for _, r := range s { //rune fmt.Printf(\"%v=>%c \", r, r) // 104=>h 101=>e 108=>l 108=>l 111=>o 32=> 24352=>张 19977=>三 } fmt.Println() } 8.2 修改字符串 要修改字符串,需要先将其转换成[]rune 或[]byte,完成后再转换为 string。 无论哪种转换,都会重新分配内存,并复制字节数组。 package main import \"fmt\" func main() { s1 := \"big\" // 强制类型转换 byteS1 := []byte(s1) byteS1[0] = 'p' fmt.Println(string(byteS1)) // pig s2 := \"白萝卜\" runeS2 := []rune(s2) runeS2[0] = '红' fmt.Println(string(runeS2)) // 红萝卜 } 将“美国第一”改成“中国第一” package main import \"fmt\" func main() { // “美国第一” s := \"美国第一\" s_rune := []rune(s) fmt.Println( \"中国\" + string(s_rune[2:])) // 中国第一 } 09.转String9.1 sprintf转string 注意:sprintf 使用中需要注意转换的格式 int 为%d float 为%f bool 为%t byte 为%c package main import \"fmt\" func main() { var i int = 20 var f float64 = 12.456 var t bool = true var b byte = 'a' var strs string strs = fmt.Sprintf(\"%d\", i) // 把 int 转换成 string fmt.Printf(\"类型: %T ,值=%v \\n\", strs, strs) // 类型: string ,值=20 strs = fmt.Sprintf(\"%f\", f) // 把 float 转换成 string fmt.Printf(\"类型: %T ,值=%v \\n\", strs, strs) // 类型: string ,值=12.456000 strs = fmt.Sprintf(\"%t\", t) // 把 bool 转换成 string fmt.Printf(\"类型: %T ,值=%v \\n\", strs, strs) // 类型: string ,值=true strs = fmt.Sprintf(\"%c\", b) // 把 byte 转换成 string fmt.Printf(\"类型: %T ,值=%v \\n\", strs, strs) // 类型: string ,值=a } 9.2 strconvpackage main import ( \"fmt\" \"strconv\" ) func main() { //1、int 转换成 string var num1 int = 20 s1 := strconv.Itoa(num1) fmt.Printf(\"类型: %T ,值=%v \\n\", s1, s1) // 类型: string ,值=20 // 2、float 转 string var num2 float64 = 20.113123 /* 参数 1:要转换的值 参数 2:格式化类型 参数 3: 保留的小数点 -1(不对小数点格式化) 参数 4:格式化的类型 */ s2 := strconv.FormatFloat(num2, 'f', 2, 64) fmt.Printf(\"类型: %T ,值=%v \\n\", s2, s2) // 类型: string ,值=20.11 // 3、bool 转 string s3 := strconv.FormatBool(true) fmt.Printf(\"类型: %T ,值=%v \\n\", s3, s3) // 类型: string ,值=20.11 //4、int64 转 string var num3 int64 = 20 s4 := strconv.FormatInt(num3, 10) /* 第二个参数10为 进制 */ fmt.Printf(\"类型 %T ,值=%v \\n\", s4, s4) // 类型 string ,值=20 } 10.String转其他10.1 string转intpackage main import ( \"fmt\" \"strconv\" ) func main() { var s = \"1234\" i64, _ := strconv.ParseInt(s, 10, 64) fmt.Printf(\"值:%v 类型:%T\", i64, i64) // 值:1234 类型:int64 } 10.2 string转floatpackage main import ( \"fmt\" \"strconv\" ) func main() { str := \"3.1415926535\" v1, _ := strconv.ParseFloat(str, 32) v2, _ := strconv.ParseFloat(str, 64) fmt.Printf(\"值:%v 类型:%T\\n\", v1, v1) // 值:3.1415927410125732 类型:float64 fmt.Printf(\"值:%v 类型:%T\", v2, v2) // 值:3.1415926535 类型:float64 } 10.3 string转boolpackage main import ( \"fmt\" \"strconv\" ) func main() { b, _ := strconv.ParseBool(\"true\") // string 转 bool fmt.Printf(\"值:%v 类型:%T\", b, b) // 值:true 类型:bool } 10.4 string转字符package main import ( \"fmt\" ) func main() { s := \"hello 张三\" for _, r := range s { //rune // 104(h) 101(e) 108(l) 108(l) 111(o) 32( ) 24352(张) 19977(三) fmt.Printf(\"%v(%c) \", r, r) } fmt.Println() } 10.5 字符串反转package main func Reverse(s string) string { r := []rune(s) for i, j := 0, len(r)-1; i < j; i, j = i+1, j-1 { r[i], r[j] = r[j], r[i] } return string(r) } func main() { a := \"Hello, Wrold\" println(a) println(Reverse(a)) // dlorW ,olleH }","categories":[{"name":"Go基础","slug":"Go基础","permalink":"http://coderedeng.github.io/categories/Go%E5%9F%BA%E7%A1%80/"}],"tags":[{"name":"Go基础","slug":"Go基础","permalink":"http://coderedeng.github.io/tags/Go%E5%9F%BA%E7%A1%80/"}]}],"categories":[{"name":"Go爬虫","slug":"Go爬虫","permalink":"http://coderedeng.github.io/categories/Go%E7%88%AC%E8%99%AB/"},{"name":"Go基础","slug":"Go基础","permalink":"http://coderedeng.github.io/categories/Go%E5%9F%BA%E7%A1%80/"},{"name":"Go进阶","slug":"Go进阶","permalink":"http://coderedeng.github.io/categories/Go%E8%BF%9B%E9%98%B6/"},{"name":"Go常用库","slug":"Go常用库","permalink":"http://coderedeng.github.io/categories/Go%E5%B8%B8%E7%94%A8%E5%BA%93/"},{"name":"Go设计模式","slug":"Go设计模式","permalink":"http://coderedeng.github.io/categories/Go%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/"}],"tags":[{"name":"Go爬虫","slug":"Go爬虫","permalink":"http://coderedeng.github.io/tags/Go%E7%88%AC%E8%99%AB/"},{"name":"Go基础","slug":"Go基础","permalink":"http://coderedeng.github.io/tags/Go%E5%9F%BA%E7%A1%80/"},{"name":"Go进阶","slug":"Go进阶","permalink":"http://coderedeng.github.io/tags/Go%E8%BF%9B%E9%98%B6/"},{"name":"Go常用库","slug":"Go常用库","permalink":"http://coderedeng.github.io/tags/Go%E5%B8%B8%E7%94%A8%E5%BA%93/"},{"name":"Go设计模式","slug":"Go设计模式","permalink":"http://coderedeng.github.io/tags/Go%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/"}]}