• [技术干货] 一文带你了解Golang中的泛型【转】
    自从2022年 Golang 1.18 发布至今已有一年多了,在1.18版本中增加了非常重磅的一个功能,那就是泛型!Golang官方也对泛型格外重视:“Generics are the biggest change we’ve made to Go since the first open source release”(泛型是自第一个开源版本以来我们对 Go 所做的最大改变)然而由于平时工作项目所用Go版本较为古老,一直没有大范围应用泛型这个特性,虽然之前刚发布时就了解学习过,但是也有些模糊了,这里还是希望能够记录一下Golang泛型,以备后续参考,我讲基于Golang官方文档、博客、youtube演讲等来进行学习。什么是泛型泛型是一种可以编写独立于使用的特定类型的代码的方法,可以通过编写函数或类型来使用一组类型中的任何一个。泛型为Golang增添了三个重要功能:函数和类型的类型参数将接口类型定义为类型集,包括没有方法的类型。也就是我们可以定义类型集和方法集类型推断,允许函数调用时省略类型参数我们从类型参数开始逐步了解类型参数 Type parameters类型参数让我们可以参数化函数或者具有类型的类型,与普通的参数列表类似,类型参数使用方括号来表示函数中使用类型参数这里有一个常见的取最小值函数,我们经常会在代码中写(新版的Golang官方库已经支持了max以及min):func Min(x, y float64) float64 {     if x < y {         return x     }     return y }我们可以通过类型参数来替换 float64 类型来使这个函数更加通用,让这个函数不仅仅适用于 float64 类型,可以这样来做:import "golang.org/x/exp/constraints" func Min[T constraints.Ordered](x, y T) T {     if x < y {         return x     }     return y } m := Min[int](2, 3)在这里我们使用了类型参数 T 来替换 float64 类型使得函数通用,由于 T 是一个新的类型,所以我们要在 [] 中声明它。函数定义好后,与普通的函数调用类似,我们需要传入函数的实参以及创建一个接收值来接受函数实际返回的结果,不同的是,我们需要在 [] 传入具体的类型值,向函数提供类型参数 int 成为实例化。实例化将会分两步进行:编译器将整个泛型函数或类型中的所有类型实参进行替换编译器验证每个类型参数是否满足了各自的约束如果编译器在第二步执行失败,实例化就会失败且程序会fail。我们也可以直接传入类型参数来实例化函数,而不需要传入具体实参进行实际调用,实例化过后我们就可以像普通函数调用一样来调用这个实例化过后的函数了:fmin := Min[float64] m := fmin(2.71, 3.14)类型中使用类型参数前面是一个函数中使用类型参数的例子,还有一个在类型中使用类型参数的例子:type Tree[T interface{}] struct {    left, right *Tree[T]    value       T}func (t *Tree[T]) Lookup(x T) *Tree[T] { ... }var stringTree Tree[string]这是一个通用的二叉树类型定义,我们再次采用了类型参数 T 来作为一个通用性的数据类型,这里定义了类型 Tree[T] 同时定义了它所具有的方法 Lookup(x T) *Tree[T]。最后一行通过 var 对变量 stringTree 做了一次实例化,传入参数类型为 string类型集func min(x,y float64)float64func Gmin[T constraints.Ordered](x, y T) T普通函数 min() 每个参数值都有一个类型,例如min函数中,限定了 x,y 及 返回值只有在 float64 类型时才有效;而函数 Gmin() 类型参数列表中每个类型参数都有一个类型,由于类型参数本身就是一种类型,因此类型参数的类型定义了类型集,这种元类型告诉了我们那些类型对该参数类型有效,因此这个元类型实际定义了类型集,我们可以称之为类型约束。在 Gmin() 中,类型越是是从约束包中导入的,这个包也是 Golang 标准库中新增的包。这个Ordered约束描述了具有可排序值的所有类型的集合,或者换句话说,约束了能够使用 < 运算符(或 <= 、 > 等)进行比较的类型范围。所以只有具有可排序值的类型才能传递给GMin,在GMin函数体中,该类型参数的值可以用于与 < 等运算符进行比较。
  • [技术干货] Go整合ElasticSearch的示例代码【转】
    go整合elasticsearch基于docker搭建开发环境在开发之前我们首先需要借助docker来构建我们的开发环境,先创建一个文件名称为docker-compose.yaml, 里面写入下面的内容:---   version: "3"   services:     elasticsearch:       image: docker.elastic.co/elasticsearch/elasticsearch:7.10.0       container_name: es01       environment:         - node.name=es01         - cluster.name=docker-cluster         - bootstrap.memory_lock=true         - "ES_JAVA_OPTS=-Xms512m -Xmx512m"         - discovery.type=single-node       ulimits:         memlock:           soft: -1           hard: -1       volumes:         - esdata:/usr/share/elasticsearch/data       ports:         - 9200:9200     kibana:       image: docker.elastic.co/kibana/kibana:7.10.0       ports:         - 5601:5601       depends_on:         - elasticsearch   volumes:     esdata:       driver: local使用docker-compose up -d 启动容器,之后在浏览器中分别验证es和kibana的运行状态验证es:http://localhost:9200/验证kibana:http://localhost:5601检查客户端apipackage main import (     "fmt"     "github.com/elastic/go-elasticsearch/v7" ) func main() {     es, err := elasticsearch.NewDefaultClient()     if err != nil {         fmt.Println(err)         return     }     res, err := es.Info()     if err != nil {         fmt.Println(err)         return     }     defer res.Body.Close()     fmt.Println(res) }索引相关操作创建索引package main import (     "context"     "fmt"     "github.com/elastic/go-elasticsearch/v7"     "log" ) func main() {     cfg := elasticsearch.Config{         Addresses: []string{             "http://localhost:9200",         },     }     es, err := elasticsearch.NewClient(cfg)     if err != nil {         log.Fatalf("Error creating the client: %s", err)     }     indexName := "test_20230726"     res, err := es.Indices.Create(         indexName,         es.Indices.Create.WithContext(context.Background()),         es.Indices.Create.WithPretty())     if err != nil {         log.Fatalf("Error creating the index: %s", err)     }     defer res.Body.Close()     fmt.Println(res.String()) }删除索引package main import (     "context"     "fmt"     "github.com/elastic/go-elasticsearch/v7"     "log" ) func main() {     cfg := elasticsearch.Config{         Addresses: []string{             "http://localhost:9200",         },     }     es, err := elasticsearch.NewClient(cfg)     if err != nil {         log.Fatalf("Error creating the client: %s", err)     }     indexName := "test_20230726"     res, err := es.Indices.Delete(         []string{indexName},         es.Indices.Delete.WithContext(context.Background()),         es.Indices.Delete.WithIgnoreUnavailable(true),         es.Indices.Delete.WithPretty(),     )     if err != nil {         log.Fatalf("Error deleting the index: %s", err)     }     defer res.Body.Close()     fmt.Println(res.String()) }修改索引package main import (     "bytes"     "context"     "encoding/json"     "fmt"     "github.com/elastic/go-elasticsearch/v7"     "github.com/elastic/go-elasticsearch/v7/esapi"     "log" ) func main() {     cfg := elasticsearch.Config{         Addresses: []string{             "http://localhost:9200",         },     }     es, err := elasticsearch.NewClient(cfg)     if err != nil {         log.Fatalf("Error creating the client: %s", err)     }     indexName := "your_index_name"     documentID := "your_document_id"     // 准备文档数据     doc := Document{         Title: "Document Title",         Body:  "This is the body of the document.",     }     // 将文档数据序列化为JSON字节     data, err := json.Marshal(doc)     if err != nil {         log.Fatalf("Error marshaling document: %s", err)     }     // 创建PUT请求     req := esapi.IndexRequest{         Index:      indexName,         DocumentID: documentID,         Body:       bytes.NewReader(data),     }     // 发送PUT请求     res, err := req.Do(context.Background(), es)     if err != nil {         log.Fatalf("Error indexing document: %s", err)     }     defer res.Body.Close()     fmt.Println(res.String()) }查询索引列表package main import (     "encoding/json"     "fmt"     "github.com/elastic/go-elasticsearch/v7"     "log" ) func main() {     cfg := elasticsearch.Config{         Addresses: []string{             "http://localhost:9200",         },     }     es, err := elasticsearch.NewClient(cfg)     if err != nil {         log.Fatalf("Error creating the client: %s", err)     }     res, err := es.Indices.Get([]string{"_all"})     if err != nil {         log.Fatalf("Error getting indices: %s", err)     }     defer res.Body.Close()     if res.IsError() {         log.Fatalf("Error response: %s", res.String())     }     var result map[string]interface{}     if err := json.NewDecoder(res.Body).Decode(&result); err != nil {         log.Fatalf("Error parsing the response body: %s", err)     }     indices, ok := result["test_20230726"].(map[string]interface{})     if !ok {         log.Fatalf("Invalid indices format in the response")     }     for index := range indices {         fmt.Println(index)     } }插入文档package main import (     "bytes"     "context"     "encoding/json"     "fmt"     "github.com/elastic/go-elasticsearch/v7"     "github.com/elastic/go-elasticsearch/v7/esapi"     "log" ) type Document struct {     Title string `json:"title"`     Body  string `json:"body"` } func main() {     cfg := elasticsearch.Config{         Addresses: []string{             "http://localhost:9200",         },     }     es, err := elasticsearch.NewClient(cfg)     if err != nil {         log.Fatalf("Error creating the client: %s", err)     }     indexName := "test_20230726"     documentID := "20230726"     doc := Document{         Title: "Document Title",         Body:  "This is the body of the document.",     }     data, err := json.Marshal(doc)     if err != nil {         log.Fatalf("Error marshaling document: %s", err)     }     req := esapi.IndexRequest{         Index:      indexName,         DocumentID: documentID,         Body:       bytes.NewReader(data),     }     res, err := req.Do(context.Background(), es)     if err != nil {         log.Fatalf("Error indexing document: %s", err)     }     defer res.Body.Close()     fmt.Println(res.String())
  • [技术干货] Golang仿ps实现获取Linux进程信息【转】
    原理遍历读取/proc/获取所有进程IDcat /proc/5181/stat中前四列分别为进程PID,进程名,进程状态,父进程PIDGo代码1.获取/proc/下面所有文件名+文件夹名为数字的名字2.读取/proc/xxx/stat获取进程信息输出package main import (     "fmt"     "io/ioutil"     "log"     "regexp"     "sort"     "strconv" ) func main() {     var process []int     var validId = regexp.MustCompile("^[0-9]+$")     infoList, err := ioutil.ReadDir("/proc")     if err != nil {         log.Println(infoList)     }     for _, info := range infoList {         if info.IsDir() && validId.MatchString(info.Name()) {             p, _ := strconv.Atoi(info.Name())             process = append(process, p)         }     }     sort.Ints(process)     statRe := regexp.MustCompile(`([0-9]+) \((.+?)\) [a-zA-Z]+ ([0-9]+)`)     fmt.Printf("%6s\t%6s\t%s\n", "PID", "PPID", "NAME")     for _, p := range process {         b, err := ioutil.ReadFile(fmt.Sprintf("/proc/%d/stat", p))         if err != nil {             continue         }         matches := statRe.FindStringSubmatch(string(b))         fmt.Printf("%6s\t%6s\t%s\n", matches[1], matches[3], matches[2])     } }package main import (     "fmt"     "io/ioutil"     "log"     "regexp"     "sort"     "strconv" ) func main() {     var process []int     var validId = regexp.MustCompile("^[0-9]+$")     infoList, err := ioutil.ReadDir("/proc")     if err != nil {         log.Println(infoList)     }     for _, info := range infoList {         if info.IsDir() && validId.MatchString(info.Name()) {             p, _ := strconv.Atoi(info.Name())             process = append(process, p)         }     }     sort.Ints(process)     statRe := regexp.MustCompile(`([0-9]+) \((.+?)\) [a-zA-Z]+ ([0-9]+)`)     fmt.Printf("%6s\t%6s\t%s\n", "PID", "PPID", "NAME")     for _, p := range process {         b, err := ioutil.ReadFile(fmt.Sprintf("/proc/%d/stat", p))         if err != nil {             continue         }         matches := statRe.FindStringSubmatch(string(b))         fmt.Printf("%6s\t%6s\t%s\n", matches[1], matches[3], matches[2])     } }
  • [技术干货] Go数据结构之HeapMap实现指定Key删除堆【转】
    堆(Heap)堆(Heap),又称为优先队列(Priority Queue)。尽管名为优先队列,但堆并不是队列。在队列中,我们可以进行的操作是向队列中添加元素和按照元素进入队列的顺序取出元素。而在堆中,我们不是按照元素进入队列的先后顺序,而是按照元素的优先级取出元素。问题背景在Linux内核中,调度器根据各个进程的优先级来进行程序的执行调度。在操作系统运行时,通常会有很多个不同的进程,各自优先级也不相同。调度器的作用是让优先级高的进程得到优先执行,而优先级较低的则需要等待。堆是一种适用于实现这种调度器的数据结构。需要提一下,现在Linux内核的调度器使用的是基于红黑树的CFS(Completely Fair Scheduler)。二叉堆的概念我们常用的二叉堆是一颗任意节点的优先级不小于其子节点的完全二叉树。完全二叉树的定义如下:若设二叉树的高度为h,除第h层外,其它各层(1~h-1)的结点数都达到最大个数,第h层从右向左连续缺若干结点,这就是完全二叉树。比如下图就是一颗完全二叉树:            10          /     \                   15        30        /  \      /  \    40    50  100   40现在假设保存的数值越小的节点的优先级越高,那么上图就是一个堆。我们将任意节点不大于其子节点的堆叫做最小堆或小根堆,将任意节点不小于其子节点的堆叫做最大堆或大根堆。因此,上图就是一个小根堆。优先级队列的实现通过使用Go语言中的container/heap包,我们可以轻松地实现一个优先级队列。这个队列可以用于解决许多问题,如任务调度、事件处理等。通过设置每个项的优先级,我们可以确保在处理队列时按照指定的顺序进行操作。Item通过定义Item结构体来表示优先级队列中的项。每个项具有值(value)和优先级(priority)。index表示项在优先级队列中的索引。// Item represents an item in the priority queue. type Item struct {  value    int // 项的值。  priority int // 项的优先级。  index    int // 项在队列中的索引。 }PriorityQueuePriorityQueue是一个切片类型,实现了heap.Interface接口。它提供了用于操作优先级队列的方法,如插入、删除和修改。Len方法返回优先级队列的长度。Less方法比较两个项的优先级。Swap方法交换两个项在优先级队列中的位置。Push方法向优先级队列中添加一个项。Pop方法移除并返回优先级队列中的最小项。Update方法用于修改项的优先级并更新其在优先级队列中的位置。// PriorityQueue 实现了 heap.Interface 接口。 type PriorityQueue []*Item // Len 返回优先级队列的长度。 func (pq PriorityQueue) Len() int {      return len(pq) } // Less 比较优先级队列中的两个项。 func (pq PriorityQueue) Less(i, o int) bool {      return pq[i].priority < pq[o].priority } // Swap 交换优先级队列中的两个项。 func (pq PriorityQueue) Swap(i, o int) {      pq[i], pq[o] = pq[o], pq[i]      pq[i].index = i      pq[o].index = o} // Push 向优先级队列中添加一个项。 func (pq *PriorityQueue) Push(x interface{}) {      item := x.(*Item)      item.index = len(*pq)      *pq = append(*pq, item) } // Pop 移除并返回优先级队列中的最小项。 func (pq *PriorityQueue) Pop() interface{} {      old := *pq      n := len(old)      item := old[n-1]      old[n-1] = nil // 避免内存泄漏      item.index = -1      *pq = old[0 : n-1]      return item } // Update 修改项的优先级并更新其在优先级队列中的位置。 func (pq *PriorityQueue) Update(item *Item, value, priority int) {  item.value = value  item.priority = priority  heap.Fix(pq, item.index) }改进但是我们经常有一种场景,需要堆的快速求最值的性质,又需要能够支持快速的访问元素,特别是删除元素。 如果我们要查找堆中的某个元素,需要遍历一遍。非常麻烦。比如延迟任务的场景,我们可以使用堆对任务的到期时间戳进行排序,从而实现到期任务自动执行,但是它没办法支持删除一个延迟任务的需求。HeapMap一种能够快速随机访问元素的数据结构是哈希表。使用哈希表实现的map可以在O(1)的时间复杂度下进行随机访问。另外,堆结构可以在O(log(n))的时间复杂度下删除元素,前提是知道要删除的元素的下标。因此,我们可以将这两个数据结构结合起来使用。使用哈希表记录堆中每个元素的下标,同时使用堆来获取最值元素。在PriorityQueue中定义一个dataMapdataMap是一个用于存储队列中的项的映射表,它的好处是可以根据项的键快速地查找到对应的项。 在PriorityQueue中,有一个数据切片data,用于存储队列中的项,并且用一个索引值index来表示项在切片中的位置。dataMap则以项的键作为索引,将项的指针映射到该键上。使用dataMap的好处是可以快速地根据键找到对应的项,而不需要遍历整个切片。这对于需要频繁查找和修改项的场景非常重要,可以提高代码的效率。如果没有dataMap,想要根据键找到对应的项则需要遍历整个切片进行查找,时间复杂度将为O(n)。而使用dataMap可以将查找的时间复杂度降低到O(1),提高代码的性能。另外,需要注意的是dataMap必须与data切片保持同步,即在对切片进行修改时,需要同时更新dataMap,保持两者的一致性。否则,在使用dataMap时会出现不一致的情况,导致错误的结果。因此,在使用PriorityQueue时,需要确保维护dataMap和data切片的一致性。push在Push时需要保证Key值唯一func (pq *PriorityQueue) Push(i *Item) error {    if i == nil || i.Key == "" {       return errors.New("error adding item: Item Key is required")    }    pq.lock.Lock()    defer pq.lock.Unlock()    if _, ok := pq.dataMap[i.Key]; ok {       return ErrDuplicateItem    }    // Copy the item value(s) so that modifications to the source item does not    // affect the item on the queue    clone, err := copystructure.Copy(i)    if err != nil {       return err    }    pq.dataMap[i.Key] = clone.(*Item)    heap.Push(&pq.data, clone)    return nil }popPopByKey方法可以根据Key查找并移除对应的元素// PopByKey searches the queue for an item with the given key and removes it // from the queue if found. Returns nil if not found. This method must fix the // queue after removing any key. func (pq *PriorityQueue) PopByKey(key string) (*Item, error) {    pq.lock.Lock()    defer pq.lock.Unlock()    item, ok := pq.dataMap[key]    if !ok {       return nil, nil    }    // Remove the item the heap and delete it from the dataMap    itemRaw := heap.Remove(&pq.data, item.index)    delete(pq.dataMap, key)    if itemRaw != nil {       if i, ok := itemRaw.(*Item); ok {          return i, nil       }    }    return nil, nil }
  • [技术干货] 基于Golang编写贪吃蛇游戏【转】
    基于终端库termbox-go做了个贪吃蛇游戏, 功能较简单,代码约160行左右一:原理介绍1. 绘制原理存储好蛇身和食物坐标都存储在Snake结构中定时300毫秒执行移动蛇身/生成食物,然后清空终端再重新根据坐标绘制点●达到模拟动画效果type Location struct {     X int     Y int } type Snake struct {     Body      []Location     Food      Location     ...... } func Draw(s *Snake) {     termbox.Clear(termbox.ColorDefault, termbox.ColorDefault)     for _, location := range s.Body {         termbox.SetCell(location.X, location.Y, '●', termbox.ColorGreen, termbox.ColorDefault)     }     termbox.SetCell(s.Food.X, s.Food.Y, '●', termbox.ColorRed, termbox.ColorDefault)     termbox.Flush() }2.贪吃蛇移动过程原理很简单,根据当前行走方向,追加一个点到[]Localtion如果蛇头位置不是食物位置,删除[]Localtion第一个点, 添加一个点(最后一个点位置+1)相当于行走了如果恰好是食物位置,添加一个点(最后一个点位置+1),再随机生成食物// 移动一步, 如果碰壁返回false, 否则返回true func (s *Snake) Move() bool {     head := s.GetHead()     switch s.Direction {     case DIRECTION_UP:         s.Body = append(s.Body, Location{head.X, head.Y - 1})     case DIRECTION_DOWN:         s.Body = append(s.Body, Location{head.X, head.Y + 1})     case DIRECTION_LEFT:         s.Body = append(s.Body, Location{head.X - 1, head.Y})     case DIRECTION_RIGHT:         s.Body = append(s.Body, Location{head.X + 1, head.Y})     }     head = s.GetHead()     // 蛇头到达食物位置时标记食物已吃,并且追加到蛇尾(s.Body[0]不用剔除, 否则剔除)     if head == s.Food {         s.FoodEated = true         s.RandomFood()         s.Score += 10     } else {         s.Body = s.Body[1:]     }     return 0 <= head.X && head.X <= s.MaxX && 0 <= head.Y && head.Y <= s.MaxY }3.生成食物过程仅需要注意是否生成在蛇身本身,是的话再生成// 判断生成的食物坐标是否在蛇身上 func (s *Snake) isFoodInSnake(location Location) bool {     for _, l := range s.Body {         if l == location {             return true         }     }     return false } // 生成食物 func (s *Snake) RandomFood() {     w, h := termbox.Size()     // 上下两边留点空隙     location := Location{rand.Intn(w-10) + 5, rand.Intn(h-10) + 5}     for s.isFoodInSnake(location) {         location = Location{rand.Intn(w), rand.Intn(h)}     }     s.Food = location }
  • [技术干货] 用Go获取短信验证码的示例代码【转】
    要用Go获取短信验证码,通常需要连接到一个短信服务提供商的API,并通过该API发送请求来获取验证码。由于不同的短信服务提供商可能具有不同的API和授权方式,我将以一个简单的示例介绍如何使用Go语言来获取短信验证码。在这个示例中,我们将使用中昱维信作为短信服务提供商。1.注册账号并获取API密钥注册并登录你的短信平台,然后获取AppID和AppKey,注册地址在代码里2.创建验证码模版创建验证码模版,获取验证码模版id3.使用Go代码调用短信服务接口// 平台注册地址 vip.veesing.com package main import (   "fmt"   "strings"   "net/http"   "io/ioutil" ) func main() {   url := "https://vip.veesing.com/smsApi/verifyCode"   method := "POST"   // 替换示例代码中的"YOUR_APP_ID"、"YOUR_APP_KEY"、"YOUR_TEMPLATE_ID"、"YOUR_PHONE"、"YOUR_CODE"为你在中昱维信账号中获得的实际值   payload := strings.NewReader("appId=YOUR_APP_ID&appKey=YOUR_APP_KEY&templateId=YOUR_TEMPLATE_ID&phone=YOUR_PHONE&variables=YOUR_CODE")   client := &http.Client {   }   req, err := http.NewRequest(method, url, payload)   if err != nil {     fmt.Println(err)     return   }   req.Header.Add("Content-Type", "application/x-www-form-urlencoded")   res, err := client.Do(req)   if err != nil {     fmt.Println(err)     return   }   defer res.Body.Close()   body, err := ioutil.ReadAll(res.Body)   if err != nil {     fmt.Println(err)     return   }   // 解析短信服务的响应response,根据返回结果判断是否发送成功   // 成功 {"returnStatus": "1 ", "message": "成功", "remainPoint": "241", "taskId": "3313746", "successCounts": "1"}   // 失败 {"returnStatus": "0", "message": "参数错误", "remainPoint": null, "taskId": null, "successCounts": null}   // 处理成功或失败的逻辑...   fmt.Println(string(body)) }
  • [技术干货] golang使用sync.singleflight解决热点缓存穿透问题【转】
    在go的sync包中,有一个singleflight包,里面有一个 singleflight.go文件,代码加注释,一共200行出头,通过 singleflight可以很容易实现缓存和去重的效果,避免重复计算,接下来我们就给大家详细介绍一下sync.singleflight如何解决热点缓存穿透问题在 go 的 sync 包中,有一个 singleflight 包,里面有一个 singleflight.go 文件,代码加注释,一共 200 行出头。内容包括以下几块儿:Group 结构体管理一组相关的函数调用工作,它包含一个互斥锁和一个 map,map 的 key 是函数的名称,value 是对应的 call 结构体。call 结构体表示一个 inflight 或已完成的函数调用,包含等待组件 WaitGroup、调用结果 val 和 err、调用次数 dups 和通知通道 chans。Do 方法接收一个 key 和函数 fn,它会先查看 map 中是否已经有这个 key 的调用在 inflight,如果有则等待并返回已有结果,如果没有则新建一个 call 并执行函数调用。DoChan 类似 Do 但返回一个 channel 来接收结果。doCall 方法包含了具体处理调用的逻辑,它会在函数调用前后添加 defer 来 recover panic 和区分正常 return 与 runtime.Goexit。如果发生 panic,会将 panicwraps 成错误返回给等待的 channel,如果是 goexit 会直接退出。正常 return 时会将结果发送到所有通知 channel。Forget 方法可以忘记一个 key 的调用,下次 Do 时会重新执行函数。这个包通过互斥锁和 map 实现了对相同 key 的函数调用去重,可以避免对已有调用的重复计算,同时通过 channel 机制可以通知调用者函数执行结果。在一些需要确保单次执行的场景中,可以使用这个包中的方法。通过 singleflight 可以很容易实现缓存和去重的效果,避免重复计算,接下来,我们来模拟一下并发请求可能导致的缓存穿透场景,以及如何用 singleflight 包来解决这个问题:package main import (    "context"    "fmt"    "golang.org/x/sync/singleflight"    "sync/atomic"    "time"    ) type Result string // 模拟查询数据库 func find(ctx context.Context, query string) (Result, error) {    return Result(fmt.Sprintf("result for %q", query)), nil } func main() {    var g singleflight.Group    const n = 200    waited := int32(n)    done := make(chan struct{})    key := "this is key"    for i := 0; i < n; i++ {       go func(j int) {          v, _, shared := g.Do(key, func() (interface{}, error) {             ret, err := find(context.Background(), key)             return ret, err          })          if atomic.AddInt32(&waited, -1) == 0 {             close(done)          }          fmt.Printf("index: %d, val: %v, shared: %v\n", j, v, shared)       }(i)    }    select {    case <-done:    case <-time.After(time.Second):       fmt.Println("Do hangs")    }    time.Sleep(time.Second * 4) }在这段程序中,如果重复使用查询结果,shared 会返回 true,穿透查询会返回 false上面的设计中还有一个问题,就是在 Do 阻塞时,所有请求都会阻塞,内存可能会出现大的问题。此时,Do 可以更换为DoChan,两者实现上完全一样,不同的是,DoChan() 通过 channel 返回结果。因此可以使用 select 语句实现超时控制ch := g.DoChan(key, func() (interface{}, error) {    ret, err := find(context.Background(), key)    return ret, err }) // Create our timeout timeout := time.After(500 * time.Millisecond) var ret singleflight.Result select { case <-timeout: // Timeout elapsed    fmt.Println("Timeout")    return case ret = <-ch: // Received result from channel    fmt.Printf("index: %d, val: %v, shared: %v\n", j, ret.Val, ret.Shared) }在超时时主动返回,不阻塞。此时又引入了另一个问题,这样的每一次的请求,并不是高可用的,成功率是无法保证的。这时候可以增加一定的请求饱和度来保证业务的最终成功率,此时一次请求还是多次请求,对于下游服务而言并没有太大区别,此时使用  singleflight  只是为了降低请求的数量级,那么可以使用 Forget() 来提高下游请求的并发。ch := g.DoChan(key, func() (interface{}, error) {    go func() {       time.Sleep(10 * time.Millisecond)       fmt.Printf("Deleting key: %v\n", key)       g.Forget(key)    }()    ret, err := find(context.Background(), key)    return ret, err })当然,这种做法依然无法保证100%的成功,如果单次的失败无法容忍,在高并发的场景下需要使用更好的处理方案,比如牺牲一部分实时性、完全使用缓存查询 + 异步更新等。
  • [问题求助] glang 如何import公开只读库
    golang 项目 引用 托管的代码库,导入代码:提示错误信息:codehub.devcloud.huaweicloud.com/zyfbpt/cas-server/spec: cannot find module providing package codehub.devcloud.huaweicloud.com/zyfbpt/cas-server/spec: unrecognized import path "codehub.devcloud.huaweicloud.com/zyfbpt/cas-server/spec": reading https://codehub.devcloud.huaweicloud.com/zyfbpt/cas-server/spec?go-get=1: 404 Not Found
  • [公告] 服务介绍
    华为云开发者插件英文名是 Huawei Cloud Toolkit,作为华为云围绕其产品能力向开发者桌面上的延伸,帮助开发者快速在本地连接华为云,打通华为云到开发者的最后一公里;支持VS Code、IntelliJ IDEA等主流IDE平台、以及华为云自研 CodeArts IDE ,帮助开发者更高效、便捷的搭建应用。致力于为开发者提供更稳定、快速、安全的编程体验。产品页地址:https://developer.huaweicloud.com/develop/toolkit.html产品页二维码:
  • [技术干货] Go语言五大主流web框架-转载
     以下 star数截止2023年6月份 1.Gin(69.1K) 项目简介:Gin 是一个用 Go (Golang) 编写的 HTTP Web 框架。 它具有类似 Martini 的 API,但性能比 Martini 快 40 倍。  仓库地址: https://github.com/gin-gonic/gin https://github.com/gin-gonic/gin  官方文档地址: 文档 | Gin Web Framework Gin 是什么?Gin 是一个用 Go (Golang) 编写的 HTTP Web 框架。 它具有类似 Martini 的 API,但性能比 Martini 快 40 倍。如果你需要极好的性能,使用 Gin 吧。如何使用 Gin?我们提供了一些 API … https://gin-gonic.com/zh-cn/docs/  2.Beego(29.8K) 项目简介:Beego用于在Go中快速开发企业应用程序,包括RESTful API、web应用程序和后端服务。它的灵感来源于Tornado, Sinatra and Flask。beego有一些特定于Go的特性,如接口和结构嵌入。  仓库地址: GitHub - beego/beego: beego is an open-source, high-performance web framework for the Go programming language. beego is an open-source, high-performance web framework for the Go programming language. - GitHub - beego/beego: beego is an open-source, high-performance web framework for the Go programming language. https://github.com/beego/beego  官方文档地址: Welcome to Beego | Beego The most easy use framework https://beego.gocn.vip/beego/zh/developing/  3.Fiber(26.5K)  项目简介:Fiber是一个Go web框架,构建在Go最快的HTTP引擎Fasthttp之上。它的设计目的是为了在零内存分配和性能的情况下简化快速开发。  仓库地址: https://github.com/gofiber/fiber https://github.com/gofiber/fiber  官方文档地址: Welcome - Fiber https://docs.gofiber.io/  4.Echo(25.8K)  项目简介:高性能、极简Go web框架  仓库地址: GitHub - labstack/echo: High performance, minimalist Go web framework High performance, minimalist Go web framework. Contribute to labstack/echo development by creating an account on GitHub. https://github.com/labstack/echo 官方文档地址: Echo - High performance, minimalist Go web framework Echo is a high performance, extensible, minimalist web framework for Go (Golang). https://echo.labstack.com/   5.Iris(24K)  项目简介:Iris是一个高效且设计良好的跨平台web框架,具有强大的功能集。构建具有无限潜力和可移植性的高性能web应用程序和API。  仓库地址: GitHub - kataras/iris: The fastest HTTP/2 Go Web Framework. New, modern, easy to learn. Fast development with Code you control. Unbeatable cost-performance ratio | 谢谢 | #golang The fastest HTTP/2 Go Web Framework. New, modern, easy to learn. Fast development with Code you control. Unbeatable cost-performance ratio :leaves: :rocket: | 谢谢 | #golang - GitHub - kataras/iris: The fastest HTTP/2 Go Web Framework. New, modern, easy to learn. Fast development with Code you control. Unbeatable cost-performance ratio | 谢谢 | #golang https://github.com/kataras/iris  官方文档地址: Iris Docs The fastest HTTP/2 Go Web Framework. Iris provides a beautifully expressive and easy to use foundation for your next website, API, or distributed app. https://www.iris-go.com/docs/#/ ———————————————— 版权声明:本文为CSDN博主「深漂小码哥」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/qq2942713658/article/details/127720799 
  • [区域初赛赛题问题] 支持Go语言吗
    如题,运行环境支持Golang 吗?
  • strcpy的基本用法详解以及模拟实现strcpy-转载
     1. strcpy的基本用法详解 1.1 问题的提出 例如 我们要把字符串"hello"复制到数组arr[20]中去时,你会怎么操作;  首先   arr = "hello";//是错误的  arr数组名是首元素的地址,是个地址常量,是个编号;难道把hello放到这个编号上?  答案应该是放到编号所指向的空间中去;   其中 destination是目标空间的地址,source是源空间的地址  1.2 strcpy的基本原理: 把源指针指向的空间的数据拷贝到目的地指针指向的空间中去;  char* p = "hello";//把首字符的地址放到p中,p就指向了这个字符串;  strcpy(arr,"hello");  "hello"传参的时候传过去的是首字符'h'的地址,传给了source;其中destination指向了arr[20]整个数组,source指向了hello中'h'的地址;然后把source指向的hello拷贝放到destination指向的arr[20]中去;  1.3使用 strcpy的注意事项: 1.源字符串必须以 '\0' 结束 当拷贝"hello"时字符串的结束标志'\0'也会被拷贝过去,'\0'也是strcpy终止拷贝的一个条件; 2.会将源字符串中的 '\0' 拷贝到目标空间 3.目标空间必须足够大,以确保能存放源字符串 例如 arr[5]=0; strcpy(arr,"hello world");这是错误的 4.目标空间必须可变 例如 char* str = "123456789000"; char* p = "hello world"; strcpy(str,p);//这也是错误的 因为该目标空间是常量字符串,不可修改; 2. 模拟实现strcpy char *my_strcpy(char *destination, const char*source) {   char *ret = destination;  assert(destination != NULL);  assert(source != NULL);  while((*destination++ = *source++))  {           ;  }  return ret;  } ———————————————— 版权声明:本文为CSDN博主「记忆&amp;碎片」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/YLG_lin/article/details/126690885 
  • [技术干货] Go高性能之方法接收器 - 指针vs值-转载
    如果您是 Go 新手,那么您一定遇到过方法和函数的概念。让我们找出两者之间的区别-通过指定参数的类型、返回值和函数体来声明函数。type Person struct {     Name string     Age int }func NewPerson(name string, age int) *Person {   return &Person{      Name: name,      Age: age,   } }方法只是一个带有接收器参数的函数。它使用相同的语法声明,并添加了接收者。func (p *Person) isAdult bool {   return p.Age > 18 }在上面的方法声明中,我们在类型上声明了isAdult方法。*Person现在我们将看到值接收器和指针接收器之间的区别。值接收者复制类型并将其传递给函数。函数堆栈现在拥有一个相等的对象,但在内存上的不同位置。这意味着对传递的对象所做的任何更改都将保留在该方法的本地。原始对象将保持不变。指针接收器将类型的地址传递给函数。函数堆栈具有对原始对象的引用。因此对传递对象的任何修改都会修改原始对象。让我们通过示例来理解这一点-package mainimport (  "fmt")type Person struct {    Name string    Age  int}func ValueReceiver(p Person) {    p.Name = "John"    fmt.Println("Inside ValueReceiver : ", p.Name)}func PointerReceiver(p *Person) {    p.Age = 24    fmt.Println("Inside PointerReceiver model: ", p.Age)}func main() {    p := Person{"Tom", 28}    p1:= &Person{"Patric", 68}    ValueReceiver(p)fmt.Println("Inside Main after value receiver : ", p.Name)    PointerReceiver(p1)fmt.Println("Inside Main after value receiver : ", p1.Age)} ------------Inside ValueReceiver :  JohnInside Main after value receiver :  TomInside PointerReceiver :  24Inside Main after pointer receiver :  24这表明具有值接收者的方法修改了对象的副本,而原始对象保持不变。Like- 通过 ValueReceiver 方法将一个人的姓名从 Tom 更改为 John,但这种更改并未反映在 main 方法中。另一方面,带有指针接收器的方法会修改实际对象。Like- 通过 PointerReceiver 方法将人的年龄从 68 岁更改为 24 岁,同样的变化反映在 main 方法中。您可以通过在指针或值接收器操作之前和之后打印出对象的地址来检查事实。那么如何在 Pointer 和 Value 接收器之间进行选择呢?如果要更改方法中接收器的状态,操作它的值,请使用指针接收器。使用按值复制的值接收器是不可能的。对值接收器的任何修改对于该副本都是本地的。如果您不需要操作接收器值,请使用值接收器。指针接收器避免在每个方法调用上复制值。如果接收器是一个大型结构,这可能会更有效,值接收器是并发安全的,而指针接收器不是并发安全的。因此,程序员需要照顾它。汇总:如果接收者是 map、func 或 chan,不要使用指向它的指针。尽量对所有方法使用相同的接收器类型。如果接收者是一个切片并且该方法没有重新切片或重新分配切片,则不要使用指向它的指针。如果方法需要改变接收者,接收者必须是一个指针。如果接收者是包含sync.Mutex或类似同步字段的结构,则接收者必须是指针以避免复制。如果接收器是大型结构或数组,则指针接收器效率更高。大有多大?假设它相当于将其所有元素作为参数传递给方法。如果感觉太大,那么对于接收器来说也太大了。函数或方法是否可以同时或在从此方法调用时改变接收者?调用方法时,值类型会创建接收器的副本,因此外部更新不会应用于此接收器。如果更改必须在原始接收器中可见,则接收器必须是指针。如果接收器是结构体、数组或切片,并且它的任何元素都是指向可能发生变化的东西的指针,则更喜欢指针接收器,因为它会使读者更清楚意图。如果接收者是一个小数组或结构,它自然是一个值类型(例如,类似time.Time类型),没有可变字段和指针,或者只是一个简单的基本类型,如 int 或 string,则值接收器更好。值接收器可以减少可以生成的垃圾量;如果将值传递给值方法,则可以使用堆栈上的副本而不是在堆上分配。(编译器试图巧妙地避免这种分配,但它并不总是成功。)不要在没有首先进行分析的情况下选择值接收器类型。最后,当有疑问时,使用指针接收器。————————————————版权声明:本文为CSDN博主「hebiwen95」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。原文链接:https://blog.csdn.net/hebiwen95/article/details/126124140
  • [技术干货] Minio RELEASE.2021-10-06T233631Z安装指导书
    1 软件介绍 minio为开源的高可用分部署对象存储服务组件,已经提供了分布式部署的解决方案,实现高可靠、高可用的资源存储,MinIO以极简主义为指导进行设计,追求极致的维护精简性和卓越的读写性能表现2 环境配置2.1 硬件平台 服务器TaiShan 200 2280处理器2*KunPeng 920内存NA硬盘NA网络NA 2.2 软件平台 软件名称版本号麒麟V10SP1MinioRELEASE.2022.03    3 系统配置3.1 关闭防火墙步骤 1 停止防火墙。# systemctl stop firewalld.service步骤 2 关闭防火墙。# systemctl disable firewalld.service----结束3.2 安装依赖若环境无外网条件,请配置本地源。步骤 1 安装依赖# yum -y install git----结束 4 软件编译4.1 Golang安装步骤 1 下载编译包。# wget https://golang.google.cn/dl/go1.17.11.linux-arm64.tar.gz --no-check-certificate步骤 2 解压到/usr/local下。# tar -zxvf go1.17.11.linux-arm64.tar.gz –C /usr/local步骤 3 配置go环境变量。# echo "export PATH=/usr/local/go/bin/:\$PATH" >> /etc/profile# sourced /etc/profile步骤 4 验证版本。# go version----结束4.2 MiniIO Server安装步骤 1 配置go代理。# export GOPROXY=https://goproxy.cn步骤 2 下载源码包。# git clone https://github.com/minio/minio.git步骤 3 切换版本# cd minio;git checkout RELEASE.2021-10-06T23-36-31Z步骤 4 编译# make步骤 5 验证版本号# ./minio --version----结束 4.3 MiniIO Client安装步骤 1 配置go代理。# export GOPROXY=https://goproxy.cn步骤 2 下载源码包。# git clone https://github.com/minio/mc.git步骤 3 切换版本# cd mc;git checkout RELEASE.2021-10-07T04-19-58Z步骤 4 编译# make步骤 5 查看版本时,发现版本并非2021-10-07T04-19-58Z,而为编译当天的日期。此问题非编译问题,而是源码本身的逻辑就是这样,详见FAQ 6.1 ----结束 5 运行验证5.1 运行MinIO服务端步骤 1 设置登录账号和密码# export MINIO_ROOT_USER=minio_test# export MINIO_ROOT_PASSWORD=minio_test步骤 2 初始化# mkdir -p /opt/data# ./minio server /opt/data步骤 3 打开网页,默认使用端口为9000,在浏览器输入地址 ip:9000输入刚才设置的账户密码 ----结束5.2 运行MinIO客户端 步骤 1 查看已添加的主机#./mc config host list  步骤 2 添加服务端./mc alias set myminio http://ip:9000 minioadmin minioadmin步骤 3 查看添加的服务端主机#./mc config host list6 FAQ6.1 mimio client查看版本号不对编译的版本是RELEASE.2021-10-07T04-19-58Z,但使用命令./mc -version看到的是当天日期版本查看Makefile,日期定义在变量BUILD_LDFLAGS---即变量LDFLAGS中LDFLAGS变量由buildscripts/gen-ldflags.go生成,查看buildscripts/gen-ldflags.go文件的源码,可以看到,版本号由当天时间定义,因此编译出来的版本显示为当天时间7 附录参考文献
  • [知识分享] 解读Go分布式链路追踪实现原理
    【摘要】 在分布式、微服务架构下,应用一个请求往往贯穿多个分布式服务,这给应用的故障排查、性能优化带来新的挑战。分布式链路追踪作为解决分布式应用可观测问题的重要技术,愈发成为分布式应用不可缺少的基础设施。本文将详细介绍分布式链路的核心概念、架构原理和相关开源标准协议,并分享我们在实现无侵入 Go 采集 Sdk 方面的一些实践。 为什么需要分布式链路追踪系统 微服务架构给运维、排障带来新挑战在分布式架构...本文分享自华为云社区《一文详解|Go 分布式链路追踪实现原理》,作者:开源小E。在分布式、微服务架构下,应用一个请求往往贯穿多个分布式服务,这给应用的故障排查、性能优化带来新的挑战。分布式链路追踪作为解决分布式应用可观测问题的重要技术,愈发成为分布式应用不可缺少的基础设施。本文将详细介绍分布式链路的核心概念、架构原理和相关开源标准协议,并分享我们在实现无侵入 Go 采集 Sdk 方面的一些实践。为什么需要分布式链路追踪系统微服务架构给运维、排障带来新挑战在分布式架构下,当用户从浏览器客户端发起一个请求时,后端处理逻辑往往贯穿多个分布式服务,这时会浮现很多问题,比如:请求整体耗时较长,具体慢在哪个服务?请求过程中出错了,具体是哪个服务报错?某个服务的请求量如何,接口成功率如何?回答这些问题变得不是那么简单,我们不仅仅需要知道某一个服务的接口处理统计数据,还需要了解两个服务之间的接口调用依赖关系,只有建立起整个请求在多个服务间的时空顺序,才能更好的帮助我们理解和定位问题,而这,正是分布式链路追踪系统可以解决的。分布式链路追踪系统如何帮助我们分布式链路追踪技术的核心思想:在用户一次分布式请求服务的调⽤过程中,将请求在所有子系统间的调用过程和时空关系追踪记录下来,还原成调用链路集中展示,信息包括各个服务节点上的耗时、请求具体到达哪台机器上、每个服务节点的请求状态等等。如上图所示,通过分布式链路追踪构建出完整的请求链路后,可以很直观地看到请求耗时主要耗费在哪个服务环节,帮助我们更快速聚焦问题。同时,还可以对采集的链路数据做进一步的分析,从而可以建立整个系统各服务间的依赖关系、以及流量情况,帮助我们更好地排查系统的循环依赖、热点服务等问题。分布式链路追踪系统架构概览核心概念在分布式链路追踪系统中,最核心的概念,便是链路追踪的数据模型定义,主要包括 Trace 和 Span。其中,Trace 是一个逻辑概念,表示一次(分布式)请求经过的所有局部操作(Span)构成的一条完整的有向无环图,其中所有的 Span 的 TraceId 相同。Span 则是真实的数据实体模型,表示一次(分布式)请求过程的一个步骤或操作,代表系统中一个逻辑运行单元,Span 之间通过嵌套或者顺序排列建立因果关系。Span 数据在采集端生成,之后上报到服务端,做进一步的处理。其包含如下关键属性:Name:操作名称,如一个 RPC 方法的名称,一个函数名StartTime/EndTime:起始时间和结束时间,操作的生命周期ParentSpanId:父级 Span 的 IDAttributes:属性,一组 <K,V> 键值对构成的集合Event:操作期间发生的事件SpanContext:Span 上下文内容,通常用于在 Span 间传播,其核心字段包括 TraceId、SpanId一般架构分布式链路追踪系统的核心任务是:围绕 Span 的生成、传播、采集、处理、存储、可视化、分析,构建分布式链路追踪系统。其一般的架构如下如所示:我们看到,在应用端需要通过侵入或者非侵入的方式,注入 Tracing Sdk,以跟踪、生成、传播和上报请求调用链路数据;Collect agent 一般是在靠近应用侧的一个边缘计算层,主要用于提高 Tracing Sdk 的写性能,和减少 back-end 的计算压力;采集的链路跟踪数据上报到后端时,首先经过 Gateway 做一个鉴权,之后进入 kafka 这样的 MQ 进行消息的缓冲存储;在数据写入存储层之前,我们可能需要对消息队列中的数据做一些清洗和分析的操作,清洗是为了规范和适配不同的数据源上报的数据,分析通常是为了支持更高级的业务功能,比如流量统计、错误分析等,这部分通常采用flink这类的流处理框架来完成;存储层会是服务端设计选型的一个重点,要考虑数据量级和查询场景的特点来设计选型,通常的选择包括使用 Elasticsearch、Cassandra、或 Clickhouse 这类开源产品;流处理分析后的结果,一方面作为存储持久化下来,另一方面也会进入告警系统,以主动发现问题来通知用户,如错误率超过指定阈值发出告警通知这样的需求等。刚才讲的,是一个通用的架构,我们并没有涉及每个模块的细节,尤其是服务端,每个模块细讲起来都要很花些功夫,受篇幅所限,我们把注意力集中到靠近应用侧的 Tracing Sdk,重点看看在应用侧具体是如何实现链路数据的跟踪和采集的。协议标准和开源实现刚才我们提到 Tracing Sdk,其实这只是一个概念,具体到实现,选择可能会非常多,这其中的原因,主要是因为:不同的编程语言的应用,可能采用不同技术原理来实现对调用链的跟踪不同的链路追踪后端,可能采用不同的数据传输协议当前,流行的链路追踪后端,比如 Zipin、Jaeger、PinPoint、Skywalking、Erda,都有供应用集成的 sdk,导致我们在切换后端时应用侧可能也需要做较大的调整。社区也出现过不同的协议,试图解决采集侧的这种乱象,比如 OpenTracing、OpenCensus 协议,这两个协议也分别有一些大厂跟进支持,但最近几年,这两者已经走向了融合统一,产生了一个新的标准 OpenTelemetry,这两年发展迅猛,已经逐渐成为行业标准。OpenTelemetry 定义了数据采集的标准 api,并提供了一组针对多语言的开箱即用的 sdk 实现工具,这样,应用只需要与 OpenTelemetry 核心 api 包强耦合,不需要与特定的实现强耦合。应用侧调用链跟踪实现方案概览应用侧核心任务应用侧围绕 Span,有三个核心任务要完成:生成 Span:操作开始构建 Span 并填充 StartTime,操作完成时填充 EndTime 信息,期间可追加 Attributes、Event 等传播 Span:进程内通过 context.Context、进程间通过请求的 header 作为 SpanContext 的载体,传播的核心信息是 TraceId 和 ParentSpanId上报 Span:生成的 Span 通过 tracing exporter 发送给 collect agent / back-end server要实现 Span 的生成和传播,要求我们能够拦截应用的关键操作(函数)过程,并添加 Span 相关的逻辑。实现这个目的会有很多方法,不过,在罗列这些方法之前,我们先看看在 OpenTelemetry 提供的 go sdk 中是如何做的。基于 OTEL 库实现调用拦截OpenTelemetry 的 go sdk 实现调用链拦截的基本思路是:基于 AOP 的思想,采用装饰器模式,通过包装替换目标包(如 net/http)的核心接口或组件,实现在核心调用过程前后添加 Span 相关逻辑。当然,这样的做法是有一定的侵入性的,需要手动替换使用原接口实现的代码调用改为包装接口实现。我们以一个 http server 的例子来说明,在 go 语言中,具体是如何做的:假设有两个服务 serverA 和 serverB,其中 serverA 的接口收到请求后,内部会通过 httpclient 进一步发起到 serverB 的请求,那么 serverA 的核心代码可能如下图所示:以 serverA 节点为例,在 serverA 节点应该产生至少两个 Span:Span1,记录 httpServer 收到一个请求后内部整体处理过程的一个耗时情况Span2,记录 httpServer 处理请求过程中,发起的另一个到 serverB 的 http 请求的耗时情况并且 Span1 应该是 Span2 的 ParentSpan我们可以借助 OpenTelemetry 提供的 sdk 来实现 Span 的生成、传播和上报,上报的逻辑受篇幅所限我们不再详述,重点来看看如何生成这两个 Span,并使这两个 Span 之间建立关联,即 Span 的生成和传播 。HttpServer Handler 生成 Span 过程对于 httpserver 来讲,我们知道其核心就是 http.Handler 这个接口。因此,可以通过实现一个针对 http.Handler 接口的拦截器,来负责 Span 的生成和传播。package http type Handler interface { ServeHTTP(ResponseWriter, *Request) } http.ListenAndServe(":8090", http.DefaultServeMux)要使用 OpenTelemetry Sdk 提供的 http.Handler 装饰器,需要如下调整 http.ListenAndServe 方法:import ( "net/http" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/sdk/trace" "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" ) wrappedHttpHandler := otelhttp.NewHandler(http.DefaultServeMux, ...) http.ListenAndServe(":8090", wrappedHttpHandler)如图所示,wrppedHttpHandler 中将主要实现如下逻辑(精简考虑,此处部分为伪代码):① ctx := tracer.Extract(r.ctx, r.Header):从请求的 header 中提取 traceparent header 并解析,提取 TraceId和 SpanId,进而构建 SpanContext 对象,并最终存储在 ctx 中;② ctx, span := tracer.Start(ctx, genOperation(r)):生成跟踪当前请求处理过程的 Span(即前文所述的Span1),并记录开始时间,这时会从 ctx 中读取 SpanContext,将 SpanContext.TraceId 作为当前 Span 的TraceId,将 SpanContext.SpanId 作为当前 Span的ParentSpanId,然后将自己作为新的 SpanContext 写入返回的 ctx 中;③ r.WithContext(ctx):将新生成的 SpanContext 添加到请求 r 的 context 中,以便被拦截的 handler 内部在处理过程中,可以从 r.ctx 中拿到 Span1 的 SpanId 作为其 ParentSpanId 属性,从而建立 Span 之间的父子关系;④ span.End():当 innerHttpHandler.ServeHTTP(w,r) 执行完成后,就需要对 Span1 记录一下处理完成的时间,然后将它发送给 exporter 上报到服务端。HttpClient 请求生成 Span 过程我们再接着看 serverA 内部去请求 serverB 时的 httpclient 请求是如何生成 Span 的(即前文说的 Span2)。我们知道,httpclient 发送请求的关键操作是 http.RoundTriper 接口:package http type RoundTripper interface { RoundTrip(*Request) (*Response, error) }OpenTelemetry 提供了基于这个接口的一个拦截器实现,我们需要使用这个实现包装一下 httpclient 原来使用的 RoundTripper 实现,代码调整如下:import ( "net/http" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/sdk/trace" "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" ) wrappedTransport := otelhttp.NewTransport(http.DefaultTransport) client := http.Client{Transport: wrappedTransport}如图所示,wrappedTransport 将主要完成以下任务(精简考虑,此处部分为伪代码):① req, _ := http.NewRequestWithContext(r.ctx, “GET”,url, nil) :这里我们将上一步 http.Handler 的请求的 ctx,传递到 httpclient 要发出的 request 中,这样在之后我们就可以从 request.Context() 中提取出 Span1 的信息,来建立 Span 之间的关联;② ctx, span := tracer.Start(r.Context(), url):执行 client.Do() 之后,将首先进入 WrappedTransport.RoundTrip() 方法,这里生成新的 Span(Span2),开始记录 httpclient 请求的耗时情况,与前文一样,Start 方法内部会从 r.Context() 中提取出 Span1 的 SpanContext,并将其 SpanId 作为当前 Span(Span2)的 ParentSpanId,从而建立了 Span 之间的嵌套关系,同时返回的 ctx 中保存的 SpanContext 将是新生成的 Span(Span2)的信息;③ tracer.Inject(ctx, r.Header):这一步的目的是将当前 SpanContext 中的 TraceId 和 SpanId 等信息写入到 r.Header 中,以便能够随着 http 请求发送到 serverB,之后在 serverB 中与当前 Span 建立关联;④ span.End():等待 httpclient 请求发送到 serverB 并收到响应以后,标记当前 Span 跟踪结束,设置 EndTime 并提交给 exporter 以上报到服务端。基于 OTEL 库实现调用链跟踪总结我们比较详细的介绍了使用 OpenTelemetry 库,是如何实现链路的关键信息(TraceId、SpanId)是如何在进程间和进程内传播的,我们对这种跟踪实现方式做个小的总结:如上分析所展示的,使用这种方式的话,对代码还是有一定的侵入性,并且对代码有另一个要求,就是保持 context.Context 对象在各操作间的传递,比如,刚才我们在 serverA 中创建 httpclient 请求时,使用的是http.NewRequestWithContext(r.ctx, ...) 而非http.NewRequest(...)方法,另外开启 goroutine 的异步场景也需要注意 ctx 的传递。非侵入调用链跟踪实现思路我们刚才详细展示了基于常规的一种具有一定侵入性的实现,其侵入性主要表现在:我们需要显式的手动添加代码使用具有跟踪功能的组件包装原代码,这进一步会导致应用代码需要显式的引用具体版本的 OpenTelemetry instrumentation 包,这不利于可观测代码的独立维护和升级。那我们有没有可以实现非侵入跟踪调用链的方案可选?所谓无侵入,其实也只是集成的方式不同,集成的目标其实是差不多的,最终都是要通过某种方式,实现对关键调用函数的拦截,并加入特殊逻辑,无侵入重点在于代码无需修改或极少修改。上图列出了现在可能的一些无侵入集成的实现思路,与 .net、java 这类有 IL 语言的编程语言不同,go 直接编译为机器码,导致无侵入的方案实现起来相对比较麻烦,具体有如下几种思路:编译阶段注入:可以扩展编译器,修改编译过程中的ast,插入跟踪代码,需要适配不同编译器版本。启动阶段注入:修改编译后的机器码,插入跟踪代码,需要适配不同 CPU 架构。如 monkey, gohook。运行阶段注入:通过内核提供的 eBPF 能力,监听程序关键函数执行,插入跟踪代码,前景光明!如,tcpdump,bpftrace。Go 非侵入链路追踪实现原理Erda 项目的核心代码主要是基于 golang 编写的,我们基于前文所述的 OpenTelemetry sdk,采用基于修改机器码的的方式,实现了一种无侵入的链路追踪方式。前文提到,使用 OpenTelemetry sdk 需要代码做一些调整,我们看看这些调整如何以非侵入的方式自动的完成:我们以 httpclient 为例,做简要的解释。gohook 框架提供的 hook 接口的签名如下:// target 要hook的目标函数 // replacement 要替换为的函数 // trampoline 将源函数入口拷贝到的位置,可用于从replcement跳转回原target func Hook(target, replacement, trampoline interface{}) error对于 http.Client,我们可以选择 hook DefaultTransport.RoundTrip() 方法,当该方法执行时,我们通过 otelhttp.NewTransport() 包装起原 DefaultTransport 对象,但需要注意的是,我们不能将 DefaultTransport 直接作为 otelhttp.NewTransport() 的参数,因为其 RoundTrip() 方法已经被我们替换了,而其原来真正的方法被写到了 trampoline 中,所以这里我们需要一个中间层,来连接 DefaultTransport 与其原来的 RoundTrip 方法。具体代码如下://go:linkname RoundTrip net/http.(*Transport).RoundTrip //go:noinline // RoundTrip . func RoundTrip(t *http.Transport, req *http.Request) (*http.Response, error) //go:noinline func originalRoundTrip(t *http.Transport, req *http.Request) (*http.Response, error) { return RoundTrip(t, req) } type wrappedTransport struct { t *http.Transport } //go:noinline func (t *wrappedTransport) RoundTrip(req *http.Request) (*http.Response, error) { return originalRoundTrip(t.t, req) } //go:noinline func tracedRoundTrip(t *http.Transport, req *http.Request) (*http.Response, error) { req = contextWithSpan(req) return otelhttp.NewTransport(&wrappedTransport{t: t}).RoundTrip(req) } //go:noinline func contextWithSpan(req *http.Request) *http.Request { ctx := req.Context() if span := trace.SpanFromContext(ctx); !span.SpanContext().IsValid() { pctx := injectcontext.GetContext() if pctx != nil { if span := trace.SpanFromContext(pctx); span.SpanContext().IsValid() { ctx = trace.ContextWithSpan(ctx, span) req = req.WithContext(ctx) } } } return req } func init() { gohook.Hook(RoundTrip, tracedRoundTrip, originalRoundTrip) }我们使用 init() 函数实现了自动添加 hook,因此用户程序里只需要在 main 文件中 import 该包,即可实现无侵入的集成。值得一提的是 req = contextWithSpan(req) 函数,内部会依次尝试从 req.Context() 和 我们保存的 goroutineContext map 中检查是否包含 SpanContext,并将其赋值给 req,这样便可以解除了必须使用 http.NewRequestWithContext(...) 写法的要求。详细的代码可以查看 Erda 仓库:https://github.com/erda-project/erda-infra/tree/master/pkg/trace参考链接https://opentelemetry.io/registry/https://opentelemetry.io/docs/instrumentation/go/getting-started/https://www.ipeapea.cn/post/go-asm/https://github.com/brahma-adshonor/gohookhttps://www.jianshu.com/p/7b3638b47845https://paper.seebug.org/1749/
总条数:166 到第
上滑加载中