Go 基础之http/net、context上下文管理、单元测试、性能基准测试、fmt包、flag包、pprof调试工具
Go 10 http/net、context上下文管理、单元测试、性能基准测试、fmt包、flag包、pprof调试工具
互斥锁
sync.Mutex
是一个结构体,是值类型。给函数传参数的时候要传指针。
两个方法
var lock sync.Mutex
lock.Lock() // 加锁
lock.Unlock() // 解锁
为什么要用锁
防止同一时刻多个goroutine操作同一个资源。
读写互斥锁
应用场景
适用于读多写少的场景下,能够提高程序的执行效率。
特点
- 读的goroutine来了获取的是读锁,后续的goroutine能读不能写。
- 写的goroutine来了获取的是写锁,后续的goroutine不管是读还是写都要等待获取锁。
使用
var rwlock sync.RWMutex
rwlock.RLock() // 获取读锁
rwlock.RUnlock() // 释放读锁
rwlock.Lock() // 获取写锁
rwlock.Unlock() // 获取写锁
等待组
sync.WaitGroup
用来等goroutine执行完再继续,是一个结构体,值类型。
使用
var wg sync.WaitGroup
wg.Add(1) // 起几个goroutine就加几
wg.Done() // 在goroutine对应的函数中,函数要结束的时候表示goroutine完成,计数器-1
wg.Wait() // 等待所有的goroutine都结束也就是计数器变为0再执行
sync.Once
使用场景
某些函数只需要还行一次的时候,就可以使用sync.Once
var once sync.Once
once.Do() // 接受的是一个没有参数没有返回值的函数,有时需要闭包的思想
var icons map[string]image.Image
var loadIconsOnce sync.Once
func loadIcons() {
icons = map[string]image.Image{
"left": loadIcon("left.png"),
"up": loadIcon("up.png"),
"right": loadIcon("right.png"),
"down": loadIcon("down.png"),
}
}
// Icon 是并发安全的
func Icon(name string) image.Image {
loadIconsOnce.Do(loadIcons)
return icons[name]
}
sync.Map
使用场景
并发操作一个map的时候,内置的map不是并发安全的。
使用
是一个开箱即用(不需要make初始化)的并发安全的map
使用方法
var syncmap sync.Map
// map[key]=value // 原生map
syncmap.Store(key,value)
syncmap.Load(key)
syncmap.LoadOrStore()
syncmap.Delete()
syncmap.Range()
原子操作
Go语言内置了一些针对内置的基本数据类型的一些并发安全的操作
var
atomic.AddInt64(&i,1)
网络编程
互联网协议
OSI七层模型
http/net
http client端
Get、Head、Post和PostForm函数发出HTTP/HTTPS请求。
resp, err := http.Get("http://example.com/")
...
resp, err := http.Post("http://example.com/upload", "image/jpeg", &buf)
...
resp, err := http.PostForm("http://example.com/form",
url.Values{"key": {"Value"}, "id": {"123"}})
带参数的GET请求示例
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.Printf("post failed, err:%v\n", err)
return
}
defer resp.Body.Close()
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Printf("get resp failed, err:%v\n", err)
return
}
fmt.Println(string(b))
}
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.Printf("post failed, err:%v\n", err)
return
}
defer resp.Body.Close()
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Printf("get resp failed, err:%v\n", err)
return
}
fmt.Println(string(b))
}
e.g.
package main
import (
"fmt"
"io/ioutil"
"net/http"
"net/url"
)
// net/http client
// 共用一个client适用于 请求比较频繁
// var (
// client = http.Client{
// Transport: &http.Transport{
// DisableKeepAlives: false,
// },
// }
// )
func main() {
// 爬虫
// res, err := http.Get("http://www.baidu.com")
// if err != nil {
// fmt.Printf("failed, err:%v\n", err)
// return
// }
// 构建一个请求对象
data := url.Values{} // url values
urlobj, _ := url.Parse("http://www.baidu.com")
data.Set("name", "hina")
data.Set("age", "18")
querystr := data.Encode() // url encode之后的URL
fmt.Println(querystr)
urlobj.RawQuery = querystr
req, _ := http.NewRequest("GET", urlobj.String(), nil)
// 发请求
res, err := http.DefaultClient.Do(req)
if err != nil {
fmt.Printf("failed, err:%v\n", err)
return
}
// 禁用KeepAlive长链接的client 请求不是特别频繁,用完就关闭该链接但需要每天连续的话,若是频繁的话需要在全局定义见上
tr := &http.Transport{
DisableKeepAlives: true,
}
client := http.Client{
Transport: tr,
}
client.Do(req)
defer res.Body.Close() // 一定要记得关闭res.Body
// 从res中把服务端返回的数据读出来
b, err := ioutil.ReadAll(res.Body)
if err != nil {
fmt.Printf("failed, err:%v\n", err)
return
}
fmt.Println(string(b))
}
http server端
package main
import (
"fmt"
"io/ioutil"
"net/http"
)
// net/http server
func f1(w http.ResponseWriter, r *http.Request) {
fmt.Println(r)
fmt.Println(r.URL)
fmt.Println(r.Method)
fmt.Println(r.MultipartForm)
fmt.Println(ioutil.ReadAll(r.Body))
b, err := ioutil.ReadFile("./go1.txt")
if err != nil {
w.Write([]byte(fmt.Sprintf("%v", err)))
return
}
w.Write(b)
}
func main() {
http.HandleFunc("/hina/day/13/", f1)
http.ListenAndServe("localhost:9000", nil)
}
处理post请求
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.Printf("read request.Body failed, err:%v\n", err)
return
}
fmt.Println(string(b))
answer := `{"status": "ok"}`
w.Write([]byte(answer))
}
自定义Server
s := &http.Server{
Addr: ":8080",
Handler: myHandler,
ReadTimeout: 10 * time.Second, // 超时时间
WriteTimeout: 10 * time.Second,
MaxHeaderBytes: 1 << 20,
}
log.Fatal(s.ListenAndServe())
context(上下文管理机制)
Context
类型,专门用来简化 对于处理单个请求的多个 goroutine 之间与请求域的数据、取消信号、截止时间等相关操作,这些操作可能涉及多个 API 调用。
对服务器传入的请求应该创建上下文,而对服务器的传出调用应该接受上下文。它们之间的函数调用链必须传递上下文,或者可以使用WithCancel
、WithDeadline
、WithTimeout
或WithValue
创建的派生上下文。当一个上下文被取消时,它派生的所有上下文也被取消。
WithCancel
WithCancel
返回带有新Done通道的父节点的副本。当调用返回的cancel函数或当关闭父上下文的Done通道时,将关闭返回上下文的Done通道,无论先发生什么情况。
取消此上下文将释放与其关联的资源,因此代码应该在此上下文中运行的操作完成后立即调用cancel。
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
}
}
}
上面的示例代码中,gen
函数在单独的goroutine中生成整数并将它们发送到返回的通道。 gen的调用者在使用生成的整数之后需要取消上下文,以免gen
启动的内部goroutine发生泄漏。
WithDeadline
返回父上下文的副本,并将deadline调整为不迟于d。如果父上下文的deadline已经早于d,则WithDeadline(parent, d)在语义上等同于父上下文。当截止日过期时,当调用返回的cancel函数时,或者当父上下文的Done通道关闭时,返回上下文的Done通道将被关闭,以最先发生的情况为准。
取消此上下文将释放与其关联的资源,因此代码应该在此上下文中运行的操作完成后立即调用cancel。
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())
}
}
上面的代码中,定义了一个50毫秒之后过期的deadline,然后我们调用context.WithDeadline(context.Background(), d)
得到一个上下文(ctx)和一个取消函数(cancel),然后使用一个select让主程序陷入等待:等待1秒后打印overslept
退出或者等待ctx过期后退出。 因为ctx50秒后就过期,所以ctx.Done()
会先接收到值,上面的代码会打印ctx.Err()取消原因。
WithTimeout
取消此上下文将释放与其相关的资源,因此代码应该在此上下文中运行的操作完成后立即调用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")
}
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")
}
使用Context的注意事项
- 推荐以参数的方式显示传递Context
- 以Context作为参数的函数方法,应该把Context作为第一个参数。
- 给一个函数方法传递Context的时候,不要传递nil,如果不知道传递什么,就使用context.TODO()
- Context的Value相关方法应该传递请求域的必要数据,不应该用于传递可选参数
- Context是线程安全的,可以放心的在多个goroutine中传递
单元测试
开发自己给自己的代码写测试
测试文件必须以_test.go
结尾
测试函数的名字必须以Test
开头,参数是t *testing.T
在目录下使用go test
或gp test -v
查看测试及结果
package splitstr
import (
"strings"
)
// 切割字符串
// example:
// abc,b => [a c]
func Splitstr(str string, sep string) []string {
// str:="abc" sep:="b"
var ret []string
index := strings.Index(str, sep)
for index >= 0 {
ret = append(ret, str[:index])
str = str[index+1:]
index = strings.Index(str, sep)
}
ret = append(ret, str)
return ret
}
package splitstr
import (
"reflect"
"testing"
)
func TestSplitstr(t *testing.T) {
ret := Splitstr("a:b:c", ":")
want := []string{"a", "b", "c"}
if !reflect.DeepEqual(ret, want) { // 无法比较的类型用反射
//测试用例失败
t.Errorf("want:%v but got:%v\n", want, ret)
}
}
// func Test2Splitstr(t *testing.T) {
// ret := Splitstr("a:b:c", ":")
// want := []string{"a", "b", "c"}
// if !reflect.DeepEqual(ret, want) {
// //测试用例失败
// t.Errorf("want:%v but got:%v\n", want, ret)
// }
// }
组测试和子测试
package splitstr
import (
"reflect"
"testing"
)
// func TestSplitstr(t *testing.T) {
// ret := Splitstr("a:b:c", ":")
// want := []string{"a", "b", "c"}
// if !reflect.DeepEqual(ret, want) {
// //测试用例失败
// t.Errorf("want:%v but got:%v\n", want, ret)
// }
// }
// func Test2Splitstr(t *testing.T) {
// ret := Splitstr("a:b:c", ":")
// want := []string{"a", "b", "c"}
// if !reflect.DeepEqual(ret, want) {
// //测试用例失败
// t.Errorf("want:%v but got:%v\n", want, ret)
// }
// }
// 组测试
// func Test3Splitstr(t *testing.T) {
// type testCase struct {
// str string
// sep string
// want []string
// }
// testGroup := []testCase{
// {"babcbebf", "b", []string{"", "a", "c", "e", "f"}},
// {"a:b:c", ":", []string{"a", "b", "c"}},
// {"abcdefg", "bc", []string{"a", "defg"}},
// }
// for _, tc := range testGroup {
// got := Splitstr(tc.str, tc.sep)
// if !reflect.DeepEqual(got, tc.want) {
// t.Fatalf("want:%v but got:%v\n", tc.want, got)
// }
// }
// }
// 子测试 ----命令:go test -run=Test3Splitstr/case1
func Test3Splitstr(t *testing.T) {
type testCase struct {
str string
sep string
want []string
}
testGroup := map[string]testCase{
"case1": {"babcbebf", "b", []string{"", "a", "c", "e", "f"}},
"case2": {"a:b:c", ":", []string{"a", "b", "c"}},
"case3": {"abcdefg", "bc", []string{"a", "defg"}},
}
for name, tc := range testGroup {
t.Run(name, func(t *testing.T) {
got := Splitstr(tc.str, tc.sep)
if !reflect.DeepEqual(got, tc.want) {
t.Fatalf("want:%v but got:%v\n", tc.want, got)
}
})
}
}
测试覆盖率
go test -cover
go test -cover -coverprofile=c.out
go tool cover -html=c.out
测试函数覆盖率:100%
测试代码覆盖率:60%
基准测试
go test -bench=Split
go test -bench=Split -benchmem
allocs/op
:内存申请数可以优化代码
var ret = make([]string, 0, strings.Count(str, sep)+1) // 将内存申请数由3变为1
// BenchmarkSplit 基准测试
func BenchmarkSplit(b *testing.B) {
for i := 0; i < b.N; i++ {
Splitstr("a:b:c", ":")
}
}
性能基准测试
// 斐波那契数列
func Fib(n int) int {
if n < 2 {
return n
}
return Fib(n-1) + Fib(n-2)
}
func benchmarkFib(b *testing.B, n int) {
for i := 0; i < b.N; i++ {
Fib(n)
}
}
func BenchmarkFib1(b *testing.B) { benchmarkFib(b, 1) }
func BenchmarkFib2(b *testing.B) { benchmarkFib(b, 2) }
func BenchmarkFib3(b *testing.B) { benchmarkFib(b, 3) }
func BenchmarkFib10(b *testing.B) { benchmarkFib(b, 10) }
func BenchmarkFib20(b *testing.B) { benchmarkFib(b, 20) }
func BenchmarkFib40(b *testing.B) { benchmarkFib(b, 40) }
fmt包
向外输出
Fprint
Fprint
系列函数会将内容输出到一个io.Writer
接口类型的变量w
中,我们通常用这个函数往文件中写入内容。
// 向标准输出写入内容
fmt.Fprintln(os.Stdout, "向标准输出写入内容")
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)
Sprint
Sprint
系列函数会把传入的数据生成并返回一个字符串。
s1 := fmt.Sprint("沙河小王子")
name := "沙河小王子"
age := 18
s2 := fmt.Sprintf("name:%s,age:%d", name, age)
s3 := fmt.Sprintln("沙河小王子")
fmt.Println(s1, s2, s3)
获取输入
fmt.Scan
- Scan从标准输入扫描文本,读取由空白符分隔的值保存到传递给本函数的参数中,换行符视为空白符。
- 本函数返回成功扫描的数据个数和遇到的任何错误。如果读取的数据个数比提供的参数少,会返回一个错误报告原因。
func main() {
var (
name string
age int
married bool
)
fmt.Scan(&name, &age, &married)
fmt.Printf("扫描结果 name:%s age:%d married:%t \n", name, age, married)
}
fmt.Scanf
- Scanf从标准输入扫描文本,根据format参数指定的格式去读取由空白符分隔的值保存到传递给本函数的参数中。
- 本函数返回成功扫描的数据个数和遇到的任何错误。
func main() {
var (
name string
age int
married bool
)
fmt.Scanf("1:%s 2:%d 3:%t", &name, &age, &married)
fmt.Printf("扫描结果 name:%s age:%d married:%t \n", name, age, married)
}
fmt.Scanln
- Scanln类似Scan,它在遇到换行时才停止扫描。最后一个数据后面必须有换行或者到达结束位置。
- 本函数返回成功扫描的数据个数和遇到的任何错误。
func main() {
var (
name string
age int
married bool
)
fmt.Scanln(&name, &age, &married)
fmt.Printf("扫描结果 name:%s age:%d married:%t \n", name, age, married)
}
bufio.NewReader
有时候我们想完整获取输入的内容,而输入的内容可能包含空格,这种情况下可以使用bufio
包来实现。示例代码如下:
func bufioDemo() {
reader := bufio.NewReader(os.Stdin) // 从标准输入生成读对象
fmt.Print("请输入内容:")
text, _ := reader.ReadString('\n') // 读到换行
text = strings.TrimSpace(text)
fmt.Printf("%#v\n", text)
}
Fscan系列
这几个函数功能分别类似于fmt.Scan
、fmt.Scanf
、fmt.Scanln
三个函数,只不过它们不是从标准输入中读取数据而是从io.Reader
中读取数据。
Sscan系列
这几个函数功能分别类似于fmt.Scan
、fmt.Scanf
、fmt.Scanln
三个函数,只不过它们不是从标准输入中读取数据而是从指定字符串中读取数据。
flag
获取命令行参数
os.Args()
package main
import (
"fmt"
"os"
)
// os.Args 获取命令行参数
func main() {
fmt.Printf("%#v\n", os.Args)
fmt.Println(os.Args[0], os.Args[2])
fmt.Printf("%T\n", os.Args)
}
flag(先解析再使用)——flag.Parse()
package main
import (
"flag"
"fmt"
)
// flag 获取命令行参数
func main() {
// 创建一个标志位参数
// name := flag.String("name", "hina", "请输入姓名")
// age := flag.Int("age", 18, "年龄")
// 使用flag
// flag.Parse() // 解析
// fmt.Println(*name)
// fmt.Println(*age)
var name string
// 将命令行中传入的name赋值给name
flag.StringVar(&name, "name", "hina", "姓名")
flag.Parse()
fmt.Println(name)
flag.Args() // 返回命令行参数后的其他参数,是[]string类型
flag.NArg() // 返回命令行参数后的其他参数个数
flag.NFlag() // 返回使用的命令行参数个数
}
pprof调试工具
main.exe -cpu=true
go tool pprof cpu.pprof
top3
// runtime_pprof/main.go
package main
import (
"flag"
"fmt"
"os"
"runtime/pprof"
"time"
)
// 一段有问题的代码
func logicCode() {
var c chan int // nil
for {
select {
case v := <-c: // 阻塞
fmt.Printf("recv from chan, value:%v\n", v)
default:
// time.Sleep(time.Second)
}
}
}
func main() {
var isCPUPprof bool // 是否开启CPUprofile的标志位
var isMemPprof bool // 是否开启内存profile的标志位
flag.BoolVar(&isCPUPprof, "cpu", false, "turn cpu pprof on")
flag.BoolVar(&isMemPprof, "mem", false, "turn mem pprof on")
flag.Parse()
if isCPUPprof {
f1, err := os.Create("./cpu.pprof") // 在当前路径下创建一个cpu.pprof的文件
if err != nil {
fmt.Printf("create cpu pprof failed, err:%v\n", err)
return
}
pprof.StartCPUProfile(file) // 往文件中记录 CPU profile信息
defer func() {
pprof.StopCPUProfile()
f1.Close()
}()
}
for i := 0; i < 8; i++ {
go logicCode()
}
time.Sleep(20 * time.Second)
if isMemPprof {
f2, err := os.Create("./mem.pprof")
if err != nil {
fmt.Printf("create mem pprof failed, err:%v\n", err)
return
}
pprof.WriteHeapProfile(f2)
f2.Close()
}
}
图形化
或者可以直接输入web,通过svg图的方式查看程序中详细的CPU占用情况。 想要查看图形化的界面首先需要安装graphviz图形化工具。
brew install graphviz
go-torch和火焰图
焰图(Flame Graph)是 Bredan Gregg 创建的一种性能分析图表,因为它的样子近似 🔥而得名。上面的 profiling 结果也转换成火焰图,如果对火焰图比较了解可以手动来操作,不过这里我们要介绍一个工具:go-torch
。这是 uber 开源的一个工具,可以直接读取 golang profiling 数据,并生成一个火焰图的 svg 文件。
安装go-torch
go get -v github.com/uber/go-torch
要生成火焰图,需要事先安装 FlameGraph工具,这个工具的安装很简单(需要perl环境支持),只要把对应的可执行文件加入到环境变量中即可。
- 下载安装perl:https://www.perl.org/get.html
- 下载FlameGraph:
git clone https://github.com/brendangregg/FlameGraph.git
- 将
FlameGraph
目录加入到操作系统的环境变量中。 - Windows平台的同学,需要把
go-torch/render/flamegraph.go
文件中的GenerateFlameGraph
按如下方式修改,然后在go-torch
目录下执行go install
即可。
压测工具wrk
推荐使用https://github.com/wg/wrk 或 https://github.com/adjust/go-wrk
使用go-torch
使用wrk进行压测:go-wrk -n 50000 http://127.0.0.1:8080/book/list
在上面压测进行的同时,打开另一个终端执行:go-torch -u http://127.0.0.1:8080 -t 30
30秒之后终端会出现如下提示:Writing svg to torch.svg
然后我们使用浏览器打开torch.svg
就能看到如下火焰图了。