Go语言学习笔记

安装环境

官网

Windows使用chocolatey安装

choco install golang

Arch使用yaourt安装

yaourt -Syu go

配置Visual Studio Code

参见VS Code配置Go插件

命令行使用

Go is a tool for managing Go source code.

Usage:

    go <command> [arguments]

The commands are:

    bug         start a bug report
    build       compile packages and dependencies
    clean       remove object files and cached files
    doc         show documentation for package or symbol
    env         print Go environment information
    fix         update packages to use new APIs
    fmt         gofmt (reformat) package sources
    generate    generate Go files by processing source
    get         download and install packages and dependencies
    install     compile and install packages and dependencies
    list        list packages or modules
    mod         module maintenance
    run         compile and run Go program
    test        test packages
    tool        run specified go tool
    version     print Go version
    vet         report likely mistakes in packages

Use "go help <command>" for more information about a command.

Additional help topics:

    buildmode   build modes
    c           calling between Go and C
    cache       build and test caching
    environment environment variables
    filetype    file types
    go.mod      the go.mod file
    gopath      GOPATH environment variable
    gopath-get  legacy GOPATH go get
    goproxy     module proxy protocol
    importpath  import path syntax
    modules     modules, module versions, and more
    module-get  module-aware go get
    packages    package lists and patterns
    testflag    testing flags
    testfunc    testing functions

Use "go help <topic>" for more information about that topic.

初期用的到的大概只有go run xxx.go, go build xxx.go, go clear

run是运行,build是编译成二进制文件,clear则是清除文件

语法

Go比较类似C语言结合Node.js

Hello World

主要分为以下几个部分(文件名为hello.go)

/* 包名称 */

// main包
package main

/* 模块引入 */
import (
    "fmt"
)

/* 函数声明 */

func main() {
    // main函数,程序入口
    fmt.Println("Hello World!")
}

使用go run hello.go,运行输出Hello World!

变量与常量

一般来说,可以直接直接使用var a int来声明一个int变量,使用var b int = 1来为一个int变量赋初值(对于这种情况,int可省略,编译器可以自动推断). 同时,又可以进一步将声明并赋初值简化为b := 1

注: 对于可以直接自动推断,而开发者又写明类型的变量,Go Lint反而会进行报错: should omit type int from declaration of var a; it will be inferred from the right-hand side

同变量类似,将var改为const就是声明常量

循环

for循环

如同C一样,可以使用初始值;循环条件;后续处理的形式进行循环
注1: 这里初始条件如果用局部变量,一定要是i := 0这种声明形式
注2: 自增、自减只有i++、i--的形式,没有++i、--i的形式,同时自增自减也不存在返回值

for i := 0; i < 10; i++ {
    fmt.Println(i)
}

同时,虽然go没有while关键字,但是只传入一个参数时,其用法等同于while

i := 10
for i > 0 {
    fmt.Println(i)
    i--
}

条件语句

if/else

如同C语言的判断一样,可以使用if/else进行逻辑判断
不同的是,这里大括号是不能省略的,而小括号则是不需要存在的

同时,Go里没有三目运算符,因此即使是一些简单的判断,也应该使用完整的if/else

if 1 == 1 {
    fmt.Println("yes")
} else {
    fmt.Println("no")
}

switch/case

与C语言的switch/case类似,Go中switch会将switch的变量逐个比较case的结果
如果相同,则执行对应的代码(只执行第一个满足的);如果所有结果都不相同,则会执行default对应的内容(如果存在)
注: 如果没有fallthrouth,执行完该代码段直接跳出,否则将会"穿透"代码段,按照C语言的效果执行

c := 'a'
switch c {
case 'a':
    fmt.Println("is a")
    fallthrough
case 'b':
    fmt.Println("is b")
case 'c':
    fmt.Println("is c")
default:
    fmt.Println("default")
}

除此之外,当switch后没有跟着一个变量名时,可以使用类似下面的语法进行选择

x := 'a'
y := 'b'
switch {
case x == 'b':
    fmt.Println("is a")
case x == 'c', y == 'b':
    fmt.Println("is b")
}

数组与分片

数组

与绝大部分语言一样,Go也存在数组

arr := [6]int{1, 2, 3, 4}
var arr2 [6]int = [6]int{1, 2, 3, 4}
arr3 := arr[1:3]

fmt.Println(arr, len(arr), cap(arr), reflect.TypeOf(arr))
fmt.Println(arr2, len(arr2), cap(arr2), reflect.TypeOf(arr2))
fmt.Println(arr3, len(arr3), cap(arr3), reflect.TypeOf(arr3))

输出结果

[1 2 3 4 0 0] 6 6 [6]int  
[1 2 3 4 0 0] 6 6 [6]int  
[2 3] 2 5 []int  

未定义的部分会初始化为对应类型的默认值,同时可以使用len()函数获取其长度

数组要求其实际存放的数值数量不大于申请的数组长度,并且数组长度必须显式指明

可以使用len(),cap()来查看数组长度

同时可以使用类似Python的[start:end]来切片出数组的一部分(切出的结果为切片(slice))

切片

切片类似于Python中的list,是可变长度数组

当声明数组时,不去指明申请的长度,则会申请出一个切片(slice),或者使用make(type, len, cap)来初始化出一个切片

slice := []int{1, 2, 3, 4}
var slice2 []int = []int{1, 2, 3, 4}
slice3 := slice[1:3]
slice4 := make([]int, 5, 10)
var slice5 []int = make([]int, 9, 9)
copy(slice5, slice)

fmt.Println(slice, len(slice), cap(slice), reflect.TypeOf(slice))
fmt.Println(slice2, len(slice2), cap(slice2), reflect.TypeOf(slice2))
fmt.Println(slice3, len(slice3), cap(slice3), reflect.TypeOf(slice3))
fmt.Println(slice4, len(slice4), cap(slice4), reflect.TypeOf(slice4))
fmt.Println(slice5, len(slice5), cap(slice5), reflect.TypeOf(slice5))
[2 3] 2 5 []int  
[1 2 3 4] 4 4 []int  
[1 2 3 4] 4 4 []int  
[2 3] 2 3 []int  
[0 0 0 0 0] 5 10 []int  
[1 2 3 4 0 0 0 0 0] 9 9 []int

除去可以使用[start:end]切片外,也可以使用append(slice, item1, item2, ...)添加元素,以及使用copy(dist_slice, source_slice)来复制切片

需要注意的是,append()不是一个成员函数,他不会改变原有的slice,而是会返回一个新的slice

关于capacity的解释

capacity属于申请但是未用到的空间,类似于C++ vector中每次扩容空间时,剩余等待使用的部分

slice := make([]int, 5, 10)
for i := 0; i < 5; i++ {
    slice[i] = i
}
slice2 := slice[1:4]
slice3 := slice2[:cap(slice2)]
slice4 := slice[1:4]
slice5 := make([]int, 2, 4)
copy(slice5, slice4)
slice6 := slice5[:cap(slice5)]
slice[2] = 9

fmt.Println(slice, len(slice), cap(slice), reflect.TypeOf(slice))
fmt.Println(slice2, len(slice2), cap(slice2), reflect.TypeOf(slice2))
fmt.Println(slice3, len(slice3), cap(slice3), reflect.TypeOf(slice3))
fmt.Println(slice4, len(slice4), cap(slice4), reflect.TypeOf(slice4))
fmt.Println(slice5, len(slice5), cap(slice5), reflect.TypeOf(slice5))
fmt.Println(slice6, len(slice6), cap(slice6), reflect.TypeOf(slice6))
[0 1 9 3 4] 5 10 []int
[1 9 3] 3 9 []int
[1 9 3 4 0 0 0 0 0] 9 9 []int
[1 9 3] 3 9 []int
[1 2] 2 4 []int
[1 2 0 0] 4 4 []int
  1. 切片操作会放弃(后移slice指向实际数据所在内存块的指针)掉被切掉前面的部分,而将被切点的后续部分存放在capacity中(减小length与capacity)
  2. capacity部分不能直接操作,如果想要恢复出来只能再次使用切片操作来“拓展长度”
  3. copy会将length范围内的复制到新的slice中
  4. 非copy的操作只能改变显示的起始位置,显示长度这些值,capacity则用于防止内存溢出;并且对于同一段内存,当元素值修改后,对应的所有slice都会被修改

具体数据结构如下图(箭头表示指向内存中起始位置的指针,横线表示数据长度)

对于golang slice的理解对于golang slice的理解

参见: 如何理解 slice 的 capacity? - 达达的回答 - 知乎

map

map是关联数组,也即C++中的map,Python中的字典(dict),JavaScript中的对象(Object)
当然按照其原理,也可以叫做哈希(hash)

dict := map[string]int{"a": 1, "b": 2, "c": 3}
var dict2 map[string]int = make(map[string]int)
fmt.Println(dict, len(dict))
fmt.Println(dict2, len(dict2))

var value int
var status bool

value, status = dict["a"]
fmt.Println(value, status)

delete(dict, "a")
dict["b"] = 5
dict["d"] = 6

fmt.Println(dict, len(dict))

value, status = dict["a"]
fmt.Println(value, status)

结果如下

map[a:1 b:2 c:3] 3
map[] 0
1 true
map[d:6 b:5 c:3] 3
0 false

map的类型为map[keyType]valueType,除去直接用用类型声明外,也可以用make()声明

取值和赋值同Python与JavaScript一样,直接使用[key]来取/赋值
不同的是,取值时有一个隐藏的第二返回值,为布尔型(bool)的值,表明该key是否存在于关联数组中
如果key存在,则会返回(value, true)二元组
否则返回(typeDefaultValue, false)二元组

使用len()可以得到键值对的个数,使用delete()则可以删除一组键值对

range

与Python类似,Go也有range关键字,不同的是Go中其不是函数

  • 对于数组、切片,其将返回(index, value)二元组
  • 对于关联数组,其将返回(key, value)二元组
  • 对于字符串,其将返回(index, runeValue)二元组
arr := []int{1, 2}
dict := map[string]int{"a": 1, "b": 2}
str := "ab"

for idx, value := range arr {
    fmt.Println(idx, value)
}
for key, value := range dict {
    fmt.Println(key, value)
}
for idx, value := range str {
    fmt.Println(idx, value)
}
0 1
1 2
a 1
b 2
0 97
1 98

函数

函数的结构

类似所有的静态类型语言,参数和返回值都要显式声明(类型在变量名后)
同时返回值允许为多值

在参数类型前使用...说明是一个边长参数
在调用时,变量后使用...可将数组、切片解包为参数
(该部分使用与Python中的*相似)

package main

import "fmt"

func add(nums ...int) (int, int) {
    var sum int = 0
    for _, num := range nums {
        sum += num
    }
    return sum, len(nums)
}

func add2(nums []int) (int, int) {
    var sum int = 0
    for _, num := range nums {
        sum += num
    }
    return sum, len(nums)
}

func main() {
    arr := []int{1, 2, 3, 4, 5}
    sum, length := add(1, 2, 3, 4, 5)
    fmt.Println("sum =", sum, "len =", length)
    sum, length = add(arr...)
    fmt.Println("sum =", sum, "len =", length)
    sum, length = add2(arr)
    fmt.Println("sum =", sum, "len =", length)
}
sum = 15 len = 5
sum = 15 len = 5
sum = 15 len = 5

闭包

闭包(closure)是指包含自由变量的函数
尽管一个变量生成在一个函数内,满足局部(函数作用域内)变量的要求,只有函数内能访问
但是它又同时不会因为函数开始而生成、函数结束而销毁
其与函数本身一同存在,如同函数本身附带的一个状态

闭包在JavaScript中运用非常广泛,如声明一个全局存在但又能够避免被外部随意更改的自由变量(如请求数据时防抖用的列表)
闭包实际是通过将一个内部的函数暴露出来,通过内层函数来操作外层函数的变量(自由变量)
而在Python中,其常见的形式是装饰器(@app.route('/data')),通过闭包的结构,来包裹一个函数,实现对输入前预处理以及对输出后进一步处理

闭包的自由变量由于不会被销毁,因此会持续占用空间,可能会对性能有影响

Go中的闭包与别的语言中的闭包同样:

package main

import "fmt"

func increase() func() int {
    var cnt int = 0
    return func() int {
        cnt += 1
        return cnt
    }
}

func main() {
    getCnt := increase()
    fmt.Println(getCnt())
    fmt.Println(getCnt())
    fmt.Println(getCnt())
    fmt.Println(getCnt())
    fmt.Println(getCnt())
}
1
2
3
4
5

指针

与C语言一样,Go也有指针,使用也和C相同
*a表示指针,&a表示取地址

比如经典的swap()实现如下

package main

import "fmt"

func swap1(a int, b int) (int, int) {
    return b, a
}

func swap2(a *int, b *int) (int, int) {
    var c int
    c = *a
    *a = *b
    *b = c
    return *a, *b
}

func main() {
    a := 1
    b := 2
    c, d := swap1(a, b)
    fmt.Println(a, b, c, d)
    c, d = swap2(&a, &b)
    fmt.Println(a, b, c, d)
}

函数参数

1 2 2 1
2 1 2 1

结构体、方法、接口

Java中的面对对象

首先先回归到Java的概念.
Java是一门基于面对对象的语言,万物皆是对象.而面对对象最重要的则是继承这一概念

举一个例子,如果要实现web端的请求(request)到响应(response)的转换过程
通常要经历的操作的如下:

  1. 接收到http发送来的request内容(一段文本)
  2. 根据这个http内容格式提取出请求的格式(如GET、POST)
  3. 处理出请求的参数
  4. 权限验证
  5. 实际的请求处理过程
  6. 将处理后的结果(如json)封装成response通过http返回回去

通常而言,后端程序员要负责开发的就是步骤5

对于面对过程的语言,可以使用类似闭包的操作来实现这个处理过程,让程序员只需要将精力放在步骤5,而不需要考虑1和6这种重复性的工作
提前实现好对request处理的函数实现成函数,将处理出的参数传入到一个函数指针内,再把该函数指针的返回值处理成response

而对于面对对象的语言,这个处理过程自然要将其封装成对象
定义一个输入为request输出为response的对象.
这时有一些问题:在步骤2里,请求可能是GET,也可能是POST,但是又没有必要重新定义步骤1,则可以再定义一个类继承自之前的类.

而为了让实现更为灵活,比如http有了新的规范等,这时没必要定义具体的实现,只需要规定好函数名、输入输出值,这样既保证了灵活性、又保证了规范

这种只有定义而不需要实现(也可以在抽象类中实现方法)的类称为抽象类,只搭好了一个框架,而没有实现,因此不能直接实例化,必须继承并且实现后才可以实例化

在C++中,类的继承是可以多继承的.既一个类可以有多个父类.
但是这会导致一个问题: 如果一个类的两个父类有一个相同的方法,那么这个类在调用父类的同名方法会导致存在歧义,也即二义性(multiple inheritance)

而Java为了避免这个问题,只允许单继承
但如果一个对象逻辑上是两个抽象类的子类,单继承则无法很好地实现该需求
因此又有了接口(interface)的概念,接口要定义则是函数的方法而非函数的框架,并且接口是可以多继承的
实际使用中,抽象类和接口除去只能单继承外,差别越来越小,但是应该遵循这种逻辑来定义

关于为什么接口多继承不会二义性
接口只有声明没有定义,因此使用的时候需要定义方法的实现,因此虽然可能有两个重名的方法,但是实现只有一个,因此不存在二义性

Java中有重写(Override)和重载(Overload)
其中重写指改变函数的执行过程,输入参数和输出返回值的格式都保持一样
重载则是只保证函数名不变,输入参数和输出返回值类型可以改变

  • 重写(Override): 在保证输入输出类型相同的前提下,改变处理过程
  • 重载(Overload): 同一个函数有不同的输入输出类型及执行过程
  • 多态: 同样的操作,但是结果不同(即重写导致的结果).同时必须是父类类型变量指向一个子类类型的对象(展现出来就是同一个类型变量执行结果不一样)
  • 抽象: 只有定义,没有实现.类似于TypeScript中的类型文件,更大的作用在于占位、补全、方便分析
  • 接口: 抽象方法的集合,除抽象类外,需要包含所有方法.同时接口不能被实例化,却可以实现.因为它本身是一个类型

Go中的面对对象

这三者是面对对象的内容

结构体与C语言的结构体类似,格式为:

type person struct {
    name string
    age  int
}

同时与Java等以面对对象为主的语言不同,Go中方法不放在类(结构体)内

而是单独写成一个函数出来

func (p person) info() string {
    return fmt.Sprintf("My name is %s, and I am %d. Address is %p. Type is %s", p.name, p.age, &p, reflect.TypeOf(p))
}

而接口的写法为

type geometry interface {
    area() float64
    perim() float64
}

单继承的写法

package main

import "fmt"

type Base struct {
    id int
}
type Child struct {
    Base
    name string
}

func (this Base) pid() {
    fmt.Println(this.id)
}

func (this Child) p() {
    fmt.Println(this.id)
    fmt.Println(this.name)
}

func main() {
    obj := Child{Base{2}, "b"}
    obj.p()
    obj.pid()
}

输出为

2
b
2

Go中的继承是直接在struct中写入另一个struct(可以多继承)
这里Go有接口、有多继承,但是没有多态的概念: 不能var obj Base = Child{Base{2}, "b"}
可以通过指针来实现多态

协程

协程关键字

Go中的协程使用go关键字
当要以协程执行一个函数、匿名函数,只需要在它前面加上go即可

协程执行的顺序是交替的,因此以下代码每次执行结果可能都不一样
这里一定要有最后的fmt.Scanln(),不然如果主进程执行完而协程还未输出完成,则会导致无输出值

如果需要直接释放CPU使劲片,可以使用runtime.Gosched()
借助runtimesync/atomic两个包,实现对多线程的数据的操作

package main

import "fmt"

func f(from string) {
    for i := 0; i < 5; i++ {
        fmt.Println(from, ":", i)
    }
}

func main() {
    go f("A")
    go f("B")
    go f("C")

    var input string
    fmt.Scanln(&input)
    fmt.Println("done")
}

通道

而go中协程之间的通信使用通道
将信息放入一个特殊的变量中,实现获取协程处理结果的目的
(因为协程的运行时许与主进程无关,因此返回值没有意义,处理完返回结果可能主程序已经执行到别的地方了)

package main

import (
    "fmt"
    "reflect"
)

func msg(message string, messages chan string) {
    fmt.Println("===", message, "===")
    messages <- message
}

func main() {
    messages := make(chan string, 2)
    fmt.Println(reflect.TypeOf(messages))
    go msg("A", messages)
    go msg("B", messages)
    go msg("C", messages)
    messages <- "D"
    messages <- "E"
    fmt.Println(<-messages)
    fmt.Println(<-messages)
    fmt.Println(<-messages)

    fmt.Scanln()
}

通道的缓存以及超时

按上面的理解,这里A,B,C执行顺序不定,同时当第一个协程执行完,主进程直接执行到最后(因此没有fmt.Scanln()会导致值会输出第一个协程的内容.同时<-会等到获取到写成中返回的通道值)

同时,默认通道为无缓存,可以在make参数中更改
这里缓存的意义是: 在取出数据前能存放多少个
因为通常而言,主进程使用<-messages来监听通道的变更
而协程中使用messages<-来向通道发送数据
这时没有一个新的数据被送入通道,立即就就被主进程取出,因此没必要缓存

但是当类似例子中,直接在主进程中相同到发送数据,可能在取出前有多个发送的操作,因此就需要有一个缓冲区来缓冲这些数据
以及这里要小心造成死锁: 一直等待通道返回数据,但是却没有数据被送入通道

可以使用for range来遍历(取出所有)通道缓存内的数据

除去传递数据外,也可以用通道来标记协程执行完成

在协程函数内,可以使用chan<- string<-chan string来表示这是一个只负责输入、输出的通道

当有多个协程时,可以使用多次<-xxx来实现等待所有协程结束以及返回值,也可以使用select来实现这一目的
(其中,使用time.after(),实现了超时处理)

select {
    case res := <-c2:
        fmt.Println(res)
    case <-time.After(time.Second * 3):
        fmt.Println("timeout 2")
}

非阻塞通道

默认的通道是阻塞的,使用selectdefault可以实现非阻塞通道
在执行到select时,通道没有被送入数据,执行default部分

关闭通道

<-xxx的返回值有两个:

  1. 通道内的值
  2. 通道是否被关闭

借助这个操作,可以在发送方发送所有数据后使用close(xxx)来关闭通道,同时接收方根据通道是否被关闭来判断是否需要继续等待

被关闭的通道不能继续发送,但是还能接受缓存中的值

定时器

使用timer := time.NewTimer(time.Second * 2)的操作来定义一个计时器,使用<-timer阻塞当前协程,而使用timer.Stop()可以关闭定时器

类似JavaScript中的setTimeout()

打点器

打点器时每隔规定时间后执行一次,类似JavaScript中的setInterval()
使用timer.NewTicker(time.Millisecond * 500)来定义打点器
通常执行需要在协程里,使用上面提到的概念来实现该操作

借助打点器,可以实现速率限制:通道传输数据的时间间隔

协程任务分配

package main

import (
    "fmt"
    "time"
)

func worker(id int, jobs <-chan int, results chan<- int) {
    // 处理过程
    for j := range jobs {
        fmt.Println("worker", id, "processing job", j)
        time.Sleep(time.Second) // 耗时的处理操作
        results <- j * 2
        fmt.Println("worker", id, "finish job", j)
    }
}

func main() {
    jobs := make(chan int, 100)
    results := make(chan int, 100)

    // 定义用于处理的协程个数
    for w := 1; w <= 3; w++ {
        go worker(w, jobs, results)
    }

    // 分配任务
    for j := 1; j <= 9; j++ {
        jobs <- j
    }
    close(jobs)

    // 等待所有任务结束
    for a := 1; a <= 9; a++ {
        fmt.Println("result", <-results)
    }
}

互斥锁

计算机系统中经典的互斥锁使用sync.Mutex{}来生成
使用mutex.Lock()来执行P操作,mutex.Unlock()来执行V操作
从而实现防止多个协程、线程、进程对同一个变量进行操作导致冲突

panic

当程序出现无法处理的错误时,可以使用panic()来抛出错误并停止执行(返回非0状态码)

defer

使用defer关键字,可以让该语句在main()结束前执行
通常用于清理操作

格式化输出

输出到控制台使用fmt.Printf()而只需要获得格式化后的字符串使用fmt.sprintf()

格式化的替换字符如下:

字符 含义
%v 输出结构体的内容
%+v 输出结构体的字段名、值
%#v 输出结构体的类型、字段名、值
%T 输出变量类型
%t 输出布尔值
%d 输出整数
%b 输出二进制整数
%c 输出整数对应的ascii字符
%x 输出十六进制编码
%f 输出浮点数
%e 输出科学计数下的浮点数
%E 输出科学计数下的浮点数
%s 输出字符串
%p 输出类型
%-a.bf 输出左对齐的宽度为a且保留b位小数的浮点数

常用包

sort

Go中的sort排序是坐地排序,不会返回值,结果存放在原来的变量中

函数 备注
Float64s(a []float64) 排序float类型
Float64sAreSorted(a []float64) bool 返回float是否排序完毕
Ints(a []int) 排序int类型
IntsAreSorted(a []int) bool 返回int类型是否排序完毕
Search(n int, f func(int) bool) int 使用二分查找搜索某个值(不存在返回n)
SearchFloat64s(a []float64, x float64) int 使用二分查找在float中搜索某个值
SearchInts(a []int, x int) int 使用二分查找在int中搜索某个值
SearchStrings(a []string, x string) int 使用二分查找在string中搜索某个值
Slice(slice interface{}, less func(i, j int) bool) 自定义类型排序
SliceIsSorted(slice interface{}, less func(i, j int) bool) bool 自定义类型是否排序完毕
SliceStable(slice interface{}, less func(i, j int) bool) 自定义类型稳定排序
Strings(a []string) 排序strings类型
StringsAreSorted(a []string) bool 返回string是否排序完毕

strings

Go中的字符串处理包

函数 备注
Compare(a, b string) int 比较两个字符串大小(字典序)
Contains(s, substr string) bool 判断一个子串是否在字符串中
ContainsAny(s, chars string) bool 子串中是否有字符在字符串中存在
ContainsRune(s string, r rune) bool 判断字符是否在字符串中存在(字符以ASCII传入)
Count(s, substr string) int 计算子串在字符串中出现的次数
EqualFold(s, t string) bool 比较小写的utf-8编码是否相等
Fields(s string) []string 按照空格将字符串分割成列表
FieldsFunc(s string, f func(rune) bool) []string 按照函数定义的要求将字符换分割成列表
HasPrefix(s, prefix string) bool 字符串前缀是否为子串
HasSuffix(s, suffix string) bool 字符串后缀是否为子串
Index(s, substr string) int 查找子串在字符串中的位置(不存在返回-1)
IndexAny(s, chars string) int 查找子串中任一字符在字符串中第一次出现的位置(不存在返回-1)
IndexByte(s string, c byte) int 查找字符在字符串中第一次出现的位置(第一次出现返回-1)
IndexFunc(s string, f func(rune) bool) int 找到第一个能让函数返回true的位置
IndexRune(s string, r rune) int 找到第一个符合要求的字符的位置
Join(a []string, sep string) string 将字符串列表合成字符串
LastIndex(s, substr string) int 找到最后一个符合要求的子串位置
LastIndexAny(s, chars string) int 找到最后一个在存在于子串中的字符在字符串中的位置
LastIndexByte(s string, c byte) int 找到最后一个符合要求的字符在字符串中的位置
LastIndexFunc(s string, f func(rune) bool) int 找到最后一个能让函数返回true的位置
Map(mapping func(rune) rune, s string) string 按照函数转换每个字符串的字符
Repeat(s string, count int) string 将字符串重复count遍
Replace(s, old, new string, n int) string 将字符串中的指定子串替换成别的子串
Split(s, sep string) []string 按照特定的子串将字符串分割成字符串列表
SplitAfter(s, sep string) []string 在指定子串后将字符串分割成字符串列表(会保留用于分隔的子串)
SplitAfterN(s, sep string, n int) []string 在指定子串后将字符串分割成字符串列表(会保留用于分隔的子串,且列表长度不大于n)
SplitN(s, sep string, n int) []string 使用指定子串将字符串分割成字符串列表(列表长度不大于n)
Title(s string) string 所有单词首字母转换成标题格式(大写)
ToLower(s string) string 将字符串转换为小写
ToLowerSpecial(c unicode.SpecialCase, s string) string 将特殊编码的字符串转换为小写
ToTitle(s string) string 所有单词转换成标题格式(大写)
ToTitleSpecial(c unicode.SpecialCase, s string) string 将特殊编码的字符串转换为标题格式
ToUpper(s string) string 字符串转换为大写
ToUpperSpecial(c unicode.SpecialCase, s string) string 将特殊编码的字符串转换为大写
Trim(s string, cutset string) string 将字符串两端在删除集合的字符删去
TrimFunc(s string, f func(rune) bool) string 将字符串两端能将函数返回true的字符删去
TrimLeft(s string, cutset string) string 将字符串开头在删除集合内字符删去
TrimLeftFunc(s string, f func(rune) bool) string 将字符串开头能将函数返回true的字符删去
TrimPrefix(s, prefix string) string 删除字符串前面的前缀字符串(如果有这个前缀)
TrimRight(s string, cutset string) string 将字符串结尾在删除集合内的字符删去
TrimRightFunc(s string, f func(rune) bool) string 将字符串结尾能将函数返回true的字符删去
TrimSpace(s string) string 删除字符串两端的空白字符
TrimSuffix(s, suffix string) string 删除字符串后面的后缀字符串(如果有这个后缀)

参考资料