第一本go的书籍
2013年买的第一本go语言的书,至今还是比较好的入门书籍
距离现在好几年了,这两年go生态相对比较好,从而线上服务转去golang。
实际就是现代C语言,没有class面向对象,只有使用函数来做面向对象。
go常用命令
go get
go get http://github.com/123/abc
$GOPATH
获取组件
go build
bin/
go run
go mod
dep
module
如果Go的版本太低不能使用,建议将Go的版本升级到最新。
环境变量中可以增加GOPROXY=https://goproxy.io 这样没有梯子的情况下可以正确的加载相应的包文件。
环境变量GO111MODULE不要设置,如果已经增加了这个变量请务必设置为GO111MODULE=auto。
在项目的根目录下使用命令go mod init projectName。
go tool
go tool nm ./example
基本类型
类型 长度(字节) 默认值 说明
bool 1 false
byte 1 0 uint8
rune 4 0 Unicode Code Point, int32
int, uint 4或8 0 32 或 64 位
int8, uint8 1 0 -128 ~ 127, 0 ~ 255,byte是uint8 的别名
int16, uint16 2 0 -32768 ~ 32767, 0 ~ 65535
int32, uint32 4 0 -21亿~ 21亿, 0 ~ 42亿,rune是int32 的别名
int64, uint64 8 0
float32 4 0.0
float64 8 0.0
complex64 8
complex128 16
uintptr 4或8 以存储指针的 uint32 或 uint64 整数
array 值类型
struct 值类型
string "" UTF-8 字符串
slice nil 引用类型
map nil 引用类型
channel nil 引用类型
interface nil 接口
function nil 函数
数组与切片
数组
Don’t communicate by sharing memory; share memory by communicating.
var arr0 [5]int = [5]int{1, 2, 3}
var arr1 = [5]int{1, 2, 3, 4, 5}
var arr2 = [...]int{1, 2, 3, 4, 5, 6}
var str = [5]string{3: "hello world", 4: "tom"}
深拷贝是指将值类型的数据进行拷贝的时候,拷贝的是数值本身,所以值类型的数据默认都是深拷贝。浅拷贝指的是拷贝的引用地址,修改拷贝过后的数据,原有的数据也被修改。 那么如何做到引用类型的深拷贝?也就是需要将引用类型的值进行拷贝。修改拷贝的值不会对原有的值造成影响。
// copy(目标切片,数据源) 深拷贝数据函数
s2 := []int{1, 2, 3, 4}
s3 := []int{7, 8, 9}
copy(s2, s3) //将s3拷贝到s2中
fmt.Println(s2) //结果 [7 8 9 4]
fmt.Println(s3) //结果 [7 8 9]
copy(s3, s2[2:]) //将s2中下标为2的位置 到结束的值 拷贝到s3中
fmt.Println(s2) //结果 [1 2 3 4]
fmt.Println(s3) //结果 [3 4 9]
copy(s3, s2) //将s2拷贝到s3中
fmt.Println(s2) //结果 [1 2 3 4]
fmt.Println(s3) //结果 [1 2 3]
二维数据
数组长度是固定的,如果要扩展数组,得使用切片
切片
make()是Go语言中的内置函数,主要用于创建并初始化slice切片类型,或者map字典类型,或者channel通道类型数据。他与new方法的区别是。new用于各种数据类型的内存分配,在Go语言中认为他返回的是一个指针。指向的是一个某种类型的零值。make 返回的是一个有着初始值的非零值。
使用make函数
var s3 []int = make([]int, 0)
or
var arr = [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
var slice0 []int = arr[start:end]
1:3
var slice1 []int = arr[:end]
var slice2 []int = arr[start:]
var slice3 []int = arr[:]
var slice4 = arr[:len(arr)-1]
切片是可索引的,并且可以由 len() 方法获取长度。
切片提供了计算容量的方法 cap() 可以测量切片最长可以达到多少。
切片是面向地址的,修改数值会影响相关变量。
map
定义语法
map[KeyType]ValueType
//1, 声明map 默认值是nil
var m1 map[key_data_type]value_data_type
声明 变量名称 map[key的数据类型]value的数据类型
//2,使用make声明
m2:=make(map[key_data_type]value_data_type)
//3,直接声明并初始化赋值map方法
m3:=map[string]int{"语文":89,"数学":23,"英语":90}
demo:
func main() {
scoreMap := make(map[string]int, 8)
scoreMap["张三"] = 90
scoreMap["小明"] = 100
fmt.Println(scoreMap)
fmt.Println(scoreMap["小明"])
fmt.Printf("type of a:%T\n", scoreMap)
}
结构体
基本上和C的struct一样
在Go语言中,使用type关键字可以定义出新的自定义类型,有了自定义类型之后我们就可以为自定义类型添加各种方法了。
type person1 struct {
name, city string
age int8
}
package main
import (
"fmt"
)
//定义结构体
type Person struct {
name string
age int
sex string
address string
}
实例:
func main() {
//实例化后并使用结构体
p := Person{} //使用简短声明方式,后面加上{}代表这是结构体
p.age = 2 //给结构体内成员变量赋值
p.address = "陕西"
p.name = "好家伙"
p.sex = "女"
fmt.Println(p.age, p.address, p.name, p.sex)//使用点.来访问结构体内成员的变量的值。
}
指针
基本上和C一致
指针是存储另一个变量的内存地址的变量。 例如: 变量B的值为100, 地址为0x1122。变量A的值为变量B的地址0x1122,那么A就拥有了变量B的地址,则A就成为指针。Go语言中通过&获取变量的地址。通过* 获取指针所对应的变量存储的数值。
package main
import (
"fmt"
)
func main() {
//定义一个变量
a := 2
fmt.Printf("变量A的地址为%p", &a) //通过%p占位符, &符号获取变量的内存地址。
//变量A的地址为0xc000072090
//创建一个指针
// 指针的声明 通过 *T 表示T类型的指针
var i *int //int类型的指针
var f *float64 //float64类型的指针
fmt.Println(i) // < nil >空指针
fmt.Println(f)
//因为指针存储的变量的地址 所以指针存储值
i = &a
fmt.Println(i) //i存储a的内存地址0xc000072090
fmt.Println(*i) //i存储这个指针存储的变量的数值2
*i = 100
fmt.Println(*i) //100
fmt.Println(a) //100通过指针操作 直接操作的是指针所对应的数值
}
指针的指针,也就是存储的不是具体的数值了,而是另一个指针的地址。
func main(){
a := 2
var i *int //声明一个int类型的指针
fmt.Println(&a) //0xc00000c1c8
i = &a //将a的地址取出来放到i里面
fmt.Println(&i) //0xc000006028
var a2 **int //声明一个指针类型的指针
a2 = &i //再把i的地址放进a2里面
fmt.Println(a2) //获取的是a2所对应的数值0xc000006028也就是i的地址
}
指针属于引用类型的数据, 所以在传递过程中是将参数的地址传给函数,将指针作为参数传递时,只有值类型的数据,需要传递指针,而引用类型的数据本身就是传递的地址,所以数组传递可以使用指针,切片是引用类型数据,则不需要传递指针传递。
package main
import (
"fmt"
)
func main() {
s := 10
fmt.Println(s) //调用函数之前数值是10
fun1(&s)
fmt.Println(s) //调用函数之后再访问则被修改成2
}
//接收一个int类型的指针作为参数
func fun1(a *int) {
*a = 2
}
使用结构体调用指针
//1 使用结构体指针
var p *Person
p = &p2 //将p2 的地址赋给p
import "encoding/json"
package main
import (
"encoding/json"
"fmt"
)
//结构体
type Prescription struct {
Name string
Unit string
Additive *Prescription
}
func main() {
p := Prescription{}
p.Name = "鹤顶红"
p.Unit = "1.2kg"
p.Additive = &Prescription{
Name: "砒霜",
Unit: "0.5kg",
}
buf, err := json.Marshal(p) //转换为json返回两个结果
if err != nil {
fmt.Println("err = ", err)
return
}
fmt.Println("json = ", string(buf))
}
package main
import (
"encoding/json"
"fmt"
)
//结构体
type Prescription struct {
Name string `json:"name"` //重新指定json字段为小写输出
Unit string `json:"unit"`
Additive *Prescription `json:"additive,omitempty"`
}
func main() {
jsonstr := `{"name":"鹤顶红","unit":"1.2kg","additive":{"name":"砒霜","unit":"0.5kg"}}`
var p Prescription
if err := json.Unmarshal([]byte(jsonstr), &p); err != nil {
fmt.Println(err)
}
fmt.Println(p)
}
流程控件
日常语言的if、for range、switch不作具体描述,比较特殊的用法是select
while(true)
{
}
select 语句类似于 switch 语句,但是select会随机执行一个可运行的case。如果没有case可运行,它将阻塞,直到有case可运行。
select 是Go中的一个控制结构,类似于用于通信的switch语句。每个case必须是一个通信操作,要么是发送要么是接收。 select 随机执行一个可运行的case。如果没有case可运行,它将阻塞,直到有case可运行。一个默认的子句应该总是可运行的。
select {
case communication clause :
statement(s);
case communication clause :
statement(s);
/* 你可以定义任意数量的 case */
default : /* 可选 */
statement(s);
}
函数与方法
如果你想让一个方法可以被别的包访问的话,你需要把这个方法的第一个字母大写。这是一种约定。
面向对象
package main
import (
"fmt"
)
type Phone interface {
call()
}
type NokiaPhone struct {
}
func (nokiaPhone NokiaPhone) call() {
fmt.Println("I am Nokia, I can call you!")
}
type IPhone struct {
}
func (iPhone IPhone) call() {
fmt.Println("I am iPhone, I can call you!")
}
func main() {
var phone Phone
phone = new(NokiaPhone)
phone.call()
phone = new(IPhone)
phone.call()
}
反射工具类
reflect
通道
通道(channel)是用来传递数据的一个数据结构。
//通道的声明
var channel chan int
//如果通道时nil 则要通过make创建通道
channel= make(chan int)
通道可用于两个 goroutine 之间通过传递一个指定类型的值来同步运行和通讯。操作符 <- 用于指定通道的方向,发送或接收。如果未指定方向,则为双向通道。
在函数或者方法前面加上关键字go,就会同时运行一个新的goroutine。
与函数不同的是goroutine调用之后会立即返回,不会等待goroutine的执行结果,所以goroutine不会接收返回值。 把封装main函数的goroutine叫做主goroutine,main函数作为主goroutine执行,如果main函数中goroutine终止了,程序也将终止,其他的goroutine都不会再执行。
虽然说Go编译器将Go的代码编译成本地可执行代码。不需要像java或者.net那样的语言需要一个虚拟机来运行,但其实go是运行在runtime调度器上的,它主要负责内存管理、垃圾回收、栈处理等等。也包含了Go运行时系统交互的操作,控制goroutine的操作,Go程序的调度器可以很合理的分配CPU资源给每一个任务。
Go1.5版本之前默认是单核执行的。从1.5之后使用可以通过runtime.GOMAXPROCS()来设置让程序并发执行,提高CPU的利用率。
package main
import (
"fmt"
"runtime"
"time"
)
func main() {
//获取当前GOROOT目录
fmt.Println("GOROOT:", runtime.GOROOT())
//获取当前操作系统
fmt.Println("操作系统:", runtime.GOOS)
//获取当前逻辑CPU数量
fmt.Println("逻辑CPU数量:", runtime.NumCPU())
//设置最大的可同时使用的CPU核数 取逻辑cpu数量
n := runtime.GOMAXPROCS(runtime.NumCPU())
fmt.Println(n) //一般在使用之前就将cpu数量设置好 所以最好放在init函数内执行
//goexit 终止当前goroutine
//创建一个goroutine
go func() {
fmt.Println("start...")
runtime.Goexit() //终止当前goroutine
fmt.Println("end...")
}()
time.Sleep(3 * time.Second) //主goroutine 休眠3秒 让子goroutine执行完
fmt.Println("main_end...")
}
延迟函数
package main
import (
"fmt"
)
func main() {
defer test(1) //第一个被defer的,函数后执行
defer test(2) //第二个被defer的,函数先执行
test(3) //没有defer的函数,第一次执行
//执行结果
//3
//2
//1
}
func test(s int) {
fmt.Println(s)
}
错误处理
实际每个函数都可以定义二个参数的返回,error类型
例如
txt, err = test("test.txt");
错误类型
package main
import (
"errors"
"fmt"
)
func main() {
err := errors.New("错误信息...")
fmt.Println(err)
num, err2 := Calculation(0)
fmt.Println(num, err2)
}
//通过内置errors包创建错误对象来返回
func Calculation(divisor int) (int, error) {
if divisor == 0 {
return 0, errors.New("错误:除数不能为零.")
}
return 100 / divisor, nil
}
panic和recover
中断信息,recover接收信息
package main
import (
"fmt"
)
func main() {
Test1()
}
func Test1() {
defer func() {
ms := recover()//这里执行恢复操作
fmt.Println(ms, "恢复执行了..") //恢复程序执行,且必须在defer函数中执行
}()
defer fmt.Println("第1个被defer执行")
defer fmt.Println("第2个被defer执行")
for i := 0; i <= 6; i++ {
fmt.Println(i);
if i == 4 {
panic("中断操作") //让程序进入恐慌 终端程序操作
}
}
defer fmt.Println("第3个被defer执行") //恐慌之后的代码是不会被执行的
}
常用工具包
官方常用工具包有如下
fmt、strings、strconv、os、io、errors
导入包
引入每个工具包,都先要先导入
使用import 导入包。go自己会默认从GO的安装目录和GOPATH环境变量中的目录,检索src下的目录进行检索包是否存在。所以导入包的时候路径要从src目录下开始写。GOPATH 就是我们自己定义的包的目录。
// 每行一个写法
import fmt
// 导入多个,还可以重命名
import (
"fmt"
"sync"
"os" "test"
_ "test.com/test" // 只调用里面的init,不调用里面的方法
)
闭包
闭包:
package main
import (
"fmt"
)
func main() {
res := closure()
fmt.Println(res) //0x49a880 返回内层函数函数体地址
r1 := res() //执行closure函数返回的匿名函数
fmt.Println(r1) //1
r2 := res()
fmt.Println(r2) //2
//普通的函数应该返回1,而这里存在闭包结构所以返回2 。
//一个外层函数当中有内层函数,这个内层函数会操作外层函数的局部变量,并且外层函数把内层函数作为返回值,则这里内层函数和外层函数的局部变量,统称为闭包结构。这个外层函数的局部变量的生命周期会发生改变,不会随着外层函数的结束而销毁。
//所以上面打印的r2 是累计到2 。
res2 := closure() //再次调用则产生新的闭包结构 局部变量则新定义的
fmt.Println(res2)
r3 := res2()
fmt.Println(r3)
}
//定义一个闭包结构的函数 返回一个匿名函数
func closure() func() int { //外层函数
//定义局部变量a
a := 0 //外层函数的局部变量
//定义内层匿名函数 并直接返回
return func() int { //内层函数
a++ //在匿名函数中将变量自增。内层函数用到了外层函数的局部变量,此变量不会随着外层函数的结束销毁
return a
}
}
sync工具
package main
import (
"fmt"
"sync"
)
//创建一个同步等待组的对象
var wg sync.WaitGroup
func main() {
wg.Add(3) //设置同步等待组的数量
go Relief1()
go Relief2()
go Relief3()
wg.Wait() //主goroutine进入阻塞状态
fmt.Println("main end...")
}
func Relief1() {
fmt.Println("func1...")
wg.Done() //执行完成 同步等待数量减1
}
func Relief2() {
defer wg.Done()
fmt.Println("func2...")
}
func Relief3() {
defer wg.Done() //推荐使用延时执行的方法来减去执行组的数量
fmt.Println("func3...")
}
互斥锁
互斥锁,当一个goroutine获得锁之后其他的就只能等待当前goroutine执行完成之后解锁后才能访问资源。对应的方法有上锁Lock()和解锁Unlock()。
读写锁
互斥锁是用来控制多个协程在访问同一个资源的时候进行加锁控制,保证了数据安全,但同时也降低了性能,如果说多个goroutine同时访问一个数据,只是读取一下数据,并没有对数据进行任何修改操作,那么不管多少个goroutine来读取都应该是可以的。主要问题在于修改。修改的数据就需要加锁操作,来保证数据在多个goroutine读取的时候统一。 读取和读取之间是不需要互斥操作的,所以我们用读写锁专门针对读操作和写操作的互斥锁。