【go】golang

语言结构

包声明:第一行代码必须使用package xxx指明该文件属于哪个包。

引入包:后面导入包,用双引号引起。

导出名:

在 Go 中,如果一个名字以大写字母开头,那么它就是已导出的。例如,Pizza 就是个已导出名,Pi 也同样,它导出自 math 包。

执行go程序

go run hello.go

还可以使用go build hello.go编译成二进制文件。

左花括号{不能单独放在一行。

数据类型

  1. 布尔型

  2. 数字类型

    int、float32、float64

  3. 字符串类型

    字符串是不可变字节序列,其本身是一个复合结构。

    字符串默认值不是nil而是"":

    var str string
    fmt.Println(str == "") // true
    

    []byte和[]rune类型的区别:

    str := "strings"
    fmt.Println([]rune(str)) // [103 111 35821 35328]
    fmt.Println([]byte(str)) // [103 111 232 175 173 232 168 128]
    // 字符串是utf-8的一个序列,当字符串为ASCⅡ码表上的字符则占用1个字节,其他字符根据需要占用2-4个字节。
    
    // 可以看出rune的长度是4,byte长度是8,这是因为rune是使用utf-8编码,当字符可以使用ASCⅡ表示时,其占一个字节,其他字符根据需要占用2-4个字节。
    
    // byte就是具体的字节个数,使用内置函数len()得到的是字符串的byte长度。
    

    字符串的内容(纯字节)可以通过索引来获取,对字符串中的每个utf-8字符访问可以通过rune类型:

    for i, ru := range str{
        // i为索引
        // ru为uft-8字符
    }
    

    除了可以使用双引号外,还可以使用反引号``:

    str := `line 1
    line 2
    line 3
    \r\n
    `
    fmt.Println(str)
    /*
    line 1
    line 2
    line 3
    \r\n
    */
    // 可以看出它不对转移字符进行解析,即按照原文本输出。
    

    常见的字符串操作:

    • 字符串比较

      // Compare 函数,用于比较两个字符串的大小,如果两个字符串相等,返回为 0。如果 a 小于 b ,返回 -1 ,反之返回 1 。不推荐使用这个函数,直接使用 == != > < >= <= 等一系列运算符更加直观。
      func Compare(a, b string) int 
      //   EqualFold 函数,计算 s 与 t 忽略字母大小写后是否相等。
      func EqualFold(s, t string) bool
      
      a := "gopher"
      b := "hello world"
      fmt.Println(strings.Compare(a, b)) // -1
      fmt.Println(strings.Compare(a, a)) // 0
      fmt.Println(strings.Compare(b, a)) // 1
      fmt.Println(strings.EqualFold("GO", "go")) // true
      fmt.Println(strings.EqualFold("壹", "一")) // false
      
    • 子串

      // 子串 substr 在 s 中,返回 true
      func Contains(s, substr string) bool
      // chars 中任何一个 Unicode 代码点在 s 中,返回 true
      func ContainsAny(s, chars string) bool
      // Unicode 代码点 r 在 s 中,返回 true
      func ContainsRune(s string, r rune) bool
      
      fmt.Println(strings.ContainsAny("team", "i")) // false
      fmt.Println(strings.ContainsAny("failure", "u & i")) // true
      fmt.Println(strings.ContainsAny("in failure", "s g")) // true
      fmt.Println(strings.ContainsAny("foo", "")) // false
      fmt.Println(strings.ContainsAny("", "")) // false
      
      // 子串出现几次
      func Count(s, sep string) int
      fmt.Println(strings.Count("cheese", "e")) // 3
      fmt.Println(len("谷歌中国")) // 12
      fmt.Println(strings.Count("谷歌中国", "")) // 5
      
    • 字符串分割

      func Split(s, sep string) []string
      
      fmt.Printf("%q\n", strings.Split("foo,bar,baz", ","))
      
      // 如果按照空格分开,这种方式不会忽略连续的空格,这种情况使用strings.Fields(strings)
      strings.Fields(str)
      
    • 前后缀

      // s 中是否以 prefix 开始
      func HasPrefix(s, prefix string) bool {
        return len(s) >= len(prefix) && s[0:len(prefix)] == prefix
      }
      // s 中是否以 suffix 结尾
      func HasSuffix(s, suffix string) bool {
        return len(s) >= len(suffix) && s[len(s)-len(suffix):] == suffix
      }
      
      fmt.Println(strings.HasPrefix("Gopher", "")) // true
      fmt.Println(strings.HasSuffix("Amigo", "go")) // true
      
    • 字符或子串在字符串中出现的位置

      // 在 s 中查找 sep 的第一次出现,返回第一次出现的索引
      func Index(s, sep string) int
      // 在 s 中查找字节 c 的第一次出现,返回第一次出现的索引
      func IndexByte(s string, c byte) int
      // chars 中任何一个 Unicode 代码点在 s 中首次出现的位置
      func IndexAny(s, chars string) int
      // 查找字符 c 在 s 中第一次出现的位置,其中 c 满足 f(c) 返回 true
      func IndexFunc(s string, f func(rune) bool) int
      // Unicode 代码点 r 在 s 中第一次出现的位置
      func IndexRune(s string, r rune) int
      
      han := func(c rune) bool {
          return unicode.Is(unicode.Han, c) // 汉字
      }
      fmt.Println(strings.IndexFunc("Hello, world", han)) // -1
      fmt.Println(strings.IndexFunc("Hello, 世界", han)) // 7
      
    • 将字符串数组连接起来

      // func Join(a []string, sep string) string
      
      fmt.Println(strings.Join([]string{"name=xxx", "age=xx"}, "&")) // name=xxx&age=xx
      
    • 字符串重复几次

      func Repeat(s string, count int) string
      fmt.Println("ba" + strings.Repeat("na", 2)) // banana
      
    • 字符替换

      func Map(mapping func(rune) rune, s string) string
      // Map 函数,将 s 的每一个字符按照 mapping 的规则做映射替换,如果 mapping 返回值 <0 ,则舍弃该字符。该方法只能对每一个字符做处理,但处理方式很灵活,可以方便的过滤,筛选汉字等。
      
      mapping := func(r rune) rune {
          switch {
          case r >= 'A' && r <= 'Z': // 大写字母转小写
              return r + 32
          case r >= 'a' && r <= 'z': // 小写字母不处理
              return r
          case unicode.Is(unicode.Han, r): // 汉字换行
              return '\n'
          }
          return -1 // 过滤所有非字母、汉字的字符
      }
      fmt.Println(strings.Map(mapping, "Hello你#¥%……\n('World\n,好Hello^(&(*界gopher..."))
      /*
      hello
      world
      hello
      gopher
      */
      
    • 字符串子串替换

      进行字符串替换时,考虑到性能问题,能不用正则尽量别用,应该用这里的函数。

      // 用 new 替换 s 中的 old,一共替换 n 个。
      // 如果 n < 0,则不限制替换次数,即全部替换
      func Replace(s, old, new string, n int) string
      // 该函数内部直接调用了函数 Replace(s, old, new , -1)
      func ReplaceAll(s, old, new string) string
      
      fmt.Println(strings.Replace("oink oink oink", "k", "ky", 2)) // oinky oinky oink
      fmt.Println(strings.Replace("oink oink oink", "oink", "moo", -1)) // moo moo moo
      fmt.Println(strings.ReplaceAll("oink oink oink", "oink", "moo")) // moo moo moo
      
  4. 派生类型

    • 指针类型

    • 数组类型

      数组必须类型和长度一致才算做是同一种类型。

      数组是值类型的,赋值和传参都是复制整个数组。

      // 初始化
      var a [3]int // {0,0,0}自动初始化为0
      b := [4]int{1, 2} // {1,2,0,0}未初始化的位置初始化为0
      c := [4]int{1, 3:10} //{1,0,0,10}指定索引位置初始化
      d := [...]int{1,2,3} // 编译器按照初始化数量确定数组长度
      // 对于符合类型初始化,可省略类型标签
      type user struct {
          name string,
          age byte
      }
      e := [...]user {
          {"Tom", 20},
          {"Sam", 21},
      }
      // 二维数组支持第一维使用...
      f := [...][2]int{
          {1,2},
          {3,4},
      }
      
        // 动态二维数组
      var a [][]int
      for i := 0; i < 10; i++ {
          var tmp []int
          for j:= 0; j < 10; j++ {
              tmp = append(tmp, j)
          }
          a = append(a, tmp)
      }
           
      row, column := 3, 4
      var answer [][]int
      for i := 0; i < row; i++ {
          inline := make([]int, column)
          answer = append(answer, inline)
      }
      // 用切片创建
      answer1 := make([][]int, row)
      for i := range answer1 {
          answer1[i] = make([]int, column)
      }
      

  • 结构化类型

  • Channel类型

  • 函数类型

  • 切片类型

  • 接口类型

  • Map类型

数据类型转化:float64(num)

常量也是前面加const,且不能用:=定义

数据定义

函数以外的语句都必须以关键字开始,例如var、func等,因此:=只能在函数内使用。

没有初始化的变量会被赋予零值:

  • 数值类型为0
  • 布尔类型为false
  • 字符串为""

结构体

type struct_name struct {
    v1 string
    v2 string
    v3 int
}

v := struct_name {v1, v2, ..., vn}
v := struct_name {key1: v1, key2:v2, ..., keyn:vn}
v.v1 = "..."

// 指针
sp := &v
sp.v1 = "..."

结构体排序

// 排序用到了sort包
import "sort"

// 常见的三种排序函数 sort.Ints sort.Floats sort.Strings是对于[]int []float []string

// 使用自定义排序函数 sort.Slice和sort.SliceStable,后者是稳定排序,包含两个参数,第一个是待排序的数组,第二个是排序函数,示例:

family := []struct {
    Name string
    Age  int
}{
    {"Alice", 23},
    {"David", 2},
    {"Eve", 2},
    {"Bob", 25},
}
sort.SliceStable(family, func(i, j int) bool {
    return family[i].Age < family[j].Age
}) // 按照年龄排序


sort.SliceStable(family, func(i, j int) bool {
    if family[i].Age != family[j].Age {
        return family[i].Age < family[j].Age
    }
    return strings.Compare(family[i].Name, family[j].Name) == 1
})// 首先按照年龄排序,然后按照名字排序

语言切片

切片本身并非动态数组或数组指针。它内部通过指针引用底层数组,设定相关属性将数据读写限定在指定区域内。

x := [...]int{0,1,2,3,4,5,6,7,8,9}
// 切片操作
/*	expression	slice					len	cap
	x[:]		[0,1,2,3,4,5,6,7,8,9]	10	10
	x[2:5]		[2,3,4]					3	8
	x[2:5:7]	[2,3,4]					3	5
	x[4:]		[4,5,6,7,8,9]			6	6
	x[:4]		[0,1,2,3]				4	10
	x[:4:6]		[0,1,2,3]				4	6
属性cap表示切片所引用数组片段的真实长度,len用于限定可读写的写元素数量。
*/

// 创建切片对象,引用类型,须使用make函数或显式初始化语句
s1 := make([]int, 3, 5) // 指定len、cap,底层数组初始化为0
s2 := make([]int, 3) // 不指定cap,和len相等

// 可以基于一个切片创建一个新的切片,二者都指向同一个底层数组,称为reslice
d := [...]int{0,1,2,3,4,5}
d1 := d[2:5]
d2 := d1[0:2]
/* make参数

make(type, len, cap)
type: 切片类型
len: 切片长度
cap: 切片容量

*/
// 用reslice实现栈
stack := make([]int, 0, 5)

push := func(x int) error {
    n := len(stack)
    if n == cap(stack) {
        return errors.New("stack is full")
    }
    stack = stack[:n+1]
    stack[n] = x
    
    return nil
}

pop := func() (int, error) {
    n := len(stack)
    if n == 0 {
        return 0, errors.New("stack is empty")
    }
    
    x := stack[n-1]
    stack = stack[:n-1]
    
    return x, nil
}

for i := 0; i < 7; i++ {
    fmt.Printf("push %d: %v, %v\n", i, push(i), stack)
}

for i := 0; i < 7; i++ {
    x, err := pop()
    fmt.Printf("pop %d, %v, %v\n", x, err, stack)
}

/*
push 0: <nil>, [0]
push 1: <nil>, [0 1]
push 2: <nil>, [0 1 2]
push 3: <nil>, [0 1 2 3]
push 4: <nil>, [0 1 2 3 4]
push 5: stack is full, [0 1 2 3 4]
push 6: stack is full, [0 1 2 3 4]
pop 4, <nil>, [0 1 2 3]
pop 3, <nil>, [0 1 2]
pop 2, <nil>, [0 1]
pop 1, <nil>, [0]
pop 0, <nil>, []
pop 0, stack is empty, []
pop 0, stack is empty, []
*/
// append 向切片尾部添加数据,如果超出cap则重新分配数组
// 重新分配的数据的cap是原来数组cap的二倍,但并非都是2倍
s := make([]int, 0, 100)
s1 := s[:2:4]
s2 := append(s1, 1,2,3,4,5)

// copy

函数

go函数的返回值可以被命名,会被视作定义在函数顶部的变量。

没有参数的return会返回已经命名的返回值。

接口

Go 语言提供了另外一种数据类型即接口,它把所有的具有共性的方法定义在一起,任何其他类型只要实现了这些方法就是实现了这个接口。

/* 定义接口 */
type interface_name interface {
   method_name1 [return_type]
   method_name2 [return_type]
   method_name3 [return_type]
   ...
   method_namen [return_type]
}

/* 定义结构体 */
type struct_name struct {
   /* variables */
}

/* 实现接口方法 */
func (struct_name_variable struct_name) method_name1() [return_type] {
   /* 方法实现 */
}
...
func (struct_name_variable struct_name) method_namen() [return_type] {
   /* 方法实现*/
}

错误处理

Go 语言通过内置的错误接口提供了非常简单的错误处理机制。

error类型是一个接口类型,这是它的定义:

type error interface {
    Error() string
}

我们可以在编码中通过实现 error 接口类型来生成错误信息。

函数通常在最后的返回值中返回错误信息。使用errors.New 可返回一个错误信息:

func Sqrt(f float64) (float64, error) {
    if f < 0 {
        return 0, errors.New("math: square root of negative number")
    }
    // 实现
}

在下面的例子中,我们在调用Sqrt的时候传递的一个负数,然后就得到了non-nil的error对象,将此对象与nil比较,结果为true,所以fmt.Println(fmt包在处理error时会调用Error方法)被调用,以输出错误,请看下面调用的示例代码:

result, err:= Sqrt(-1)

if err != nil {
   fmt.Println(err)
}

并发

我们只需要通过 go 关键字来开启 goroutine 即可,同时go语言提供了sync和channel两种方式支持协程的并发。

  1. sync

    例如我们希望并发下载 N 个资源,多个并发协程之间不需要通信,那么就可以使用 sync.WaitGroup,等待所有并发协程执行结束。

    import (
    	"fmt"
    	"sync"
    	"time"
    )
    
    var wg sync.WaitGroup
    
    func download(url string) {
    	fmt.Println("start to download", url)
    	time.Sleep(time.Second) // 模拟耗时操作
    	wg.Done() // 减去一个计数
    }
    
    func main() {
    	for i := 0; i < 3; i++ {
    		wg.Add(1) // 为wg添加一个计数
    		go download("a.com/" + string(i+'0'))
    	}
    	wg.Wait() // 等待所有携程执行结束
    	fmt.Println("Done!")
    }
    
  2. channel

    使用 channel 信道,可以在协程之间传递消息。阻塞等待并发协程返回消息。

    var ch = make(chan string, 10) // 创建大小为 10 的缓冲信道
    
    func download(url string) {
    	fmt.Println("start to download", url)
    	time.Sleep(time.Second)
    	ch <- url // 将 url 发送给信道
    }
    
    func main() {
    	for i := 0; i < 3; i++ {
    		go download("a.com/" + string(i+'0'))
    	}
    	for i := 0; i < 3; i++ {
    		msg := <-ch // 等待信道返回消息。
    		fmt.Println("finish", msg)
    	}
    	fmt.Println("Done!")
    }
    

    channel是用来传递数据的一个数据结构。通道可用于两个 goroutine 之间通过传递一个指定类型的值来同步运行和通讯。操作符 <- 用于指定通道的方向,发送或接收。如果未指定方向,则为双向通道。

单元测试

假设我们希望测试 package main 下 calc.go 中的函数,要只需要新建 calc_test.go 文件,在calc_test.go中新建测试用例即可。

// calc.go
package main

func add(num1 int, num2 int) int {
	return num1 + num2
}
// calc_test.go
package main

import "testing"

func TestAdd(t *testing.T) {
	if ans := add(1, 2); ans != 3 {
		t.Error("add(1, 2) should be equal to 3")
	}
}

运行 go test,将自动运行当前 package 下的所有测试用例,如果需要查看详细的信息,可以添加-v参数。

$ go test -v
=== RUN   TestAdd
--- PASS: TestAdd (0.00s)
PASS
ok      example 0.040s

包和模块

Package

一般来说,一个文件夹可以作为 package,同一个 package 内部变量、类型、方法等定义可以相互看到。

比如我们新建一个文件 calc.gomain.go 平级,分别定义 add 和 main 方法。

// calc.go
package main

func add(num1 int, num2 int) int {
	return num1 + num2
}
// main.go
package main

import "fmt"

func main() {
	fmt.Println(add(3, 5)) // 8
}

运行 go run main.go,会报错,add 未定义:

./main.go:6:14: undefined: add

因为 go run main.go 仅编译 main.go 一个文件,所以命令需要换成

$ go run main.go calc.go
8

$ go run .
8

Go 语言也有 Public 和 Private 的概念,粒度是包。如果类型/接口/方法/函数/字段的首字母大写,则是 Public 的,对其他 package 可见,如果首字母小写,则是 Private 的,对其他 package 不可见。

Modules

Go Modules 是 Go 1.11 版本之后引入的,Go 1.11 之前使用 $GOPATH 机制。Go Modules 可以算作是较为完善的包管理工具。同时支持代理,国内也能享受高速的第三方包镜像服务。接下来简单介绍 go mod 的使用。Go Modules 在 1.13 版本仍是可选使用的,环境变量 GO111MODULE 的值默认为 AUTO,强制使用 Go Modules 进行依赖管理,可以将 GO111MODULE 设置为 ON。

在一个空文件夹下,初始化一个 Module

$ go mod init example
go: creating new go.mod: module example

此时,在当前文件夹下生成了go.mod,这个文件记录当前模块的模块名以及所有依赖包的版本。

接着,我们在当前目录下新建文件 main.go,添加如下代码:

package main

import (
	"fmt"

	"rsc.io/quote"
)

func main() {
	fmt.Println(quote.Hello())  // Ahoy, world!
}

运行 go run .,将会自动触发第三方包 rsc.io/quote的下载,具体的版本信息也记录在了go.mod中:

module example

go 1.13

require rsc.io/quote v3.1.0+incompatible

我们在当前目录,添加一个子 package calc,代码目录如下:

demo/
   |--calc/
      |--calc.go
   |--main.go

calc.go 中写入

package calc

func Add(num1 int, num2 int) int {
	return num1 + num2
}

在 package main 中如何使用 package cal 中的 Add 函数呢?import 模块名/子目录名 即可,修改后的 main 函数如下:

package main

import (
	"fmt"
	"example/calc"

	"rsc.io/quote"
)

func main() {
	fmt.Println(quote.Hello())
	fmt.Println(calc.Add(10, 3))
}

$ go run .
Ahoy, world!
13

一些思考问题

  1. 用内置delete函数对map元素进行删除后,内存会真的释放吗?

    不会。唯一的方法是重新创建一个map对旧map进行复制。https://github.com/golang/go/issues/20135