Go语言圣经 - Go语言圣经 (gopl-zh.github.io)
指令/环境/目录结构
环境配置
| 名称 | 含义 | 值 |
|---|---|---|
| GOPATH | go项目目录 | /work/project/goproject |
| GOROOT | go安装目录 | /opt/go |
| GOOS | 指定编译后的文件运行的os | linux windows |
指令
go build xxx.go # 编译并生成可执行文件
go run xxx.go # 编译运行某个程序
目录
目录结构如下:
bin
pkg
src 表示源码
src/project1 代表项目名称 src/project1/main 代表包名
包内的 main.go中必须指定 package为main
命名规范
- 文件名: 下划线+小写
- 结构体/函数名:大驼峰。
- 对象/变量名: 小驼峰。
- 成员变量/方法: 大驼峰或小驼峰。
- 接口名: 接口名应该是描述性的名词,并以 “-er” 结尾。例如,Reader、Writer、Formatter、CloseNotifier 等。
- 常量名:大写+下划线。
- 包名=文件名
语法
基本数据
声明
var s,o string
var a="ok"
var x int = 5
/*-------------------------------*
使用:时相当于自带 var 和 type
:=仅能用于局部变量的声明,全局变量不能用:=
*--------------------------------*/
b:=2
// 综上所述,声明变量有如下几种:
var a int //a=0
var a = 2
var a int = 2
a := 2
// 以下几个声明后默认为nil
var a chan int
var a func(string) int
var a error // error 是接口
var a,b *int
//var一次声明几个变量
var (
a int
b string
c []float32
d func() bool
e struct {
x int
}
)
//匿名变量
func GetData() (int, int) {
return 100, 200
}
func main(){
a, _ := GetData()
_, b := GetData()
fmt.Println(a, b)
}
指针
// 指针的基础知识跳过
p := new(int) // p, *int 类型, 指向匿名的 int 变量
fmt.Println(*p) // "0"
*p = 2 // 设置 int 匿名变量的值为 2
fmt.Println(*p) // "2"
const
const变量必须是编译器就能确定值的
//type可以不写
const identifier [type] = value
const (
a = "abc"
b = len(a)
c = unsafe.Sizeof(a)
)
/*
iota是go的枚举生成器
在第一行引用 iota时,为0
之后每定义一行const变量,iota自增1,并赋值给常量
*/
const (
a = iota
b
c
d = 10*iota
e
) // 0,1,2,30,40
别名
type ttttt=uint8
// 从此以后多了一个类型 ttttt,定义byte就相当于定义uint8
基础变量
bool:布尔类型,只能取 true 或 false。int、int8、int16、int32、int64:整型类型,分别表示不同位数的整数。其中 int 的大小是和平台相关的,通常为 32 位或 64 位。uint、uint8、uint16、uint32、uint64:无符号整型类型,分别表示不同位数的无符号整数。float32、float64:浮点型类型,分别表示单精度浮点数和双精度浮点数。complex64、complex128:复数类型,用于表示实部和虚部均为浮点数的复数。byte:=uint8,常用于表示 ASCII 码字符。rune:=int32,常用于表示 Unicode 字符。string:字符串类型,用于表示文本字符串。
rune和string
在go中,一个字符用rune标识,rune==int32始终为4字节。 因为unicode/utf8为了标识世界上的全部符号,最大就是4字节。
go字符串内部不是rune而是一系列字节。
如果想用下标去定位字符,可以用[]rune
rune
//通过 `去定义多行字符串
s := "プログラム"
fmt.Printf("% x\n", s) // "e3 83 97 e3 83 ad e3 82 b0 e3 83 a9 e3 83 a0"
r := []rune(s)
for i,v :=range r{
fmt.Printf("%c%c",v,r[i])
}
string
// 声明
s :="hello, world"
// 截取和连接
s1:=s[0:2]+"a"
// 字符串面值,通过` `定义
const GoUsage = `Go is a tool for managing Go source code.
Usage:
go command [arguments]
...`
在执行s+= ...时会对原字符串进行拷贝
在执行s[a:b]时不会进行拷贝,字符串会尽量共享内存
遍历字符串时:
for i, r := range "Hello, 世界" {
fmt.Printf("%d\t%q\t%d\n", i, r, r)
}
0 'H' 72
1 'e' 101
2 'l' 108
3 'l' 108
4 'o' 111
5 ',' 44
6 ' ' 32
7 '世' 19990
10 '界' 30028
可以看到i并不是+1的,而是根据字符在String中的下标更新的,因为在执行 range时自动完成了utf8的转码
for i := 0; i < len(ste); i++ {
fmt.Printf("ascii: %c %d\n", str[i], str[i])
}
此时程序因为解码失败会报错
变量转化
todo
// 数字->字符串
x:= 123
y := fmt.Sprintf("%d", x)
fmt.Println(y, strconv.Itoa(x)) // "123 123"
复合数据
数组
var a [3]int
for i, v := range a {
fmt.Printf("%d %d\n", i, v)
}
q := [...]int{1, 2, 3} // 让编译器自动确定数组大小
q:=[100]string{0:"$",1:"%"} // 根据下标定义值
切片
切片与数组在声明上仅有
长度的区别,声明切片时不指定长度
s := []int{0, 1, 2, 3, 4, 5}
s1=s
s1=s[:]
make([]T, len)
make([]T, len, cap)
append
func appendInt(x []int, y int) []int {
var z []int
zlen := len(x) + 1
if zlen <= cap(x) {
// There is room to grow. Extend the slice.
z = x[:zlen]
} else {
// There is insufficient space. Allocate a new array.
// Grow by doubling, for amortized linear complexity.
zcap := zlen
if zcap < 2*len(x) {
zcap = 2 * len(x)
}
z = make([]int, zlen, zcap)
copy(z, x) // a built-in function; see text
}
z[len(x)] = y
return z
}
Map
go语言底层Map采用哈希表结构
map[xxx]int在key不存在时返回0(与cpp同)
ages := make(map[string]int)
ages := map[string]int{
"alice": 31,
"charlie": 34,
}
delete(ages, "alice")
for k, v := range ages {
}
age, ok := ages["bob"]
if !ok { // 没有这个key
}
流程控制
if
if i < 1{
}
// 可以在if前简单的执行一句
if i:=math.Abs(-1); i>=0{
}
for
// 1.无内容for
sum := 0
for {
sum++
if sum > 100 {
break
}
}
// 2.仅判断条件for
var i int
for i <= 10 {
i++
}
// 3.完整 for
for i:=0;i<5;i++{
...
}
//4. range for, val为元素的副本
for key, val := range map {
...
}
for key :=range map{
}
switch
//switch无需break
var a = "hello"
switch a {
case "hello":
fmt.Println(1)
case "world":
fmt.Println(2)
case "a","b":
...
default:
fmt.Println(0)
}
//switch前也可以执行一个简单的语句
switch a:="Hello"; a{
...
}
var r int = 11
switch {
case r > 10 && r < 20:
fmt.Println(r)
}
var s = "hello"
switch {
case s == "hello":
fmt.Println("hello")
fallthrough // 执行完这个case后,继续向下执行
case s != "world":
fmt.Println("world")
}
defer
// defer后的语句会被推迟到函数结束时执行,一般用于文件资源释放等
func test(){
defer fmt.Println("world")
fmt.Println("hello")
}
goto
err := firstCheckError()
if err != nil {
goto onExit
}
err = secondCheckError()
if err != nil {
goto onExit
}
fmt.Println("done")
return
onExit:
fmt.Println(err)
exitProcess()
函数
函数声明
// 只有一个返回值时无需加括号
// 有结构体名时代表为某个类写方法
func [结构体名] 函数名 (参数列表) [返回值|(返回值列表)]{
函数体
}
// 几个相同类型的变量可以只写一个类型
func hypot(x, y float64) float64 {
return math.Sqrt(x*x + y*y)
}
// 函数同样可以是一个变量
var a func(string) int
func fire(x string) {
fmt.Println(x)
return 2
}
func main(){
a=fire
a("abc")
}
作用域和变量逃逸
var global *int
func f() {
var x int
x = 1
global = &x
}
func g() {
y := new(int)
*y = 1
}
在上例中,变量x就逃逸了,编译器可以分析出结果,并自动将x创建在堆内存上
而y没有逃逸,虽然他用new关键字声明,但还是会被回收
匿名函数
// 在声明时就调用的
func(data int) {
fmt.Println("hello", data)
}(100)
// 赋值给某个变量的
var f func(int) = func(data int) {
fmt.Println("hello", data)
}
f(100)
闭包
简单来看,闭包=静态(编译时确定的)大函数A中的动态(运行时确定)小函数B,其中B引用了A中的局部变量。
package main
import (
"fmt"
)
// 提供一个值, 每次调用函数会指定对值进行累加
func Accumulate(value int) func() int {
// 返回一个闭包
return func() int {
value++
return value
}
}
func main() {
accumulator := Accumulate(1)
// 累加1并打印
fmt.Println(accumulator())
fmt.Println(accumulator())
fmt.Printf("%p\n", &accumulator)
// 创建一个累加器, 初始值为1
accumulator2 := Accumulate(10)
// 累加1并打印
fmt.Println(accumulator2())
// 打印累加器的函数地址
fmt.Printf("%p\n", &accumulator2)
} // 2 3 11
闭包=函数体和引用环境,函数体定义了闭包的行为,而引用环境则提供了函数体所需要的外部变量。
如果你把闭包返回出去了,那么该闭包内引用到的局部变量都算
逃逸,会声明在堆内存
可变参数
可变参数就是长度不定的参数,当作数组一样处理即可
func myfunc(args ...int) {
for _, arg := range args {
fmt.Println(arg)
}
}
结构体/对象
type 类型名 struct {
字段1 字段1类型
字段2 字段2类型
…
}
type Point struct {
X int
Y int
}
//声明
p:=new(Point) //返回指针
p:=&Point{} // 与上同
p:=Point{} // 返回结构体对象
var p Point // 与上同
p:=Point{1,2}
p := Point{
X: 1,
Y: 2
}
方法
我们一般用一个特殊的函数来给类添加方法:
package main
import "fmt"
// 车轮
type Person struct {
Age int
}
func (p Person) say() {
fmt.Println(p.Age)
}
func main() {
p := Person{Age: 2}
p.say()
}
// 需要注意的是,每次执行p.say()都会拷贝一遍p,因此我们最好写成这样:
// 这样就是对p本身进行操作
func (p *Person) say() {
fmt.Println(p.Age)
}
//在调用时
p.say()
继承
在结构体中直接写另一个结构体就是继承
随后,内部结构体的所有方法/变量都会被外部结构体具有
package main
import "fmt"
type A struct {
ax, ay int
}
type B struct {
A
bx, by float32
}
func main() {
b := B{A{1, 2}, 3.0, 4.0}
fmt.Println(b.ax, b.ay, b.bx, b.by)
fmt.Println(b.A)
}
构造函数
在go中,没有构造函数的概念,如果要构造函数,请手动调用:
type Person struct {
Name string
Age int
}
func NewPerson(name string, age int) *Person {
return &Person{Name: name, Age: age}
}
func main(){
p1:=NewPerson("Java",30)
}
析构函数
todo
func SetFinalizer(x, f interface{})
创建结构体
type Point struct {
X, Y int
}
type Circle struct {
Point
Radius int
}
type Wheel struct {
Circle
Spokes int
}
p:=Point{1,2}
p:=Point{X:1,Y:2}
w = Wheel{Circle{Point{8, 8}, 5}, 20}
w = Wheel{
Circle: Circle{
Point: Point{X: 8, Y: 8},
Radius: 5,
},
Spokes: 20
}
包
导入包
每个包是由一个全局唯一的字符串所标识的导入路径定位。出现在import语句中的导入路径也是字符串。
import (
"fmt"
"math/rand"
"encoding/json"
"golang.org/x/net/html"
"github.com/go-sql-driver/mysql"
)
包名,文件名
- 包名必须与最后一级目录名相同
- 一个包内的若干文件,都会通过包名被引用
// a.go
package test
func A(){
}
// b.go
package test
func B(){
}
// c.go: c.go是另外的一个包
package main
import "test"
func main(){
test.A()
test.B()
}
包名冲突
包名冲突一般发生在最后一级包名相同的情况
import (
"crypto/rand"
mrand "math/rand" // alternative name mrand avoids conflict
)
SDK
fmt
package main
import (
"fmt"
)
func main() {
// %d 表示整型数字,%s 表示字符串
var stockcode=123
var enddate="2020-12-31"
var url="Code=%d&endDate=%s"
var target_url=fmt.Sprintf(url,stockcode,enddate)
fmt.Println(target_url)
}
input
容器
数组
声明数组时,必须指定大小,这是数组和切片声明的区别
var 数组变量名 [元素数量]Type
var a [3]int
var a [3]int =[3]int{1,2}
a:=[3]int{1,2,3}
var a [4][2]int
var a =[2][2]int{{1,1,},{1,1}}
// i 为index,v为值
for i, v := range a {
fmt.Println(i, v)
}
//数组可以用==和!=判断是否相等
// 程序会检查每一项是否相等
a==b,a!=b
// 若b与a形状相同,则将b拷贝给a
a=b
切片
go中的切片与Python大致相同:
- go的切片只支持 [start:end]
- go的切片底层与数组共享内存
- go中切片和数组本身就是不同的数据类型,不能直接=赋值
- go中的切片在声明时,不能也不需要指定大小
// 声明
var a []int
var a []int = []int{1,2,3,4,5}
a:=[]int{1,2,3,4,5}
a:=b[1:2]
a := make([]int, 5) // len(a)=5
b := make([]int, 0, 5) // len(b)=0, cap(b)=5
// 操作
// 在执行append后切片可能发生扩容,此时会返回新切片
a = append(a, 1) // 追加1个元素
a = append(a, 1, 2, 3) // 追加多个元素
a = append(a, []int{1,2,3}...) // 追加一个切片, 切片需要解包
/* 数组拷贝
copy(dest,src []T) int
拷贝最小长度并返回该长度
*/
slice1 := []int{1, 2, 3, 4, 5}
slice2 := []int{5, 4, 3}
len:=copy(slice2, slice1) // 只会复制slice1的前3个元素到slice2中
len=copy(slice1, slice2) // 只会复制slice2的3个元素到slice1的前3个位置
map
var mapname map[keytype]valuetype
var mp map[string]int{
"a":1,
"b":2,
"c":3,
}
mp["avb"]=2
delete(mp,"avb")
// ok为true时代表存在,反之不存在
// elem在不存在时为默认值
elem, ok = mp[key]
文件io
Go语言数据I/O对象及操作 (biancheng.net)
package main
import (
"fmt"
"io"
"strings"
)
func main() {
r := strings.NewReader("Hello, Reader!")
b := make([]byte, 8)
for {
n, err := r.Read(b)
fmt.Printf("n = %v err = %v b = %v\n", n, err, b)
fmt.Printf("b[:n] = %q\n", b[:n])
if err == io.EOF {
break
}
}
}
高级
错误
异常
defer
todo
接口
go程
goroutine是一种轻量级的线程实现方式,goroutine具有更小的内存占用和创建开销。
func main() {
go sayHello()
fmt.Println("Main function")
}
func sayHello() {
fmt.Println("Hello, goroutine!")
}
信道
信道 chan是go中的通信机制
使用chan时必须使用符号 <-
非缓冲信道
package main
import "fmt"
func sum(s []int, c chan int) {
sum := 0
for _, v := range s {
sum += v
}
c <- sum // 将和送入 c
}
func main() {
s := []int{7, 2, 8, -9, 4, 0}
var c = make(chan int)
go sum(s[:len(s)/2], c)
go sum(s[len(s)/2:], c)
x, y := <-c, <-c // 从 c 中接收
fmt.Println(x, y)
}
带缓冲的信道
package main
import( "fmt"
"time")
func main() {
ch := make(chan int,2)
ch <- 1
ch <- 2
fmt.Println(<-ch)
fmt.Println(<-ch)
}
总结,管道有带缓冲和不带缓冲之分,在声明信道时,我们可以指定大小x,此时声明的是缓冲管道,不带大小时声明的是非缓冲管道
非缓冲管道:
非缓冲管道在放入或取出数据时,是会进入阻塞的,例如: A向管道放了数据,只要该数据没有被取走,则A会一直sleep. B从管道取数据,只要管道一直为空,则B会一直等待。
package main import "fmt" func sum(s []int, c chan int) { sum := 0 for _, v := range s { sum += v } c <- sum // 将和送入 c } func main() { s := []int{7, 2, 8, -9, 4, 0} var c = make(chan int) go sum(s[:len(s)/2], c) go sum(s[len(s)/2:], c) x, y := <-c, <-c // 从 c 中接收 fmt.Println(x, y) }在上面的代码中,go了两个sum进程,在执行
c<-sum时,这两个进程都陷入sleep缓冲管道:
缓冲管道在放入数据时,若管道满,则陷入等待。在取数据时,若管道空,则陷入等待。
要注意区分非缓冲管道和缓冲管道的区别。大小为1的缓冲管道!=非缓冲管道
close和range
发送方可以调用 close来告知接收方信道关闭了
接收方可以通过 v, ok := <-ch,若ok为false则代表信道关闭
另外, range chan 会不断取信道值,直到信道关闭
package main
import (
"fmt"
)
func fibonacci(n int, c chan int) {
x, y := 0, 1
for i := 0; i < n; i++ {
c <- x
x, y = y, x+y
}
close(c)
}
func main() {
c := make(chan int, 10)
go fibonacci(cap(c), c)
for i := range c {
fmt.Println(i)
}
}
select
select语句会从多个可执行go程中选一个执行,若没有能执行的则陷入等待。
package main
import "fmt"
func fibonacci(c, quit chan int) {
x, y := 0, 1
for {
select {
case c <- x:
x, y = y, x+y
case <-quit:
fmt.Println("quit")
return
}
}
}
func main() {
c := make(chan int)
quit := make(chan int)
go func() {
for i := 0; i < 10; i++ {
fmt.Println(<-c)
}
quit <- 0
}()
fibonacci(c, quit)
}
在上面的代码中,main go程会在进行 c<-x后陷入阻塞,直到 func go程 打印 <-c, 随后,在经历了10次放入..打印后,main go程最后一次放入了斐波那契数列的第11项的值,并计算出了12项,等待放入,但func go程执行了quit,导致 main go程退出。
default
当select中其他go程都没准备好时,会执行defautl的内容
package main
import (
"fmt"
"time"
)
func main() {
tick := time.Tick(100 * time.Millisecond)
boom := time.After(500 * time.Millisecond)
for {
select {
case <-tick:
fmt.Println("tick.")
case <-boom:
fmt.Println("BOOM!")
return
default:
fmt.Println(" .")
time.Sleep(50 * time.Millisecond)
}
}
}
互斥锁
当我们只需要互斥的访问临界区时,就需要加互斥锁,而无需使用信道
package main
import (
"fmt"
"sync"
"time"
)
// SafeCounter 的并发使用是安全的。
type SafeCounter struct {
v map[string]int
mux sync.Mutex
}
// Inc 增加给定 key 的计数器的值。
func (c *SafeCounter) Inc(key string) {
c.mux.Lock()
// Lock 之后同一时刻只有一个 goroutine 能访问 c.v
c.v[key]++
c.mux.Unlock()
}
// Value 返回给定 key 的计数器的当前值。
func (c *SafeCounter) Value(key string) int {
c.mux.Lock()
// Lock 之后同一时刻只有一个 goroutine 能访问 c.v
defer c.mux.Unlock()
return c.v[key]
}
func main() {
c := SafeCounter{v: make(map[string]int)}
for i := 0; i < 1000; i++ {
go c.Inc("somekey")
}
time.Sleep(time.Second)
fmt.Println(c.Value("somekey"))
}
泛型
func Add[T 此处填写类型1|此处填写类型2](a T, b T) T {
return a + b
}