语言结构
包声明:第一行代码必须使用package xxx
指明该文件属于哪个包。
引入包:后面导入包,用双引号引起。
导出名:
在 Go 中,如果一个名字以大写字母开头,那么它就是已导出的。例如,
Pizza
就是个已导出名,Pi
也同样,它导出自math
包。
执行go程序
go run hello.go
还可以使用go build hello.go
编译成二进制文件。
左花括号{不能单独放在一行。
数据类型
-
布尔型
-
数字类型
int、float32、float64
-
字符串类型
字符串是不可变字节序列,其本身是一个复合结构。
字符串默认值不是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
-
-
派生类型
-
指针类型
-
数组类型
数组必须类型和长度一致才算做是同一种类型。
数组是值类型的,赋值和传参都是复制整个数组。
// 初始化 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两种方式支持协程的并发。
-
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!") }
-
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.go
, main.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
一些思考问题
-
用内置delete函数对map元素进行删除后,内存会真的释放吗?
不会。唯一的方法是重新创建一个map对旧map进行复制。https://github.com/golang/go/issues/20135