关于godailylib:Go中调用外部命令的几种姿势

引子在工作中,我时不时地会须要在Go中调用外部命令。前段时间我做了一个工具,在钉钉群中增加了一个机器人,@这个机器人能够让它执行一些写好的脚本程序实现指定的工作。机器人倒是不难,照着钉钉开发者文档增加好机器人,而后@这个机器人就会向一个你指定的服务器发送一个POST申请,申请中会附带文本音讯。所以我要做的就是搭一个Web服务器,能够用go原生的net/http包,也能够用gin/fasthttp/fiber这些Web框架。收到申请之后,查看附带文本中的关键字去调用对应的程序,而后返回后果。 go规范库中的os/exec包对调用内部程序提供了反对,本文具体介绍os/exec的应用姿态。 运行命令Linux中有个cal命令,它能够显示指定年、月的日历,如果不指定年、月,默认为以后工夫对应的年月。如果应用的是Windows,举荐装置msys2,这个软件蕴含了绝大多数的Linux常用命令。 那么,在Go代码中怎么调用这个命令呢?其实也很简略: func main() { cmd := exec.Command("cal") err := cmd.Run() if err != nil { log.Fatalf("cmd.Run() failed: %v\n", err) }}首先,咱们调用exec.Command传入命令名,创立一个命令对象exec.Cmd。接着调用该命令对象的Run()办法运行它。 如果你理论运行了,你会发现什么也没有产生,哈哈。事实上,应用os/exec执行命令,规范输入和规范谬误默认会被抛弃。 显示输入exec.Cmd对象有两个字段Stdout和Stderr,类型皆为io.Writer。咱们能够将任意实现了io.Writer接口的类型实例赋给这两个字段,继而实现规范输入和规范谬误的重定向。io.Writer接口在 Go 规范库和第三方库中随处可见,例如*os.File、*bytes.Buffer、net.Conn。所以咱们能够将命令的输入重定向到文件、内存缓存甚至发送到网络中。 显示到规范输入将exec.Cmd对象的Stdout和Stderr这两个字段都设置为os.Stdout,那么输入内容都将显示到规范输入: func main() { cmd := exec.Command("cal") cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr err := cmd.Run() if err != nil { log.Fatalf("cmd.Run() failed: %v\n", err) }}运行程序。我在git bash运行,失去如下后果: 输入了中文,检查一下环境变量LANG的值,果然是zh_CN.UTF-8。如果想输入英文,能够将环境变量LANG设置为en_US.UTF-8: $ echo $LANGzh_CN.UTF-8$ LANG=en_US.UTF-8 go run main.go失去输入: 输入到文件关上或创立文件,而后将文件句柄赋给exec.Cmd对象的Stdout和Stderr这两个字段即可实现输入到文件的性能。 func main() { f, err := os.OpenFile("out.txt", os.O_WRONLY|os.O_CREATE, os.ModePerm) if err != nil { log.Fatalf("os.OpenFile() failed: %v\n", err) } cmd := exec.Command("cal") cmd.Stdout = f cmd.Stderr = f err = cmd.Run() if err != nil { log.Fatalf("cmd.Run() failed: %v\n", err) }}os.OpenFile关上一个文件,指定os.O_CREATE标记让操作系统在文件不存在时主动创立一个,返回该文件对象*os.File。*os.File实现了io.Writer接口。 ...

November 8, 2022 · 4 min · jiezi

关于godailylib:Go-每日一库之-roaring

简介汇合是软件中的根本形象。实现汇合的办法有很多,例如 hash set、tree等。要实现一个整数汇合,位图(bitmap,也称为 bitset 位汇合,bitvector 位向量)是个不错的办法。应用 n 个位(bit),咱们能够示意整数范畴[0, n)。如果整数 i 在汇合中,第 i 位设置为 1。这样汇合的交加(intersection)、并集(unions)和差集(difference)能够利用整数的按位与、按位或和按位与非来实现。而计算机执行位运算是十分迅速的。 上一篇文章我介绍了bitset这个库。 bitset 在某些场景中会耗费大量的内存。例如,设置第 1,000,000 位,须要占用超过 100kb 的内存。为此 bitset 库的作者又开发了压缩位图库:roaring。 本文首先介绍了 roaring 的应用。最初剖析 roaring 的文件存储格局。 装置本文代码应用 Go Modules。 创立目录并初始化: $ mkdir -p roaring && cd roaring$ go mod init github.com/darjun/go-daily-lib/roaring装置roaring库: $ go get -u github.com/RoaringBitmap/roaring应用基本操作func main() { bm1 := roaring.BitmapOf(1, 2, 3, 4, 5, 100, 1000) fmt.Println(bm1.String()) // {1,2,3,4,5,100,1000} fmt.Println(bm1.GetCardinality()) // 7 fmt.Println(bm1.Contains(3)) // true bm2 := roaring.BitmapOf(1, 100, 500) fmt.Println(bm2.String()) // {1,100,500} fmt.Println(bm2.GetCardinality()) // 3 fmt.Println(bm2.Contains(300)) // false bm3 := roaring.New() bm3.Add(1) bm3.Add(11) bm3.Add(111) fmt.Println(bm3.String()) // {1,11,111} fmt.Println(bm3.GetCardinality()) // 3 fmt.Println(bm3.Contains(11)) // true bm1.Or(bm2) // 执行并集 fmt.Println(bm1.String()) // {1,2,3,4,5,100,500,1000} fmt.Println(bm1.GetCardinality()) // 8 fmt.Println(bm1.Contains(500)) // true bm2.And(bm3) // 执行交加 fmt.Println(bm2.String()) // {1} fmt.Println(bm2.GetCardinality()) // 1 fmt.Println(bm2.Contains(1)) // true}下面演示了两种创立 roaring bitmap 的形式: ...

July 18, 2022 · 5 min · jiezi

关于godailylib:Go-每日一库之-bitset

简介咱们都晓得计算机是基于二进制的,位运算是计算机的根底运算。位运算的劣势很显著,CPU 指令原生反对、速度快。基于位运算的位汇合在无限的场景中替换汇合数据结构能够收到意想不到的成果。bitset库实现了位汇合及相干操作,无妨拿来即用。 装置本文代码应用 Go Modules。 创立目录并初始化: $ mkdir -p bitset && cd bitset$ go mod init github.com/darjun/go-daily-lib/bitset装置bitset库: $ go get -u github.com/bits-and-blooms/bitset应用位汇合的基本操作有: 查看位(Test):查看某个索引是否为 1。类比查看元素是否在汇合中设置位(Set):将某个索引设置为 1。类比向汇合增加元素革除位(Clear):将某个索引革除,设置为 0。类比从汇合中删除元素翻转位(Flip):如果某个索引为 1,则设置为 0,反之设置为 1并(Union):两个位汇合执行并操作。类比汇合的并交(Intersection):两个位汇合执行交操作。类比汇合的交位汇合个别用于小数值的非负整数的场景中。就拿游戏中简略的签到举例吧,很多游戏都有签到流动,短的有 7 天的,长的有 30 天。这种就很适宜应用位汇合。每个位的值示意其索引地位对应的那天有没有签到。 type Player struct { sign *bitset.BitSet}func NewPlayer(sign uint) *Player { return &Player{ sign: bitset.From([]uint64{uint64(sign)}), }}func (this *Player) Sign(day uint) { this.sign.Set(day)}func (this *Player) IsSigned(day uint) bool { return this.sign.Test(day)}func main() { player := NewPlayer(1) // 第一天签到 for day := uint(2); day <= 7; day++ { if rand.Intn(100)&1 == 0 { player.Sign(day - 1) } } for day := uint(1); day <= 7; day++ { if player.IsSigned(day - 1) { fmt.Printf("day:%d signed\n", day) } }}bitset 提供了多种创立 BitSet 对象的办法。 ...

July 16, 2022 · 3 min · jiezi

关于godailylib:Go-每日一库之-testing

简介testing是 Go 语言规范库自带的测试库。在 Go 语言中编写测试很简略,只须要遵循 Go 测试的几个约定,与编写失常的 Go 代码没有什么区别。Go 语言中有 3 种类型的测试:单元测试,性能测试,示例测试。上面顺次来介绍。 单元测试单元测试又称为功能性测试,是为了测试函数、模块等代码的逻辑是否正确。接下来咱们编写一个库,用于将示意罗马数字的字符串和整数互转。罗马数字是由M/D/C/L/X/V/I这几个字符依据肯定的规定组合起来示意一个正整数: M=1000,D=500,C=100,L=50,X=10,V=5,I=1;只能示意 1-3999 范畴内的整数,不能示意 0 和正数,不能示意 4000 及以上的整数,不能示意分数和小数(当然有其余简单的规定来示意这些数字,这里暂不思考);每个整数只有一种示意形式,个别状况下,连写的字符示意对应整数相加,例如I=1,II=2,III=3。然而,十位字符(I/X/C/M)最多呈现 3 次,所以不能用IIII示意 4,须要在V右边增加一个I(即IV)来示意,不能用VIIII示意 9,须要应用IX代替。另外五位字符(V/L/D)不能间断呈现 2 次,所以不能呈现VV,须要用X代替。// roman.gopackage romanimport ( "bytes" "errors" "regexp")type romanNumPair struct { Roman string Num int}var ( romanNumParis []romanNumPair romanRegex *regexp.Regexp)var ( ErrOutOfRange = errors.New("out of range") ErrInvalidRoman = errors.New("invalid roman"))func init() { romanNumParis = []romanNumPair{ {"M", 1000}, {"CM", 900}, {"D", 500}, {"CD", 400}, {"C", 100}, {"XC", 90}, {"L", 50}, {"XL", 40}, {"X", 10}, {"IX", 9}, {"V", 5}, {"IV", 4}, {"I", 1}, } romanRegex = regexp.MustCompile(`^M{0,3}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$`)}func ToRoman(n int) (string, error) { if n <= 0 || n >= 4000 { return "", ErrOutOfRange } var buf bytes.Buffer for _, pair := range romanNumParis { for n > pair.Num { buf.WriteString(pair.Roman) n -= pair.Num } } return buf.String(), nil}func FromRoman(roman string) (int, error) { if !romanRegex.MatchString(roman) { return 0, ErrInvalidRoman } var result int var index int for _, pair := range romanNumParis { for roman[index:index+len(pair.Roman)] == pair.Roman { result += pair.Num index += len(pair.Roman) } } return result, nil}在 Go 中编写测试很简略,只须要在待测试性能所在文件的同级目录中创立一个以_test.go结尾的文件。在该文件中,咱们能够编写一个个测试函数。测试函数名必须是TestXxxx这个模式,而且Xxxx必须以大写字母结尾,另外函数带有一个*testing.T类型的参数: ...

August 4, 2021 · 7 min · jiezi

关于godailylib:Go-每日一库之-gorillasessions

简介上一篇文章《Go 每日一库之 securecookie》中,咱们介绍了 cookie。同时提到 cookie 有两个毛病,一是数据不宜过大,二是平安问题。session 是服务器端的存储计划,能够存储大量的数据,而且不须要向客户端传输,从而解决了这两个问题。然而 session 须要一个能惟一标识用户的 ID,这个 ID 个别寄存在 cookie 中发送到客户端保留,随每次申请一起发送到服务器。cookie 和 session 通常配套应用。 gorilla/sessions是 gorilla web 开发工具包中治理 session 的库。它提供了基于 cookie 和本地文件系统的 session。同时预留扩大接口,能够应用其它的后端存储 session 数据。 本文先介绍sessions提供的两种 session 存储形式,而后通过第三方扩大介绍在多个 Web 服务器实例间如何放弃登录状态。 疾速应用本文代码应用 Go Modules。 创立目录并初始化: $ mkdir gorilla/sessions && cd gorilla/sessions$ go mod init github.com/darjun/go-daily-lib/gorilla/sessions装置gorilla/sessions库: $ go get -u github.com/valyala/gorilla/sessions当初咱们实现在服务器端通过 session 存储一些信息的性能: package mainimport ( "fmt" "github.com/gorilla/mux" "github.com/gorilla/sessions" "log" "net/http" "os")var ( store = sessions.NewFilesystemStore("./", securecookie.GenerateRandomKey(32), securecookie.GenerateRandomKey(32)))func set(w http.ResponseWriter, r *http.Request) { session, _ := store.Get(r, "user") session.Values["name"] = "dj" session.Values["age"] = 18 err := sessions.Save(r, w) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } fmt.Fprintln(w, "Hello World")}func read(w http.ResponseWriter, r *http.Request) { session, _ := store.Get(r, "user") fmt.Fprintf(w, "name:%s age:%d\n", session.Values["name"], session.Values["age"])}func main() { r := mux.NewRouter() r.HandleFunc("/set", set) r.HandleFunc("/read", read) log.Fatal(http.ListenAndServe(":8080", r))}整个程序逻辑比拟清晰,别离在/set和/read门路下挂上设置和读取的处理函数。重点是变量store。咱们调用session.NewFilesystemStore()办法创立了一个*sessions.FilesystemStore类型的对象,它会将咱们的 session 内容存储到文件系统(即本地磁盘上)。咱们须要给NewFilesytemStore()办法传入至多 2 个参数,第一个参数指定 session 存储的本地磁盘门路。后续参数顺次指定hashKey和blockKey(可省略),前者用于验证,后者用于加密,咱们能够应用securecookie生成足够随机的 key,详情见前一篇介绍securecookie的文章。 ...

July 26, 2021 · 4 min · jiezi

关于godailylib:Go-每日一库之-nethttp基础和中间件

简介简直所有的编程语言都以Hello World作为入门程序的示例,其中有一部分以编写一个 Web 服务器作为实战案例的开始。每种编程语言都有很多用于编写 Web 服务器的库,或以规范库,或通过第三方库的形式提供。Go 语言也不例外。本文及后续的文章就去摸索 Go 语言中的各个Web 编程框架,它们的根本应用,浏览它们的源码,比拟它们优缺点。让咱们先从 Go 语言的规范库net/http开始。规范库net/http让编写 Web 服务器的工作变得非常简单。咱们一起摸索如何应用net/http库实现一些常见的性能或模块,理解这些对咱们学习其余的库或框架将会很有帮忙。 Hello World应用net/http编写一个简略的 Web 服务器非常简单: package mainimport ( "fmt" "net/http")func index(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "Hello World")}func main() { http.HandleFunc("/", index) http.ListenAndServe(":8080", nil)}首先,咱们调用http.HandleFunc("/", index)注册门路处理函数,这里将门路/的处理函数设置为index。处理函数的类型必须是: func (http.ResponseWriter, *http.Request)其中*http.Request示意 HTTP 申请对象,该对象蕴含申请的所有信息,如 URL、首部、表单内容、申请的其余内容等。 http.ResponseWriter是一个接口类型: // net/http/server.gotype ResponseWriter interface { Header() Header Write([]byte) (int, error) WriteHeader(statusCode int)}用于向客户端发送响应,实现了ResponseWriter接口的类型显然也实现了io.Writer接口。所以在处理函数index中,能够调用fmt.Fprintln()向ResponseWriter写入响应信息。 仔细阅读net/http包中HandleFunc()函数的源码: func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) { DefaultServeMux.HandleFunc(pattern, handler)}咱们发现它间接调用了一个名为DefaultServeMux对象的HandleFunc()办法。DefaultServeMux是ServeMux类型的实例: type ServeMux struct { mu sync.RWMutex m map[string]muxEntry es []muxEntry // slice of entries sorted from longest to shortest. hosts bool // whether any patterns contain hostnames}var DefaultServeMux = &defaultServeMuxvar defaultServeMux ServeMux像这种提供默认类型实例的用法在 Go 语言的各个库中十分常见,在默认参数就曾经足够的场景中应用默认实现很不便。ServeMux保留了注册的所有门路和处理函数的对应关系。ServeMux.HandleFunc()办法如下: ...

July 14, 2021 · 5 min · jiezi

关于godailylib:Go-每日一库之-bubbletea

简介bubbletea是一个简略、玲珑、能够十分不便地用来编写 TUI(terminal User Interface,控制台界面程序)程序的框架。内置简略的事件处理机制,能够对外部事件做出响应,如键盘按键。一起来看下吧。先看看bubbletea能做出什么成果: 感激kiyonlin举荐。 疾速应用本文代码应用 Go Modules。 创立目录并初始化: $ mkdir bubbletea && cd bubbletea$ go mod init github.com/darjun/go-daily-lib/bubbletea装置bubbletea库: $ go get -u github.com/charmbracelet/bubbleteabubbletea程序都须要有一个实现bubbletea.Model接口的类型: type Model interface { Init() Cmd Update(Msg) (Model, Cmd) View() string}Init()办法在程序启动时会立即调用,它会做一些初始化工作,并返回一个Cmd通知bubbletea要执行什么命令;Update()办法用来响应内部事件,返回一个批改后的模型,和想要bubbletea执行的命令;View()办法用于返回在管制台上显示的文本字符串。上面咱们来实现一个 Todo List。首先定义模型: type model struct { todos []string cursor int selected map[int]struct{}}todos:所有待实现事项;cursor:界面上光标地位;selected:已实现标识。不须要任何初始化工作,实现一个空的Init()办法,并返回nil: import ( tea "github.com/charmbracelet/bubbletea")func (m model) Init() tea.Cmd { return nil}咱们须要响应按键事件,实现Update()办法。按键事件产生时会以相应的tea.Msg为参数调用Update()办法。通过对参数tea.Msg进行类型断言,咱们能够对不同的事件进行对应的解决: func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: switch msg.String() { case "ctrl+c", "q": return m, tea.Quit case "up", "k": if m.cursor > 0 { m.cursor-- } case "down", "j": if m.cursor < len(m.todos)-1 { m.cursor++ } case "enter", " ": _, ok := m.selected[m.cursor] if ok { delete(m.selected, m.cursor) } else { m.selected[m.cursor] = struct{}{} } } } return m, nil}约定: ...

June 16, 2021 · 5 min · jiezi

关于godailylib:为-tunny-提交的一次-PR

背景上周我写了一篇文章Go 每日一库之 ants,深刻分析了ants这个 goroutine 池的实现。在重复浏览了多遍panjf2000对于ants的起源的文章——GMP 并发调度器深度解析之手撸一个高性能 goroutine pool,我感觉播种满满。这篇文章对于了解 Go 的 goroutine 并发机制有很大的参考价值,强烈建议一读。而后我花了几个小时工夫具体浏览了ants的源码,代码写的很棒,十分柔美。而后我写了一遍文章剖析了ants的源码,见ants源码赏析。在写介绍ants的文章和深刻浏览源码过程中,网上的材料提及 Go 语言中 goroutine 池的实现,时常会带上tunny这个库。于是,我又去钻研了tunny的源码,产出一篇文章Go 每日一库之 tunny。 在浏览tunny源码时,我发现有个办法的实现有些问题。我也在Go 每日一库之 tunny中也指出了: 原理咱们晓得 slice 构造中有一个指向数组的指针。假如一开始tunny中有 5 个 worker,示意图如下: 数组中每个元素都是一个指针,指向一个worker构造。而后咱们应用s := s[:4]缩容了,进变成了上面这样: 当初最初一个元素无奈通过切片拜访了,然而又被底层数组援用着,无奈被 Go 运行时的 gc 清理掉,360 软件管家都不行。这就有内存透露了。尽管这个透露并不重大,一是因为量不可能很大,因为 worker 数量无限,二是下次扩容后地位被笼罩就能够开释原 worker 了(因为没有援用它的指针了)。 ants源码中也有相似的操作,然而会将缩容掉的元素置为nil。例如worker_stack.go中的reset()办法: func (wq *workerStack) reset() { for i := 0; i < wq.len(); i++ { wq.items[i].task <- nil wq.items[i] = nil } wq.items = wq.items[:0]}PR确定了问题。我先 fork 了tunny的仓库。点击 fork 按钮: ...

June 14, 2021 · 1 min · jiezi

关于godailylib:Go-每日一库之-tunny

简介之前写过一篇文章介绍了ants这个 goroutine 池实现。过后在网上查看相干材料的时候,发现了另外一个实现tunny。趁着工夫相近,正好钻研一番。也好比拟一下这两个库。那就让咱们开始吧。 疾速开始本文代码应用 Go Modules。 创立目录并初始化: $ mkdir tunny && cd tunny$ go mod init github.com/darjun/go-daily-lib/tunny应用go get从 GitHub 获取tunny库: $ go get -u github.com/Jeffail/tunny为了不便地和ants做一个比照,咱们将ants中的示例从新用tunny实现一遍:还是那个分段求和的例子: const ( DataSize = 10000 DataPerTask = 100)func main() { numCPUs := runtime.NumCPU() p := tunny.NewFunc(numCPUs, func(payload interface{}) interface{} { var sum int for _, n := range payload.([]int) { sum += n } return sum }) defer p.Close() // ...}应用也非常简单,首先创立一个Pool,这里应用tunny.NewFunc()。 第一个参数为池子大小,即同时有多少个 worker (也即 goroutine)在工作,这里设置成逻辑 CPU 个数,对于 CPU 密集型工作,这个值设置太大无意义,反而有可能导致 goroutine 切换频繁而升高性能。 ...

June 10, 2021 · 5 min · jiezi

关于godailylib:Go-每日一库之-ants

简介解决大量并发是 Go 语言的一大劣势。语言内置了不便的并发语法,能够十分不便的创立很多个轻量级的 goroutine 并发解决工作。相比于创立多个线程,goroutine 更轻量、资源占用更少、切换速度更快、无线程上下文切换开销更少。然而受限于资源总量,零碎中可能创立的 goroutine 数量也是受限的。默认每个 goroutine 占用 8KB 内存,一台 8GB 内存的机器满打满算也只能创立 8GB/8KB = 1000000 个 goroutine,更何况零碎还须要保留一部分内存运行日常治理工作,go 运行时须要内存运行 gc、解决 goroutine 切换等。应用的内存超过机器内存容量,零碎会应用替换区(swap),导致性能急速降落。咱们能够简略验证一下创立过多 goroutine 会产生什么: func main() { var wg sync.WaitGroup wg.Add(10000000) for i := 0; i < 10000000; i++ { go func() { time.Sleep(1 * time.Minute) }() } wg.Wait()}在我的机器上(8G内存)运行下面的程序会报errno 1455,即Out of Memory谬误,这很好了解。审慎运行。 另一方面,goroutine 的治理也是一个问题。goroutine 只能本人运行完结,内部没有任何伎俩能够强制j完结一个 goroutine。如果一个 goroutine 因为某种原因没有自行完结,就会呈现 goroutine 泄露。此外,频繁创立 goroutine 也是一个开销。 鉴于上述起因,天然呈现了与线程池一样的需要,即 goroutine 池。个别的 goroutine 池主动治理 goroutine 的生命周期,能够按需创立,动静缩容。向 goroutine 池提交一个工作,goroutine 池会主动安顿某个 goroutine 来解决。 ...

June 4, 2021 · 5 min · jiezi

关于godailylib:Go-每日一库之-reflect

简介反射是一种机制,在编译时不晓得具体类型的状况下,能够透视构造的组成、更新值。应用反射,能够让咱们编写出能对立解决所有类型的代码。甚至是编写这部分代码时还不存在的类型。一个具体的例子就是fmt.Println()办法,能够打印出咱们自定义的构造类型。 尽管,一般来说都不倡议在代码中应用反射。反射影响性能、不易浏览、将编译时就能查看进去的类型问题推延到运行时以 panic 模式体现进去,这些都是反射的毛病。然而,我认为反射是肯定要把握的,起因如下: 很多规范库和第三方库都用到了反射,尽管裸露的接口做了封装,不须要理解反射。然而如果要深入研究这些库,理解实现,浏览源码, 反射是绕不过来的。例如encoding/json,encoding/xml等;如果有一个需要,编写一个能够解决所有类型的函数或办法,咱们就必须会用到反射。因为 Go 的类型数量是有限的,而且能够自定义类型,所以应用类型断言是无奈达成指标的。Go 语言规范库reflect提供了反射性能。 接口反射是建设在 Go 的类型零碎之上的,并且与接口密切相关。 首先简略介绍一下接口。Go 语言中的接口约定了一组办法汇合,任何定义了这组办法的类型(也称为实现了接口)的变量都能够赋值给该接口的变量。 package mainimport "fmt"type Animal interface { Speak()}type Cat struct {}func (c Cat) Speak() { fmt.Println("Meow")}type Dog struct {}func (d Dog) Speak() { fmt.Println("Bark")}func main() { var a Animal a = Cat{} a.Speak() a = Dog{} a.Speak()}下面代码中,咱们定义了一个Animal接口,它约定了一个办法Speak()。而后定义了两个构造类型Cat和Dog,都定义了这个办法。这样,咱们就能够将Cat和Dog对象赋值给Animal类型的变量了。 接口变量蕴含两局部:类型和值,即(type, value)。类型就是赋值给接口变量的值的类型,值就是赋值给接口变量的值。如果晓得接口中存储的变量类型,咱们也能够应用类型断言通过接口变量获取具体类型的值: type Animal interface { Speak()}type Cat struct { Name string}func (c Cat) Speak() { fmt.Println("Meow")}func main() { var a Animal a = Cat{Name: "kitty"} a.Speak() c := a.(Cat) fmt.Println(c.Name)}下面代码中,咱们晓得接口a中保留的是Cat对象,间接应用类型断言a.(Cat)获取Cat对象。然而,如果类型断言的类型与理论存储的类型不符,会间接 panic。所以理论开发中,通常应用另一种类型断言模式c, ok := a.(Cat)。如果类型不符,这种模式不会 panic,而是通过将第二个返回值置为 false 来表明这种状况。 ...

June 1, 2021 · 7 min · jiezi

关于godailylib:Go-每日一库之-fasttemplate

<article class=“article fmt article-content”><h2>简介</h2><p><code>fasttemplate</code>是一个比较简单、易用的小型模板库。<code>fasttemplate</code>的作者valyala另外还开源了不少优良的库,如赫赫有名的<code>fasthttp</code>,后面介绍的<code>bytebufferpool</code>,还有一个重量级的模板库<code>quicktemplate</code>。<code>quicktemplate</code>比规范库中的<code>text/template</code>和<code>html/template</code>要灵便和易用很多,前面会专门介绍它。明天要介绍的<code>fasttemlate</code>只专一于一块很小的畛域——字符串替换。它的指标是为了代替<code>strings.Replace</code>、<code>fmt.Sprintf</code>等办法,提供一个简略,易用,高性能的字符串替换办法。</p><p>本文首先介绍<code>fasttemplate</code>的用法,而后去看看源码实现的一些细节。</p><h2>疾速应用</h2><p>本文代码应用 Go Modules。</p><p>创立目录并初始化:</p><pre><code class=“cmd”>$ mkdir fasttemplate && cd fasttemplate$ go mod init github.com/darjun/go-daily-lib/fasttemplate</code></pre><p>装置<code>fasttemplate</code>库:</p><pre><code class=“cmd”>$ go get -u github.com/valyala/fasttemplate</code></pre><p>编写代码:</p><pre><code class=“golang”>package mainimport ( “fmt” “github.com/valyala/fasttemplate”)func main() { template := name: {{name}}age: {{age}} t := fasttemplate.New(template, “{{”, “}}”) s1 := t.ExecuteString(map[string]interface{}{ “name”: “dj”, “age”: “18”, }) s2 := t.ExecuteString(map[string]interface{}{ “name”: “hjw”, “age”: “20”, }) fmt.Println(s1) fmt.Println(s2)}</code></pre><ul><li>定义模板字符串,应用<code>{{</code>和<code>}}</code>示意占位符,占位符能够在创立模板的时候指定;</li><li>调用<code>fasttemplate.New()</code>创立一个模板对象<code>t</code>,传入开始和完结占位符;</li><li>调用模板对象的<code>t.ExecuteString()</code>办法,传入参数。参数中有各个占位符对应的值。生成最终的字符串。</li></ul><p>运行后果:</p><pre><code class=“cmd”>name: djage: 18</code></pre><p>咱们能够自定义占位符,下面别离应用<code>{{</code>和<code>}}</code>作为开始和完结占位符。咱们能够换成<code>[[</code>和<code>]]</code>,只须要简略批改一下代码即可:</p><pre><code class=“golang”>template := name: [[name]]age: [[age]]t := fasttemplate.New(template, “[[”, “]]”)</code></pre><p>另外,须要留神的是,传入参数的类型为<code>map[string]interface{}</code>,然而<code>fasttemplate</code>只承受类型为<code>[]byte</code>、<code>string</code>和<code>TagFunc</code>类型的值。这也是为什么下面的<code>18</code>要用双引号括起来的起因。</p><p>另一个须要留神的点,<code>fasttemplate.New()</code>返回一个模板对象,如果模板解析失败了,就会间接<code>panic</code>。如果想要本人处理错误,能够调用<code>fasttemplate.NewTemplate()</code>办法,该办法返回一个模板对象和一个谬误。实际上,<code>fasttemplate.New()</code>外部就是调用<code>fasttemplate.NewTemplate()</code>,如果返回了谬误,就<code>panic</code>:</p><pre><code class=“golang”>// src/github.com/valyala/fasttemplate/template.gofunc New(template, startTag, endTag string) *Template { t, err := NewTemplate(template, startTag, endTag) if err != nil { panic(err) } return t}func NewTemplate(template, startTag, endTag string) (*Template, error) { var t Template err := t.Reset(template, startTag, endTag) if err != nil { return nil, err } return &t, nil}</code></pre><p><strong>这其实也是一种习用法,对于不想处理错误的示例程序,间接<code>panic</code>有时也是一种抉择</strong>。例如<code>html.template</code>规范库也提供了<code>Must()</code>办法,个别这样用,遇到解析失败就<code>panic</code>:</p><pre><code class=“golang”>t := template.Must(template.New(“name”).Parse(“html”))</code></pre><p><strong>占位符两头外部不要加空格!!!</strong></p><p><strong>占位符两头外部不要加空格!!!</strong></p><p><strong>占位符两头外部不要加空格!!!</strong></p><h2>快捷方式</h2><p>应用<code>fasttemplate.New()</code>定义模板对象的形式,咱们能够屡次应用不同的参数去做替换。然而,有时候咱们要做大量一次性的替换,每次都定义模板对象显得比拟繁琐。<code>fasttemplate</code>也提供了一次性替换的办法:</p><pre><code class=“golang”>func main() { template := name: [name]age: [age] s := fasttemplate.ExecuteString(template, “[”, “]”, map[string]interface{}{ “name”: “dj”, “age”: “18”, }) fmt.Println(s)}</code></pre><p>应用这种形式,咱们须要同时传入模板字符串、开始占位符、完结占位符和替换参数。</p><h2><code>TagFunc</code></h2><p><code>fasttemplate</code>提供了一个<code>TagFunc</code>,能够给替换减少一些逻辑。<code>TagFunc</code>是一个函数:</p><pre><code class=“golang”>type TagFunc func(w io.Writer, tag string) (int, error)</code></pre><p>在执行替换的时候,<code>fasttemplate</code>针对每个占位符都会调用一次<code>TagFunc</code>函数,<code>tag</code>即占位符的名称。看上面程序:</p><pre><code class=“golang”>func main() { template := name: {{name}}age: {{age}} t := fasttemplate.New(template, “{{”, “}}”) s := t.ExecuteFuncString(func(w io.Writer, tag string) (int, error) { switch tag { case “name”: return w.Write([]byte(“dj”)) case “age”: return w.Write([]byte(“18”)) default: return 0, nil } }) fmt.Println(s)}</code></pre><p>这其实就是<code>get-started</code>示例程序的<code>TagFunc</code>版本,依据传入的<code>tag</code>写入不同的值。如果咱们去查看源码就会发现,实际上<code>ExecuteString()</code>最终还是会调用<code>ExecuteFuncString()</code>。<code>fasttemplate</code>提供了一个规范的<code>TagFunc</code>:</p><pre><code class=“golang”>func (t *Template) ExecuteString(m map[string]interface{}) string { return t.ExecuteFuncString(func(w io.Writer, tag string) (int, error) { return stdTagFunc(w, tag, m) })}func stdTagFunc(w io.Writer, tag string, m map[string]interface{}) (int, error) { v := m[tag] if v == nil { return 0, nil } switch value := v.(type) { case []byte: return w.Write(value) case string: return w.Write([]byte(value)) case TagFunc: return value(w, tag) default: panic(fmt.Sprintf(“tag=%q contains unexpected value type=%#v. Expected []byte, string or TagFunc”, tag, v)) }}</code></pre><p>规范的<code>TagFunc</code>实现也非常简单,就是从参数<code>map[string]interface{}</code>中取出对应的值做相应解决,如果是<code>[]byte</code>和<code>string</code>类型,间接调用<code>io.Writer</code>的写入办法。如果是<code>TagFunc</code>类型则间接调用该办法,将<code>io.Writer</code>和<code>tag</code>传入。其余类型间接<code>panic</code>抛出谬误。</p><p>如果模板中的<code>tag</code>在参数<code>map[string]interface{}</code>中不存在,有两种解决形式:</p><ul><li>间接疏忽,相当于替换成了空字符串<code>""</code>。规范的<code>stdTagFunc</code>就是这样解决的;</li><li>保留原始<code>tag</code>。<code>keepUnknownTagFunc</code>就是做这个事件的。</li></ul><p><code>keepUnknownTagFunc</code>代码如下:</p><pre><code class=“golang”>func keepUnknownTagFunc(w io.Writer, startTag, endTag, tag string, m map[string]interface{}) (int, error) { v, ok := m[tag] if !ok { if _, err := w.Write(unsafeString2Bytes(startTag)); err != nil { return 0, err } if _, err := w.Write(unsafeString2Bytes(tag)); err != nil { return 0, err } if _, err := w.Write(unsafeString2Bytes(endTag)); err != nil { return 0, err } return len(startTag) + len(tag) + len(endTag), nil } if v == nil { return 0, nil } switch value := v.(type) { case []byte: return w.Write(value) case string: return w.Write([]byte(value)) case TagFunc: return value(w, tag) default: panic(fmt.Sprintf(“tag=%q contains unexpected value type=%#v. Expected []byte, string or TagFunc”, tag, v)) }}</code></pre><p>后半段解决与<code>stdTagFunc</code>一样,函数前半部分如果<code>tag</code>未找到。间接写入<code>startTag</code> + <code>tag</code> + <code>endTag</code>作为替换的值。</p><p>咱们后面调用的<code>ExecuteString()</code>办法应用<code>stdTagFunc</code>,即间接将未辨认的<code>tag</code>替换成空字符串。如果想保留未辨认的<code>tag</code>,改为调用<code>ExecuteStringStd()</code>办法即可。该办法遇到未辨认的<code>tag</code>会保留:</p><pre><code class=“golang”>func main() { template := name: {{name}}age: {{age}} t := fasttemplate.New(template, “{{”, “}}”) m := map[string]interface{}{“name”: “dj”} s1 := t.ExecuteString(m) fmt.Println(s1) s2 := t.ExecuteStringStd(m) fmt.Println(s2)}</code></pre><p>参数中短少<code>age</code>,运行后果:</p><pre><code class=“cmd”>name: djage:name: djage: {{age}}</code></pre><h2>带<code>io.Writer</code>参数的办法</h2><p>后面介绍的办法最初都是返回一个字符串。办法名中都有<code>String</code>:<code>ExecuteString()/ExecuteFuncString()</code>。</p><p>咱们能够间接传入一个<code>io.Writer</code>参数,将后果字符串调用这个参数的<code>Write()</code>办法间接写入。这类办法名中没有<code>String</code>:<code>Execute()/ExecuteFunc()</code>:</p><pre><code class=“golang”>func main() { template := name: {{name}}age: {{age}} t := fasttemplate.New(template, “{{”, “}}”) t.Execute(os.Stdout, map[string]interface{}{ “name”: “dj”, “age”: “18”, }) fmt.Println() t.ExecuteFunc(os.Stdout, func(w io.Writer, tag string) (int, error) { switch tag { case “name”: return w.Write([]byte(“hjw”)) case “age”: return w.Write([]byte(“20”)) } return 0, nil })}</code></pre><p>因为<code>os.Stdout</code>实现了<code>io.Writer</code>接口,能够间接传入。后果间接写到<code>os.Stdout</code>中。运行:</p><pre><code class=“cmd”>name: djage: 18name: hjwage: 20</code></pre><h2>源码剖析</h2><p>首先看模板对象的构造和创立:</p><pre><code class=“golang”>// src/github.com/valyala/fasttemplate/template.gotype Template struct { template string startTag string endTag string texts [][]byte tags []string byteBufferPool bytebufferpool.Pool}func NewTemplate(template, startTag, endTag string) (*Template, error) { var t Template err := t.Reset(template, startTag, endTag) if err != nil { return nil, err } return &t, nil}</code></pre><p>模板创立之后会调用<code>Reset()</code>办法初始化:</p><pre><code class=“golang”>func (t *Template) Reset(template, startTag, endTag string) error { t.template = template t.startTag = startTag t.endTag = endTag t.texts = t.texts[:0] t.tags = t.tags[:0] if len(startTag) == 0 { panic(“startTag cannot be empty”) } if len(endTag) == 0 { panic(“endTag cannot be empty”) } s := unsafeString2Bytes(template) a := unsafeString2Bytes(startTag) b := unsafeString2Bytes(endTag) tagsCount := bytes.Count(s, a) if tagsCount == 0 { return nil } if tagsCount+1 > cap(t.texts) { t.texts = make([][]byte, 0, tagsCount+1) } if tagsCount > cap(t.tags) { t.tags = make([]string, 0, tagsCount) } for { n := bytes.Index(s, a) if n < 0 { t.texts = append(t.texts, s) break } t.texts = append(t.texts, s[:n]) s = s[n+len(a):] n = bytes.Index(s, b) if n < 0 { return fmt.Errorf(“Cannot find end tag=%q in the template=%q starting from %q”, endTag, template, s) } t.tags = append(t.tags, unsafeBytes2String(s[:n])) s = s[n+len(b):] } return nil}</code></pre><p>初始化做了上面这些事件:</p><ul><li>记录开始和完结占位符;</li><li>解析模板,将文本和<code>tag</code>切离开,别离寄存在<code>texts</code>和<code>tags</code>切片中。后半段的<code>for</code>循环就是做的这个事件。</li></ul><p>代码细节点:</p><ul><li>先统计占位符一共多少个,一次结构对应大小的文本和<code>tag</code>切片,留神结构正确的模板字符串文本切片肯定比<code>tag</code>切片大 1。像这样<code>| text | tag | text | … | tag | text |</code>;</li><li>为了防止内存拷贝,应用<code>unsafeString2Bytes</code>让返回的字节切片间接指向<code>string</code>外部地址。</li></ul><p>看下面的介绍,貌似有很多办法。实际上外围的办法就一个<code>ExecuteFunc()</code>。其余的办法都是间接或间接地调用它:</p><pre><code class=“golang”>// src/github.com/valyala/fasttemplate/template.gofunc (t *Template) Execute(w io.Writer, m map[string]interface{}) (int64, error) { return t.ExecuteFunc(w, func(w io.Writer, tag string) (int, error) { return stdTagFunc(w, tag, m) })}func (t *Template) ExecuteStd(w io.Writer, m map[string]interface{}) (int64, error) { return t.ExecuteFunc(w, func(w io.Writer, tag string) (int, error) { return keepUnknownTagFunc(w, t.startTag, t.endTag, tag, m) })}func (t *Template) ExecuteFuncString(f TagFunc) string { s, err := t.ExecuteFuncStringWithErr(f) if err != nil { panic(fmt.Sprintf(“unexpected error: %s”, err)) } return s}func (t *Template) ExecuteFuncStringWithErr(f TagFunc) (string, error) { bb := t.byteBufferPool.Get() if _, err := t.ExecuteFunc(bb, f); err != nil { bb.Reset() t.byteBufferPool.Put(bb) return “”, err } s := string(bb.Bytes()) bb.Reset() t.byteBufferPool.Put(bb) return s, nil}func (t *Template) ExecuteString(m map[string]interface{}) string { return t.ExecuteFuncString(func(w io.Writer, tag string) (int, error) { return stdTagFunc(w, tag, m) })}func (t *Template) ExecuteStringStd(m map[string]interface{}) string { return t.ExecuteFuncString(func(w io.Writer, tag string) (int, error) { return keepUnknownTagFunc(w, t.startTag, t.endTag, tag, m) })}</code></pre><p><code>Execute()</code>办法结构一个<code>TagFunc</code>调用<code>ExecuteFunc()</code>,外部应用<code>stdTagFunc</code>:</p><pre><code class=“golang”>func(w io.Writer, tag string) (int, error) { return stdTagFunc(w, tag, m)}</code></pre><p><code>ExecuteStd()</code>办法结构一个<code>TagFunc</code>调用<code>ExecuteFunc()</code>,外部应用<code>keepUnknownTagFunc</code>:</p><pre><code class=“golang”>func(w io.Writer, tag string) (int, error) { return keepUnknownTagFunc(w, t.startTag, t.endTag, tag, m)}</code></pre><p><code>ExecuteString()</code>和<code>ExecuteStringStd()</code>办法调用<code>ExecuteFuncString()</code>办法,而<code>ExecuteFuncString()</code>办法又调用了<code>ExecuteFuncStringWithErr()</code>办法,<code>ExecuteFuncStringWithErr()</code>办法外部应用<code>bytebufferpool.Get()</code>取得一个<code>bytebufferpoo.Buffer</code>对象去调用<code>ExecuteFunc()</code>办法。所以外围就是<code>ExecuteFunc()</code>办法:</p><pre><code class=“golang”>func (t *Template) ExecuteFunc(w io.Writer, f TagFunc) (int64, error) { var nn int64 n := len(t.texts) - 1 if n == -1 { ni, err := w.Write(unsafeString2Bytes(t.template)) return int64(ni), err } for i := 0; i < n; i++ { ni, err := w.Write(t.texts[i]) nn += int64(ni) if err != nil { return nn, err } ni, err = f(w, t.tags[i]) nn += int64(ni) if err != nil { return nn, err } } ni, err := w.Write(t.texts[n]) nn += int64(ni) return nn, err}</code></pre><p>整个逻辑也很清晰,<code>for</code>循环就是<code>Write</code>一个<code>texts</code>元素,以以后的<code>tag</code>执行<code>TagFunc</code>,索引 +1。最初写入最初一个<code>texts</code>元素,实现。大略是这样:</p><pre><code class=“golang”>| text | tag | text | tag | text | … | tag | text |</code></pre><p>注:<code>ExecuteFuncStringWithErr()</code>办法应用到了后面文章介绍的<code>bytebufferpool</code>,感兴趣能够回去翻看。</p><h2>总结</h2><p>能够应用<code>fasttemplate</code>实现<code>strings.Replace</code>和<code>fmt.Sprintf</code>的工作,而且<code>fasttemplate</code>灵活性更高。代码清晰易懂,值得一看。</p><p>吐槽:对于命名,<code>Execute()</code>办法外面应用<code>stdTagFunc</code>,<code>ExecuteStd()</code>办法外面应用<code>keepUnknownTagFunc</code>办法。我想是不是把<code>stdTagFunc</code>改名为<code>defaultTagFunc</code>好一点?</p><p>大家如果发现好玩、好用的 Go 语言库,欢送到 Go 每日一库 GitHub 上提交 issue</p><h2>参考</h2><ol><li>fasttemplate GitHub:github.com/valyala/fasttemplate</li><li>Go 每日一库 GitHub:https://github.com/darjun/go-daily-lib</li></ol><h2>我</h2><p>我的博客:https://darjun.github.io</p><p>欢送关注我的微信公众号【GoUpUp】,独特学习,一起提高~</p></article> ...

May 25, 2021 · 5 min · jiezi