每天go一点点,只记录学习中, 自己用到和迷惑的东西, 记录精华部分, 不求大而全.
忙成狗, 把最近经常用的更新一波.
Hello world程序
package main // 入口必须是package main
import "fmt"
func main() { // 入口必须是main方法
fmt.Println("hello world")
}
运行go程序
在终端输入命令:
go run helloworld.go //直接运行
go build helloworld.go // 进行编译,得到二进制文件,很好的便携性
./helloworld
应用程序入口
- 必须是main包: package main
- 必须是main函数:func main()
- 文件名不一定是main.go
- 目录名不一定是main(无需和package保持一致)
退出返回值
Go的main函数不支持任何返回值
通过os.Exit来返回状态
func main() {
fmt.Println("hello world")
os.Exit(0) // 返回状态
}
获取命令行参数
Go的main不支持传入参数
通过os.Args获取命令行参数
测试程序
文件需要以_test.go结尾
测试函数要以Test开头
go数据类型
基本数据类型
不允许类型转化
package typetest
import "testing"
type myInt int64 // 定义int64的别名
func TestImplicit(t *testing.T) {
var a int = 1
var b int64
// b = a // cannot use a (type int) as type int64 in assignment
b = int64(a) // 支持显示类型转换
t.Log(b)
var c myInt
// c = b // cannot use b (type int64) as type myInt in assignment
c = myInt(b) // 支持显示类型转换
t.Log(c)
}
指针类型/string类型
func TestPoint(t *testing.T) {
a := 1
aPtr := &a
// aPtr = aPtr + 1 // 不支持指针运算 mismatched types *int and int
t.Log(a, aPtr) // 1 0xc0000a60d8
t.Logf("%T, %T", a, aPtr) // int, *int
}
func TestString(t *testing.T) {
var s string // string位数值类型,不是引用类型,所以不能用nil去判断
t.Log("*" + s + "*") // **
t.Log(len(s)) // 0
/* 判断字符串为空
if s=="" {
....
}
*/
}
运算符
Go语言没有++a --a
== 比较数组
==其他语言==中,数组名往往是引用类型,我们比较数据名的时候,往往比较的是引用是否相同,而不是值是否相同。
==go==中,如果两数组满足一定条件,可以进行数组值的比较
&^ 按位置零
右边为1: 左边bit位置零
右边为0: 左边bit位不变
1 &^ 1 == 0
0 &^ 1 == 0
1 &^ 0 == 1
0 &^ 0 == 0
条件与循环
循环:for
条件 if
if的条件可以分为两部分: 变量定义、条件(应用于函数多返回值)
条件 switch
func TestCondition(t *testing.T) {
n := 0
for n < 5 { /* while */
t.Log(n)
n++
}
}
func TestCondition2(t *testing.T) {
for i := 0; i < 5; i++ {
switch i {
case 1, 3:
t.Log("Odd")
case 2, 4:
t.Log("Even")
default:
t.Log("Other")
}
}
}
数组和切片
数组的声明
func TestArrayInit(t *testing.T) {
var a [3]int // 初始化为0
b := [...]int{1,2,3} // 自动判断元素个数
}
func TestArrayTravs(t *testing.T) {
/* 传统方法 */
for i:=0;i<len(a);i++ {
t.Log(a[i])
}
/* go range方式 */
for idx, e range a { //获取下标idx和值e
t.Log(idx, e)
}
/* go range方式 */
for _, e range a { // 不获取下标, 用 _ 占位
t.Log(e)
}
}
数组切片
切片
声明方式:
func TestSlict(t *testing.T) {
a := []int{1, 2, 3, 4} // 声明并初始化
t.Log(len(a), cap(a)) // 打印切片的元素个数, 容量大小
b := make([]int, 3, 5)
t.Log(len(b), cap(b)) // 3 5
b = append(b, 1) // 向切片b添加一个元素1
t.Log(b[0])
}
切片的共享存储结构
months := []string{"", "January", ..., "December"} // months是切片,含有13个元素, len==13, capacity==13
Q2 := months[4:7] // len==3, capacity==9 (从下标4开始,一直到切片months的最后,下标为12)
summer := mouths[6:9] // len==3, capacity==7
// Q2和summer共享了内存 months[6] ,图中绿色部分
summer[0] = "Unknow" // 绿色部分改为了Unknow
t.Log(Q2) // Q2共享绿色部分,也改变了
数组 vs 切片
数组长度固定
数组可比较
Map
map声明
- 最常用的是前两种声明方法。
- 使用make是为了指定capacity,避免map动态的增长,提升性能。
- make只指定capacity,但不初始化len(因为指定len的部分,会初始化为0,但是map是不能初始化为0的)
- 可以访问map的len,
len(m), 但是==不能访问==map的capacity,cap(m)
map的元素访问
访问map不存在的key,如果value类型是int,则默认是0. 不是nil, ==不能通过nil来判断key是否存在==
为了区分key是否存在,利用go的==多返回值== v, ok := m[key]
func TestMapKV(t *testing.T) {
m := map[int]int{} // 声明一个空的map
if v, ok := m[1]; ok { // 访问map一个元素的时候, 会返回两个值, v: value, ok: bool 是否存在
t.Logf("key 1's value is :%d", v)
} else {
t.Log("key 1 not exists!")
}
}
map的遍历
map与工厂模式
实现set
func TestSet(t *testing.T) {
set1 := map[int]bool{} // 声明一个map,key是int,value是bool
elem := 1
if exists := checkexist(set1, elem); !exists { // 判断key是否存在
set1[elem] = true
} else {
t.Logf("key %d already exists!", elem)
}
delete(set1, 1) // 删除key值
}
// 判断key是否存在
func checkexist(set map[int]bool, elem int) bool {
if set[elem] == true {
return true
} else {
return false
}
}
// ----------------------------------------------------------------
func TestSetNew(t *testing.T) {
set := map[int]bool{}
// set[1] = true
if _, ok := set[1]; ok {
fmt.Println("key exists!")
} else {
fmt.Println("key not exists!")
}
}
string
与其他语言的差异
- 不能通过nil判断, 要通过
s == ""判断 - string==只可读==,不能赋值
s[0]='c' // 出错; 返回的字节数,不是字符数 - 可以存放任意数据,不一定是可见字符
unicode vs utf8
Unicode是字符集,有哪些字符
utf8是unicode在内存中怎么存的(16进制)
rune 能够获取字符串的unicode编码
字符串遍历
当遍历字符串的时候,==得到的value是rune(unicode编码)==,不是utf8
t.Logf("%[1]c, %[1]d", c) : [1]表示都用第一个参数c
字符串常用函数
-
字符串分割与连接
-
字符串和其他类型的转换
- 整型转字符串,可以直接转
- 字符串转整型,有两个返回值,所以要用if判断一下
函数
与其他语言的区别
函数装饰器(函数式编程)
// 可以将timeSpend理解为函数装饰器
func timeSpend(innerFun func(op int) int) func(op int) int {
// 入参是要传入给innerFun的参数
return func(n int) int {
startTime := time.Now()
fmt.Println("startTime is : ", startTime)
// 执行传入的程序
ret := innerFun(n)
// 打印耗时
fmt.Println("the funciton cost :", time.Since(startTime).Seconds())
// 返回innerFun的返回值
return ret
}
}
func testCostFun(n int) int {
time.Sleep(time.Second * 2)
return n
}
func TestTimeSpendFunc(t *testing.T) {
// 类似装饰器,添加点功能,然后把新的函数返回(一般会保证入参和返回参数和被装饰的函数一致)
retFunc := timeSpend(testCostFun)
retFunc(10)
}
可变参数
可变长参数会被==转化成数组==,我们可以对其进行遍历
延迟执行defer
defer的函数,会在函数的返回值返回之前执行。
和C++的finally类似
用于==释放资源、锁==等。
go的面向对象编程
go不支持继承。
接口采用ducktype的方式
封装(数据、行为)
数据的封装:结构体
结构体初始化
行为的封装
行为的定义
// 定义一个myStruct的String方法, 入参是(), 返回值是string的方法
func (e myStruct) String() string {
return fmt.Sprintf("name: %s, age is %d", e.name, e.age)
}
/* 这两个方法定义一个就好
func (e *myStruct) String() string {
return fmt.Sprintf("name: %s, age is %d", e.name, e.age)
}
*/
func TestStruct(t *testing.T) {
e := myStruct{18, "panghu"} // e为myStruct对象
e1 := &myStruct(18, "panghu2") // ePtr 为myStruct对象的指针
// 无论是对象,还是对象指针,调用的方式都是 '.' 成员运算符
t.Log(e.String())
t.Log(e1.String())
}
go的接口
利用ducktype实现接口
go接口与其他接口的区别
- 非入侵性:接口的实现不依赖于接口(不需要显示的用
implement) - client对接口有依赖,所以可以将接口和client打包一个package,接口的实现可以单独打包
接口变量
type自定义类型
package type_test
import (
"fmt"
"testing"
)
type innerFunc func() string // 自定义函数类型 innerFunc 表示函数类型: 入参为空,返回值是string
// 这样就可以用innerFunc 来代替 func() string函数类型了
// 入参:inner是func() string类型, 返回值是 func() string类型
func sayHelloFirst(inner innerFunc) innerFunc {
return func() string {
fmt.Println("hello !")
ret := inner()
return ret
}
}
func WriteGOGOGO() string {
return "gogogo"
}
func TestType(t *testing.T) {
ret := sayHelloFirst(WriteGOGOGO)
t.Log(ret()) // 注意ret是一个函数,调用要加ret()
}
扩展(组合)与复用(继承)
go不支持继承, 只能使用组合。
通过==匿名嵌套==,可以使用父类的方法。
-
但==实际上还是组合==,不是继承,因为无法实现多态(不支持父类引用指向子类对象)。
-
go不支持LSP原则(父类能用的地方,子类都能用),不支持父类引用指向子类对象(==不支持隐式类型转换==)
接口实现多态
用接口变量来接收实现对象的指针。
需要注意一点,==接口变量只能接收实现类对象的指针==,不能接受实现类的对象
空接口与断言
空接口类似C++里面的 void*, 可以==接收任何类型==(注意不是指针)
但是==使用空接口的时候,必须将其转换为某种类型==
- go里面不是使用强制类型转换,而是使用断言
p.(sometype) - 断言返回两个值
v, ok,只有第二个值是true,才可以使用第一个转换后的值
接口的最佳实践
错误处理
go的错误机制与其他语言的区别
panic 和 recover
panic and os.Exit
func panic(v interface{})
panic的形参是一个空接口,可以接收任何类型,我们通常传入一个error类型
panic() vs os.Exit()
func Exit(code int)
func TestPanicRecoverOsExit(t *testing.T) {
// 比较panic和Os.exit
// fmt.Println("call panic:")
// ERROR1 := errors.New("this is panic")
// panic(ERROR1) // 打印堆栈信息
fmt.Println("call os.Exit():")
os.Exit(-1) // exit status 255
}
recover: 捕获panic
- 在
defer中调用recover需要在defer中进行调用,如果不在defer中调用,则不会阻止panicking sequence。- recover会返回传给panic的 err value
- 不在
defer中调用recover- recover不会阻止panickiing sequence
- recover() 返回 nil
// 在defer中调用recover
func TestRecoverAndPanic(t *testing.T) {
// defer捕获panic错误, 因为在defer中,所以会阻止panicking sequence
defer func() {
if err := recover(); err != nil {
fmt.Printf("(err value is : %s(%T)", err, err) // this is err from paninc(*errors.errorString)
}
}()
// panic
ERROR1 := errors.New("this is err from paninc")
panic(ERROR1)
}
// 在defer外部调用recover
func TestRecoverAndPanic(t *testing.T) {
fmt.Println("here========")
// 如果recover不在defer中调用, 不会阻止panicking sequence
// recover()返回nil,不会返回err信息
if err := recover(); err != nil {
fmt.Printf("(err value is : %s(%T)", err, err)
}
fmt.Println("here============")
ERROR1 := errors.New("this is err from paninc")
panic(ERROR1)
}
/* 输出 */
here========
here============
--- FAIL: TestRecoverAndPanic (0.00s)
panic: this is err from paninc [recovered]
panic: this is err from paninc
goroutine 6 [running]:
testing.tRunner.func1.1(0x1119280, 0xc000048480)
/usr/local/go/src/testing/testing.go:940 +0x2f5
testing.tRunner.func1(0xc000108120)
/usr/local/go/src/testing/testing.go:943 +0x3f9
panic(0x1119280, 0xc000048480)
构建可复用模块:package
package的约定
- 一个目录下的所有文件,package名字必须一致。
设置GOPATH
vi ~/.bash_profile
# 添加自己的项目路径
export GOPATH="/Users/panghu/go:/Users/panghu/Documents/go_project" # 会自动添加src目录,所以package只需要从src下的目录开始写就ok
source ~/.zshrc
重启Atom,查看GOPATH路径:
构建一个package
init方法
package test_init
// main被执行前,这两个init方法都会被执行
func init() {
fmt.Println("init1")
}
func init() {
fmt.Println("init2")
}
使用远程的packaage
go get -u https://github.com/easierway/concurrent_map.git
import cm "github.com/easierway/concurrent_map" //起一个别名
依赖管理vendor
这样就可以将当前project/package所依赖的特殊版本的package放到vendor目录下
常用的依赖管理工具
这些工具都利用了vendor路径
brew install glide
# 进入项目
glide init
# 安装
glid install
协程机制
线程threadvs协程groutine
java线程和KSE(系统线程/内核对象)对应关系是1:1
协程和KSE对应关系是多对多
package groutine_test
import (
"fmt"
"testing"
)
// 利用协程的方式,执行函数 func(i int) {fmt.Println(i)}
func TestGroutine(t *testing.T) {
for i := 1; i < 10; i++ { // 这里启动了9个协程
go func(i int) { // func前加go
fmt.Println(i)
}(i) // 方法体后加() 因为go都是值传递,所以传入的参数会被各个协程func复制,不存在竞争关系。
}
}
共享内存并发机制(锁、等待组)
sync.Mutex
sync.WaitGroup
package sharemem_test
import (
"fmt"
"sync"
"testing"
"time"
)
// 线程不安全的协程
func TestCount(t *testing.T) {
count := 0
for i := 0; i < 5000; i++ {
go func(i int) {
count++
}(i)
}
// 防止TestCount所在的协程执行的比我们自己用go启动的协程快,
//所以让TestCount等待1秒,让我们自己启动的协程都执行完毕
time.Sleep(1 * time.Second)
fmt.Printf("count is : %d\n", count) // 4476, 因为没有加锁,所以不是我们的预期值5000
}
// 线程安全的协程
func TestCountThreadSafe(t *testing.T) {
var mut sync.Mutex
count := 0
for i := 0; i < 5000; i++ {
go func() {
defer func() {
mut.Unlock()
}() // 在defer里面释放锁,防止忘记了
mut.Lock()
count++
}()
}
// 防止TestCountThreadSafe所在的协程执行的比我们自己用go启动的协程快,
//所以让TestCountThreadSafe等待1秒,让我们自己启动的协程都执行完毕
time.Sleep(1 * time.Second)
fmt.Printf("count is : %d\n", count) // 4476, 因为没有加锁,所以不是我们的预期值5000
}
// 线程安全,并且等待自己起的线程都执行完再结束的函数
func TestCountThreadSafeWaitGroup(t *testing.T) {
var mut sync.Mutex
var wg sync.WaitGroup
count := 0
// 启5000个协程
for i := 0; i < 5000; i++ {
// 将协程加入到waitgroup中
wg.Add(1)
go func() { // 启动一个协程
defer func() {
mut.Unlock()
wg.Done() // 协程执行完,把waitgroup数量-1
}()
mut.Lock()
count++
}()
}
wg.Wait() // 等待waitgroup里面的所有协程结束
fmt.Printf("count is %d\n", count)
}
CSP并发机制
CSP vs Actor
除此之外,go会主动的去处理channel传来的消息,和Actor的被动处理不同。
go channel的两种类型
代码实战
package async_test
import (
"fmt"
"testing"
"time"
)
// 异步服务
// (目标:调用这个异步服务RemoteService后,无需阻塞去等待它的返回值,而是在需要的时候再去获取 ==》 利用channel实现)
// 将RemoteServcie的返回值,放到channel中,等待接收方去主动获取
func RemoteService() string {
fmt.Println("remote service started!")
time.Sleep(time.Millisecond * 100)
fmt.Println("remote service done")
return "Result of remote servcie"
}
// InvokeAsyncService的工作有两个
// 1. 构造一个channel(连接生产者和消费者)
// 1.1 channel的生产者:协程(协程去调用RemoteService,并把返回的结果写channel)
// 1.2 channel的消费者:调用方TestAsyncService (InvokeAsyncService把channel返回给调用方)
// 2. 启了一个协程,该协程调用RemoteService,并将RemoteService的结果写入到channel中
func InvokeAsyncService() chan string {
fmt.Println("InvokeAsyncService start...")
// 构造一个阻塞型的channel(只能传递string类型的消息)
// 这个channel用于RemoteServcie往里面写数据
// 阻塞型channel
retChn := make(chan string)
// 非阻塞型channel
// retChn := make(chan string, 2) // 可以放两个string类型消息
// 启一个协程,调用RemoteService,并将RemoteService的结果,写入上面创建的chennel中
go func() {
fmt.Println("call RemoteServcie...")
// 获取RemoteService的结果
ret := RemoteService()
// 将结果放到 channel中,如果接收方不在,会被阻塞
fmt.Println("send ret data to channel....")
retChn <- ret
}()
fmt.Println("InvokeAsyncService Ended!")
return retChn
}
func DoSomethingElse() {
fmt.Println("do something else...")
time.Sleep(time.Millisecond * 1000)
fmt.Println("do somthing else DONE")
}
func TestAsyncService(t *testing.T) {
// 获取channel,channel的写入是异步服务的结果(由InvokeAsyncService启的协程向channel中写入)
chRet := InvokeAsyncService()
// 调完异步服务后,做自己的事情,不用等待异步服务结束
DoSomethingElse()
// 自己的事情做完了,去channel中获取异步服务RemoteService的结果数据
// 如果channel中没有数据:阻塞状态,等待数据到来
fmt.Printf("channel message received: %s \n", <-chRet)
}
多路选择和超时机制
func RemoteService() string {
time.Sleep(time.Millisecond * 100)
return "Result of remote servcie"
}
func InvokeAsyncService() chan string {
retChn := make(chan string)
go func() {
ret := RemoteService()
retChn <- ret
}()
return retChn
}
func TestAsyncService(t *testing.T) {
chRet := InvokeAsyncService()
select {
case ret := <-chRet:
fmt.Printf("async service's result is: %s\n", ret)
case <-time.After(time.Millisecond * 200): // 超过200毫秒,还没有结果,就超时
t.Error("time out!")
}
}
addressable
有几个点需要解释下:
- 常数为什么不可以寻址?: 如果可以寻址的话,我们可以通过指针修改常数的值,破坏了常数的定义。
- map的元素为什么不可以寻址?:两个原因,如果对象不存在,则返回零值,零值是不可变对象,所以不能寻址,如果对象存在,因为Go中map实现中元素的地址是变化的,这意味着寻址的结果是无意义的。
- 为什么slice不管是否可寻址,它的元素读是可以寻址的?:因为slice底层实现了一个数组,它是可以寻址的。
- 为什么字符串中的字符/字节又不能寻址呢:因为字符串是不可变的。
空接口切片
原文: InterfaceSlice
我们都知道, interface{}可以接收任意类型, 所以经常会有人写出下面的代码
var dataSlice []int = foo()
var interfaceSlice []interface{} = dataSlice
然后会报错:
cannot use dataSlice (type []int) as type []interface { } in assignment
你会有这样的疑问: 为什么我可以给interface{}赋任何值, 却不能给[]interface{}赋任何切片呢?
为什么会报错?
为什么这个式子不能成立[]interface{} := []myType?
主要有两个原因:
[]interface{} 并不是一个空接口interface{}类型, 而是一个切片slice类型! 只不过这个切片的元素都是空接口interface{}而已.
这么说或许还有点模糊, 我们来针对每个interface{}来看一下, 其实, 每个interface{}在接收一个变量后, 会有两部分, 一部分是该变量的类型, 一部分是该变量的值或者指针.
所以对于一个长度为N的[]interface{}来说, 它的数据长度是N*2. 而[]myType虽然长度也是N, 但是它的每个元素都是myType, 不需要再记录类型, 所以[]myType的数据部分的长度为N.
所以, 由于[]interface{}和[]myType的数据类型都是不同的, 自然也就无法快速赋值.
该怎么做呢?
make实例化一个长度为len([]myType)的空接口切片.
然后对每个元素进行赋值.
var dataSlice []int = foo()
var interfaceSlice []interface{} = make([]interface{}, len(dataSlice))
for i, d := range dataSlice {
interfaceSlice[i] = d
}
反射的应用
今天在工作中, 遇到别人开发一个接口, 返回一个**interface{}, 我只知道返回值是一个结构体struct**.
现在我要从这个interface{}中获取我所需要的字段值. 应该怎么做呢?
或许你会说, 直接**类型选择**不就ok了吗? 类似下面这样
retValue := functionOfThrid()
switch t := retValue.(type) {
case structType1:
...
case structType2:
...
default:
...
}
但是如果你无法枚举所有的struct类型呢?
此时我们就要用到反射了.
整体的思路是:
- 获取retValue的type, 判断它的Kind(结构体 or 结构体指针)
- 获取retValue的value, 并通过它去获取相应字段的值.
retValue := functionOfThird()
t := reflect.TypeOf(retValue)
v := reflect.ValueOf(retValue)
switch t.Kind() {
case reflect.Ptr: // 结构体指针类型(已知肯定是结构体)
fieldValue = v.Elem().FieldByName("fieldName").Int() // 获取整型字段1
fieldValue2 = v.Elem().FieldByName("fieldName2").String() // 获取字符串字段2
case reflect.Struct: // 结构体类型
fieldValue = v.FieldByName("fieldName").Int()
fieldValue2 = v.FieldByName("fieldName2").String()
default:
//err处理
}
这里没有涉及反射的基础知识. 有兴趣可以查看下文, 但是不推荐使用反射!!
Json解析
go的json解析相对于动态语言还是比较麻烦的, 其中最重要的还是要知道如何根据json写出对应的正确的struct
没啥好总结的, 直接贴原文 细说Golang的JSON解析