介绍

记录一些可能常用到的实例,便于快速查询

读文件

按字节读取文件

这种方式是以字节为单位来读取,相对底层一些,代码量也较大

package main
import (
    "fmt"
    "io"
    "os"
)
func main() {
    file,_ := os.Open("test.txt")
    defer file.Close()
    buf := make([]byte, 1024)    // 每次读取1024字节
    var res string    // 存放最终的结果
    for  {
        count, err := file.Read(buf)
        if err == io.EOF {    // 检测是否到结尾
            break
        } else {
            currBytes := buf[:count]    // 读取了count字节
            res += string(currBytes)    // 最终结果
        }
    }
    fmt.Println(res)
}

借助ioutil来读取

由于 os.File 也是 io.Reader 的实现,我们可以调用 ioutil.ReadAll(io.Reader) 方法,将文件所有字节读取出来,省去了使用字节缓存循环读取的过程。

package main
import (
    "fmt"
    "io/ioutil"
    "os"
)
func main() {
    file,_ := os.Open("test.txt")
    defer file.Close()
    byteRes, _ := ioutil.ReadAll(file)    // 返回存放结果的切片
    fmt.Printf("%T\n", byteRes)    // []uint8
    fmt.Println(string(byteRes))
}

仅使用ioutil包来完成读取操作

为了进一步简化文件读取操作,ioutil 还提供了 ioutil.ReadFile(filename string) 方法,一行代码搞定读取任务

package main
import (
    "fmt"
    "io/ioutil"
)
func main() {
    data, _ := ioutil.ReadFile("test.txt")
    fmt.Println(string(data))
}

利用Scanner按行读取

逐行读取文件内容,这个时候可以 Scanner 来完成

package main
import (
    "bufio"
    "fmt"
    "os"
)
func main() {
    file, _ := os.Open("test.txt")
    defer file.Close()
    scanner := bufio.NewScanner(file)    // 类似Java中的Scanner
    for scanner.Scan() {
        fmt.Println(scanner.Text())
    }
}

写文件

使用ioutil

刚才用到了ioutil.ReadFile,与之对应的,肯定也有WriteFile 该函数属于全覆盖写入,如果文件不存在,则会根据指定的权限创建文件,如果存在,则会先清空文件原有内容,然后再写入新数据

package main
import (
    "fmt"
    "io/ioutil"
    "os"
)
func main() {
    data := []byte("hello d4m1ts")
    fmt.Println(os.FileMode(0666).String())    // -rw-rw-rw-
    ioutil.WriteFile("test.txt", data, 0666)
}

通过File句柄

os.OpenFile(name string, flag int, perm FileMode)方法,通过指定额外的 读写方式文件权限 参数,使文件操作变得更为灵活。 flag 有以下几种常用的值:

  • os.O_CREATE: create if none exists 不存在则创建
  • os.O_RDONLY: read-only 只读
  • os.O_WRONLY: write-only 只写
  • os.O_RDWR: read-write 可读可写
  • os.O_TRUNC: truncate when opened 文件长度截为0:即清空文件
  • os.O_APPEND: append 追加新数据到文件
    package main
    import (
      "fmt"
      "os"
    )
    func main() {
      file,_ := os.OpenFile("test.txt", os.O_RDWR | os.O_APPEND | os.O_CREATE, 0666) // 按照特定权限打开
      defer file.Close()
      data := []byte("hello d4m1ts")
      count, _ := file.Write(data)    // 按字节写入,返回的count为写入的字节数
      fmt.Println(count)
      count, _ = file.WriteString("\nHello D4m1ts")    // 按字符串写入
      fmt.Println(count)
      file.Sync()    // 确保写入到磁盘
    }
    

    通过bufio包

    这种方式其实是在File句柄上做了一层封装,调用方式和上面直接写入非常相似
    package main
    import (
      "bufio"
      "fmt"
      "os"
    )
    func main() {
      file,_ := os.OpenFile("test.txt", os.O_RDWR | os.O_APPEND | os.O_CREATE, 0666)
      defer file.Close()
      writer := bufio.NewWriter(file)
      data := []byte("hello d4m1ts")
      count, _ := writer.Write(data)    // 按字节写入,返回的count为写入的字节数
      fmt.Println(count)
      count, _ = writer.WriteString("\nHello D4m1ts")    // 按字符串写入
      fmt.Println(count)
      writer.Flush()    // 清空缓存,确保写入到磁盘
    }
    

    正则表达式

    匹配内容是否存在

    package main
    import (
      "fmt"
      "regexp"
    )
    func main() {
      regex := "\\d{1,3}"
      res, _ := regexp.MatchString(regex, "123123")
      res1, _ := regexp.MatchString(regex, "aaaa")
      fmt.Println(res)    // true
      fmt.Println(res1)    // false
    }
    

    提取内容

    package main
    import (
      "fmt"
      "regexp"
    )
    func main() {
      regex, _ := regexp.Compile("(\\d{1,3})\\d{1,3}")    // 编译正则表达式
      fmt.Println(regex.MatchString("123123123123"))    // true
      fmt.Println(regex.FindString("123213123123"))     // 123213 返回第一个匹配的
      fmt.Println(regex.FindStringIndex("123213123123"))    // [0 6] 返回第一个匹配的开始和结尾的索引
      fmt.Println(regex.FindStringSubmatch("123213123123"))    // [123213 123] 返回包括()这种子匹配的
      fmt.Println(regex.FindAllString("123213123123",-1))    // [123213 123123] 返回匹配的所有内容,n表示为返回个数,-1则返回全部
      fmt.Println(regex.FindAllStringSubmatch("123213123123",-1))    // [[123213 123] [123123 123]] 同时返回子匹配的结果
      fmt.Println(regex.FindAll([]byte("123123123123"), -1))    // [[49 50 51 49 50 51] [49 50 51 49 50 51]] 通过字节去匹配,返回的也是字节的结果
    }
    

    替换内容

    package main
    import (
     "fmt"
     "regexp"
    )
    func main() {
     regex, _ := regexp.Compile("(\\d{1,3})\\d{1,3}")   // 编译正则表达式
     fmt.Println(regex.ReplaceAllString("123123123213","a"))    // aa
    }
    

    其他

    在线正则表达式测试网站:https://regex101.com/ golang正则匹配的时候,.默认是不匹配换行的,所以要匹配多行数据就容易出问题 这个时候可以采用如下格式,让.也可以匹配换行
    (?s).*
    
    实例:

JSON序列化和反序列化

标准JSON模块

Go 语言标准库 encoding/json 提供了操作 JSON 的方法,一般可以使用 json.Marshaljson.Unmarshal 来序列化和解析 JSON 字符串 实例一:

package main
import (
    "encoding/json"
    "fmt"
)
func main() {
    // 序列化
    s1 := []string{"apple", "peach", "pear"}
    s2, _ := json.Marshal(s1)    // 转字符串
    fmt.Println(string(s2))    // ["apple","peach","pear"]
    // 反序列化
    var s3 [] string
    json.Unmarshal([]byte(s2), &s3)    // 字符串恢复
    fmt.Println(s3)    // [apple peach pear]
    fmt.Println(len(s3), s3[0])    // 3 apple
}

实例二:

package main
import (
    "encoding/json"
    "fmt"
)
// 定义结构体,变量名注意大写,因为跨到json包了
type User struct {
    Name string
    Age int
}
func main() {
    // 初始化结构体
    user := User{
        Name: "d4m1ts",
        Age: 18,
    }
    // 序列化,转字符串
    s1, _ := json.Marshal(user)
    fmt.Println(string(s1))
    // 反序列化,恢复为原来的结构
    user2 := User{}
    json.Unmarshal(s1, &user2)
    fmt.Println(user2)
}

更优的jsoniter

标准库 encoding/json 在使用时需要预先定义结构体,使用时显得不够灵活。这时候可以尝试使用 github.com/json-iterator/go 模块,其除了提供与标准库一样的接口之外,还提供了一系列更加灵活的操作方法

go get -v github.com/json-iterator/go
package main
import (
    "fmt"
    jsoniter "github.com/json-iterator/go"
)
func main() {
    // 反序列化,恢复为原来的结构
    s := `{"a":"b", "c":["d","e","f"]}`
    res := jsoniter.Get([]byte(s), "c")
    fmt.Println(res.ToString())    // ["d","e","f"] 只解析"c"的内容部分
}

扩展

大多数时候获取的json数据可能是映射+切片形式的,只有上面的一些内容很难搞,所以还是补充一下 使用.GetInterface()会自动给结果转换为interface{},再通过这个结果继续转换,如[]interface{} 想要拿到最后的数据,只需要通过数据.(对应的格式)即可,如 aaa.(string)表示转换为string

[!note] 一步一步看吧,用.GetInterface()或者等报错提示,就可以看到应该转换的格式了,如下图右边就是可以转换的格式

func main() {
    strings := "{\"a\":[{\"b\":\"c\"}]}"
    var res interface{}
    res = jsoniter.Get([]byte(strings), "a").GetInterface()
    fmt.Println(res.([]interface{}))
    for _,i := range res.([]interface{}) {
        fmt.Println(i.(map[string]interface{}))
    }
}

时间日期

package main
import (
    "fmt"
    "time"
)
func main() {
    p := fmt.Println
    // 现在的时间
    now := time.Now()
    p(now)
    // 休眠1秒
    time.Sleep(time.Second * 1)
    p(time.Now())    // 现在的时间
    // 格式类型转换
    t1, _ := time.Parse(time.RFC3339, "2012-11-01T22:08:41+00:00")
    p(t1)    // 2012-11-01 22:08:41 +0000 +0000
    // 格式化输出
    p(now.Format("3:04PM"))    // 5:14PM
    p(now.Format("Mon Jan _2 15:04:05 2006"))    // Tue Dec 28 17:15:49 2021
    p(now.Format("2006-01-02T15:04:05.999999-07:00")) // 2021-12-28T17:15:49.121777+08:00
    p(now.Format(time.RFC850))    // Tuesday, 28-Dec-21 17:20:02 CST
    fmt.Printf("%d-%02d-%02dT%02d:%02d:%02d-00:00\n",
        now.Year(), now.Month(), now.Day(),
        now.Hour(), now.Minute(), now.Second()) // 2021-12-28T17:20:02-00:00
}

随机数

伪随机数

Go的math/rand包提供伪随机数生成。例如,rand.Intn返回一个随机int n,0 <= n <100。 伪随机生成的数字是确定的,不论在什么机器、什么时间,只要执行的随机代码一样,那么生成的随机数就一样

package main
import (
    "fmt"
    "math/rand"
)
func main() {
    p := fmt.Println
    p(rand.Int())    // 5577006791947779410
    p(rand.Int31n(int32(60)))    // 27
    for i:=0; i<5; i+=1 {
        p(rand.Float64())    // 0.6645600532184904 每次执行的结果都是一样
    }
}

真随机数

crypto/rand是为了提供更好的随机性满足密码对随机数的要求,在linux上已经有一个实现就是/dev/urandomcrypto/rand就是从这个地方读“真随机”数字返回,但性能比较慢

package main
import (
    "crypto/rand"
    "math/big"
)
func main() {
    for i := 0; i < 4; i++  {
        n, _ := rand.Int(rand.Reader, big.NewInt(100))
        println(n.Int64())
    }
}

URL解析

可以直接使用url.Parse(string u)来解析,其中包括方案,身份验证信息,主机,端口,路径,查询参数和查询片段等信息

package main
import (
    "fmt"
    "net/url"
)
func main() {
    u := "https://admin:password@blog.gm7.org/test/point?a=123&b=test"
    uu, _ := url.Parse(u)
    fmt.Println(uu.Scheme)
    fmt.Println(uu.User)
    fmt.Println(uu.User.Username())
    fmt.Println(uu.Host)
    fmt.Println(uu.Path)
    fmt.Println(uu.Hostname())
    fmt.Println(uu.Query())
}
/*
https
admin:password
admin
blog.gm7.org
/test/point
blog.gm7.org
map[a:[123] b:[test]]
*/

Base64

package main
import (
    "encoding/base64"
    "fmt"
)
func main() {
    a := "123456"
    res := base64.StdEncoding.EncodeToString([] byte(a))
    fmt.Println(res)    // MTIzNDU2
    decod, _ := base64.StdEncoding.DecodeString(res)
    fmt.Println(string(decod)) // 123456
}

命令行参数

从命令行获取参数,得到的是一个切片

package main
import (
    "fmt"
    "os"
)
func main() {
    args := os.Args
    fmt.Println(args)
}
/*
 go run Hello.go 123 456
[/var/folders/fw/tddtsjp91wb9q64l5xt7jd540000gn/T/go-build3185553057/b001/exe/Hello 123 456]
*/

执行系统命令

在 Golang 中用于执行命令的库是 os/execexec.Command 函数返回一个 Cmd 对象,根据不同的需求,可以将命令的执行分为三种情况

  1. 只执行命令,不获取结果
  2. 执行命令,并获取结果(不区分 stdoutstderr
  3. 执行命令,并获取结果(区分 stdoutstderr

    只执行命令,不获取结果

    直接调用Run()函数

    package main
    import (
     "fmt"
     "os/exec"
    )
    func main() {
     res := exec.Command("open", "-na", "Calculator").Run() // run会阻塞等到命令执行结束
     fmt.Println(res) // <nil>
    }
    

    执行命令获取结果

    可以调用 cmdCombinedOutput 函数

    package main
    import (
     "fmt"
     "os/exec"
    )
    func main() {
     cmd := exec.Command("ls", "-la")
     res, _ := cmd.CombinedOutput()
     fmt.Println(string(res)) // ls -la 执行结果
    }
    

    执行命令获取结果并区分stdoutstderr

    package main
    import (
     "bytes"
     "fmt"
     "os/exec"
    )
    func main() {
     cmd := exec.Command("ls", "-la")
     var stdout, stderr bytes.Buffer
     cmd.Stdout = &stdout
     cmd.Stderr = &stderr
     cmd.Run()
     fmt.Println(string(stdout.Bytes())) // ls -la 执行结果
    }
    

    多条命令使用管道执行

    就是类似shell里面|的作用,ls -la | grep go

    package main
    import (
     "fmt"
     "os"
     "os/exec"
    )
    func main() {
     cmd1 := exec.Command("ls", "-la")
     cmd2 := exec.Command("grep", "go")
     cmd2.Stdin, _ = cmd1.StdoutPipe()
     cmd2.Stdout = os.Stdout
     cmd2.Start()
     cmd1.Run()
     cmd2.Wait()
     fmt.Println(cmd2.Stdout) // ls -la | grep go 执行结果
    }
    

    扩展

    前面每个空格间都需要单独一个参数,有时候很长就不方便,可以采用如下的方式来

    cmd := exec.Command("/bin/sh","-c","expr 2 + 33")
    

    HTTP请求

    快速发起get请求

    package main
    import (
     "fmt"
     "io/ioutil"
     "net/http"
    )
    func main() {
     url := "https://www.baidu.com/"
     response,_ := http.Get(url)    // 发起get请求
     defer response.Body.Close()
     fmt.Println(response.StatusCode)
     fmt.Println(response.Header.Get("Server"))
     body, _ := ioutil.ReadAll(response.Body)
     fmt.Println(string(body))    // 源码
    }
    

    收到的数据包

    GET / HTTP/1.1
    Host: 127.0.0.1:8000
    User-Agent: Go-http-client/1.1
    Accept-Encoding: gzip
    

    带有参数的GET请求

    可以直接在上面的url后面直接构造参数,也可以通过如下的方式手动设置参数

    package main
    import (
     "fmt"
     "net/http"
     "net/url"
    )
    func main() {
     u := "http://baidu.com/"
     Url, _ := url.Parse(u)
     param := url.Values{}
     param.Set("name", "d4m1ts")
     param.Set("中文", "汉字测试")
     Url.RawQuery = param.Encode()    // 包含URL编码
     fmt.Println(Url)    // http://baidu.com/?name=d4m1ts&%E4%B8%AD%E6%96%87=%E6%B1%89%E5%AD%97%E6%B5%8B%E8%AF%95
     resp,_ := http.Get(Url.String())
     fmt.Println(resp.StatusCode)    // 200
    }
    

    收到的数据包

    GET /?name=d4m1ts&%E4%B8%AD%E6%96%87=%E6%B1%89%E5%AD%97%E6%B5%8B%E8%AF%95 HTTP/1.1
    Host: 127.0.0.1:8000
    User-Agent: Go-http-client/1.1
    Accept-Encoding: gzip
    

    返回JSON的数据包

    返回是json格式,如何快速格式化数据

  4. 返回的json内容

    {
    "args": {}, 
    "headers": {
      "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9", 
      "Accept-Encoding": "gzip, deflate", 
      "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8", 
      "Dnt": "1", 
      "Host": "httpbin.org", 
      "Sec-Gpc": "1", 
      "Upgrade-Insecure-Requests": "1", 
      "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.55 Safari/537.36", 
      "X-Amzn-Trace-Id": "Root=1-61cbb6de-6e8a5d6a2710be6f22da6f92"
    }, 
    "origin": "213.239.21.35", 
    "url": "http://httpbin.org/get"
    }
    
  5. 一样的发起http请求,只是最后用JSON来反序列化而已

    package main
    import (
      "fmt"
      jsoniter "github.com/json-iterator/go"
      "io/ioutil"
      "net/http"
    )
    func main() {
      u := "http://httpbin.org/get"
      resp, err := http.Get(u)
      if err == nil {    // 请求成功
          body, _ := ioutil.ReadAll(resp.Body)
    
          origin := jsoniter.Get(body, "origin")
          fmt.Println(origin.ToString())    // 213.239.21.35
          headers := jsoniter.Get(body, "headers")
          fmt.Println(headers.Get("Host").ToString())    // httpbin.org
      }
    }
    

    自定义Header头

    package main
    import (
      "fmt"
      jsoniter "github.com/json-iterator/go"
      "io/ioutil"
      "net/http"
      "time"
    )
    func main() {
      // 创建一个HTTP请求,但是不发送请求
      u := "http://httpbin.org/get"
      req, _ := http.NewRequest("GET", u, nil)
      req.Header.Set("User-Agent", "Test GO")
      req.Header.Set("Name", "d4m1ts")
      // 发送刚才创建的请求
      client := http.Client{
          Timeout: 3*time.Second,    // 超时时间
      }
      resp, _ := client.Do(req)
      body, _ := ioutil.ReadAll(resp.Body)
      headers := jsoniter.Get(body, "headers")
      fmt.Println(headers.ToString())
    }
    /*
    {
      "Accept-Encoding": "gzip",
      "Host": "httpbin.org",
      "Name": "d4m1ts",
      "User-Agent": "Test GO",
      "X-Amzn-Trace-Id": "Root=1-61cbbb0d-68f21a6c5c36abd861b6fe99"
    }
    */
    

    收到的数据包

    GET / HTTP/1.1
    Host: 127.0.0.1:8000
    User-Agent: Test GO
    Name: d4m1ts
    Accept-Encoding: gzip
    

    快速发起POST请求

    方法一:使用http.Post,有一点点麻烦

    package main
    import (
      "net/http"
      "strings"
    )
    func main() {
      u := "http://127.0.0.1:8000"
      http.Post(u, "application/x-www-form-urlencoded", strings.NewReader("aa=bb"))
    }
    

    收到的http请求

    POST / HTTP/1.1
    Host: 127.0.0.1:8000
    User-Agent: Go-http-client/1.1
    Content-Length: 5
    Content-Type: application/x-www-form-urlencoded
    Accept-Encoding: gzip
    aa=bb
    

    方法二:使用http.PostForm,相对简单,但是无法设置content-type,没那么自由

    package main
    import (
      "net/http"
      "net/url"
    )
    func main() {
      u := "http://127.0.0.1:8000"
      param := url.Values{}
      param.Set("aaa", "bbb")
      http.PostForm(u, param)
    }
    

    收到的http请求

    POST / HTTP/1.1
    Host: 127.0.0.1:8000
    User-Agent: Go-http-client/1.1
    Content-Length: 7
    Content-Type: application/x-www-form-urlencoded
    Accept-Encoding: gzip
    aaa=bbb
    

    自定义POST请求

    get差不多,只不过是多了设置content-type和post数据的步骤而已 举例为发送json数据

    package main
    import (
      "bytes"
      "encoding/json"
      "net/http"
    )
    func main() {
      // 创建请求,但是不发起
      u := "http://127.0.0.1:8000"
      param := map[string]string{
          "aa": "bb",
          "name": "d4m1ts",
      }
      buf, _ := json.Marshal(param)    // 序列化的json
      req, _ := http.NewRequest("POST", u, bytes.NewReader(buf))
      req.Header.Set("User-Agent", "UA TEST")
      req.Header.Set("Content-Type", "application/json")
      // 发送刚才的请求
      client := http.Client{}
      client.Do(req)
    }
    

    收到的http数据包

    POST / HTTP/1.1
    Host: 127.0.0.1:8000
    User-Agent: UA TEST
    Content-Length: 27
    Content-Type: application/json
    Accept-Encoding: gzip
    {"aa":"bb","name":"d4m1ts"}
    

    忽略证书

    有些时候一些ssl网站因为证书问题也会抛出panic异常,所以一般可以忽略SSL证书,在初始化http客户端的时候忽略,代码如下

    tr := &http.Transport{
      TLSClientConfig: &tls.Config{InsecureSkipVerify: true},    // 忽略SSL证书
    }
    Client := http.Client{
      Transport: tr,
    }
    

    cookieJar和代理设置

    // 初始化Client
      jar, _ := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})
      proxy,_ := url.Parse("socks5://127.0.0.1:1080")
      netTransport := &http.Transport{
          Proxy:                 http.ProxyURL(proxy),
          MaxIdleConnsPerHost:   10,
          ResponseHeaderTimeout: time.Second * time.Duration(5),
      }
      Client = http.Client{
          Transport: netTransport,
          Jar:     jar,
          Timeout: time.Second * 10,
      }
      Client.Get("http://cip.cc")
    

    gzip解压

    有些返回的数据是二进制乱码的,这个时候我们就需要进行gzip解压

    resp, _ := Client.Do(req)
      reader,_ := gzip.NewReader(resp.Body)
      source, _ := ioutil.ReadAll(reader)
      fmt.Println(string(source))
    

    go flag

    这个库主要用来判断工具命令行传入的参数用的 虽然go有os.Args,但是不如这个好用 演示:

    func main() {
      var filePath string
      flag.StringVar(&filePath, "file", "", "markdown文件路径")
      flag.Parse()
      if flag.Lookup("file").DefValue == flag.Lookup("file").Value.String() { // 避免使用默认参数,所以加个判断
          flag.Usage()
      }
      if flag.NFlag() == 0 { // 使用的命令行参数个数,这个地方可以用来判断用户是否输入参数(程序正常情况下会使用默认参数)
          flag.Usage()
          os.Exit(0)
      }
    }
    

    [!NOTE] 还有一些其他的参数,可以自己研究下,基本上看到名字就知道啥意思,主要用的就上面那些 如果觉得帮助不好看,可以重写flag.Usage()这个方法

flag.Usage = func() {
        fmt.Fprintf(flag.CommandLine.Output(), "markdown图片自动上传到图床\nUsage of %s:\n", os.Args[0])
        flag.PrintDefaults()
    }

颜色输出

最简单的版本

package main
import "fmt"
func main() {
    fmt.Printf("\033[1;31;40m%s\033[0m\n","Red.")
    fmt.Printf("\033[1;37;41m%s\033[0m\n","Red.")
}

输出所有颜色

package main
import "fmt"
func main() {
   for b := 40; b <= 47; b++ { // 背景色彩 = 40-47
      for f := 30; f <= 37; f++ { // 前景色彩 = 30-37
         for d := range []int{0, 1, 4, 5, 7, 8} { // 显示方式 = 0,1,4,5,7,8
            fmt.Printf(" %c[%d;%d;%dm%s(f=%d,b=%d,d=%d)%c[0m ", 0x1B, d, b, f, "", f, b, d, 0x1B)
         }
         fmt.Println("")
      }
      fmt.Println("")
   }
}

但是每一次都这样很难记住,所以已经有人给他封装成了一个包:https://github.com/fatih/color,可以快速看下它的说明文档 实例:

package main
import (
    "github.com/fatih/color"
)
func main() {
    color.Blue("aaaa%scccc", "bbb")
    color.Red("red red")
    color.Magenta("And many others ..")
}

参考

Copyright © aaron 2023 all right reserved,powered by Gitbook该文章修订时间: 2023-08-26 17:44:27

results matching ""

    No results matching ""