Go Learning Notes

导言

如果要学习一门新的语言,入门的话需要了解哪些知识?

  • 有哪几种数据类型?, 如何定义?, 如何使用?
  • 有哪些流控制?, 如何遍历可迭代的数据类型?
  • 如何定义一个函数?,如何使用?
  • 如何定义一个类?,如何实现面向对象编程?(是否有继承,重载机制等)
  • 最重要的就是—> 写好代码后要如何编译运行?
  • 与其他语言的特别之处?

为什么学习Go

谷歌下Go语言的优点,比如并发性好,部署简单等等,由于没用过,所以这里对Go语言的评价,之后再补,现在学习Go的主要原因纯粹只是瞎打发时间…

Go 命令行使用

  • go get “package name” — 获取包
  • go run
  • go build

数据类型

Go是一个静态语言, 与C/C++类似, 上面图片中大部分数据类型,与C/C++相同,下面就逐一了解比较特殊的一些数据类型. 可以注意到上图中很多数据类型指明大小,如uint8,int32, 因此需要的是int的大小取决于运行环境的cpu大小可能为64位或32位.

byte 和 rune

  • byteuint8别名 (alias for uint8)

  • runeint32的别名(alias for int32)

    1
    2
    3
    4
    5
    6
    7
    // byte is an alias for uint8 and is equivalent to uint8 in all ways. It is
    // used, by convention, to distinguish byte values from 8-bit unsigned
    // integer values.
    type byte = uint8
    // rune is an alias for int32 and is equivalent to int32 in all ways. It is
    // used, by convention, to distinguish character values from integer values.
    type rune = int32

    byte用来表示raw data, 用以与0~255int类型区分开来,

    rune用来表示Unicode字符, 用于区分int32的数字类型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    package main

    import "fmt"
    const test rune = '好' // rune (unicode code point)
    const test2 string = "好"
    func main(){
    // 注意字符和字符串的区别
    fmt.Println(test) // 输出 22909
    fmt.Println(test2) // 输出 好
    }

复数类型

Go原生支持复数类型, complex64类型的实部和虚部都为float32, complex128类型实部和虚部都为float64

1
2
3
4
5
6
7
8
9
10
package main

import "fmt"

func main(){
var comp64 complex64 = 5 + 5i
var comp128 complex128 = 6 + 6i
fmt.Println(comp64) // (5+5i)
fmt.Println(comp128) // (6+6i)
}

数组

数组创建初始化

1
arr := [5]int{}  // 大括号表示初始化内容,不写默认为0

引用类型

引用类型有slice, map, channel

如何创建引用类型?

引用类型必须要通过关键字make创建,以便完成内存分配相关属性的初始化

1
2
// make 实现内存分配和初始化
func make([]T, len, [cap]) []T

slice 切片(动态数组)

slice 是个只读对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import (
"fmt"
"github.com/bradfitz/iter" // 引入 iter.N 方法
)

func main(){
sl := make([]int, 0, 6) // 类似于C++ 中的 int* sl = new int[6];
for x := range iter.N(6) {
sl = append(sl, x)
}
fmt.Println(sl) // 输出 [0 1 2 3 4 5]
// 输出 [1 2 3 4]
fmt.Println(sl[1:5]) // 利用切片特性,返回一个新的包含有第1个元素到第4个元素的slice
}

map

map 类似于Python中的dict

1
2
3
4
5
6
7
8
9
package main

import "fmt"

func main(){
mp := make(map[string]int) // 定义 key 为 string类型 ,value 为 int 类型的 map
mp["key"] = 123
fmt.Println(mp) // map[key:123]
}

Channel

Channel是Go中的一个核心类型,你可以把它看成一个管道,通过它并发核心单元就可以发送或者接收数据进行通讯(communication)。

  • 它的操作符(数据表示数据传送方向)是箭头 <-

    1
    2
    ch <- v    // 计算机 发送值v到Channel ch中
    v := <-ch // 计算机 从Channel ch中接收数据,并将数据赋值给v

    <-总是优先和最左边的类型结合。(The <- operator associates with the leftmost chan possible)

    1
    2
    3
    4
    chan<- chan int    // 等价 chan<- (chan int)
    chan<- <-chan int // 等价 chan<- (<-chan int)
    <-chan <-chan int // 等价 <-chan (<-chan int)
    chan (<-chan int)
  • 创建方法

    1
    2
    3
    ch := make(chan int,100)  // 创建双向传输int类型数据的 channel, 容量为100
    ch1 := make(chan<- float64) // 创建只能用来发 float64类型数据的channel
    ch2 := make(<-chan int) // 创建只能用来接收int类型数据的channel
  • 使用示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    package main

    import (
    "time"
    "fmt"
    )
    func main() {
    go func() {
    time.Sleep(1 * time.Hour)
    }()
    c := make(chan int)
    go func() {
    for i := 0; i < 10; i = i + 1 {
    c <- i
    }
    close(c)
    }()
    for i := range c {
    fmt.Println(i)
    }
    fmt.Println("Finished")
    }

定义变量方式

利用 var定义

  • 利用var定义变量, 可以自动初始化为零值
  • 具有类型推断功能, 无需定义类型(如果在定义变量后自行初始化, 编译器会自动推断变量类型)
1
2
3
4
5
6
7
8
9
10
11
var t1 int = 123  // 正常
var t2 = 123 // 无需定义类型, 自动类型推断
var t3 int // 需要定义类型, 自动初始化赋零值

// 批量定义
var x, y, z int
var s, n = "abc", 123
var (
a int
b float32
)

利用:=进行变量定义

  • 只能用于定义局部变量
  • 具有类型推断, 不会自动初始化
1
2
3
4
5
func test() {
t1 := 1
// t1 := 2 (会报错, 不可重复定义)
t1, t2 := 2,3 // 不会报错
}

控制流语句

Go语言只有三种控制语句,if..else,switch..case,for (注意没有while语句)

if..else

1
2
3
4
5
if 表达式 {  // 左大括号不可另起一行
do something
} else { // else 不可另起一行
do something
}

switch..case

1
2
3
4
5
6
7
8
switch 表达式 {
case 表达式1:
do something
case 表达式2:
do something
default:
do something
}

for

1
2
3
4
5
6
7
8
// for 可以用来代替while
for 表达式 {
do something
}
// for 遍历
for i:=1;i<10;i++{ // 没有++i,只有后缀自增
do something
}

for .. range

Go 语言中 range 关键字用于 for 循环中迭代数组(array)、切片(slice)、通道(channel)或集合(map)的元素。在数组和切片中它返回元素的索引和索引对应的值,在集合中返回 key-value 对的 key 值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package main

import (
"github.com/bradfitz/iter"
"fmt"
)

func main() {
str :="welcome to cocofe`s website"
arr := [10]int{}
sls := make([]string,10)
mp := make(map[string]int)
for i:= range iter.N(10){ // 借助第三方包,实现Python中的range(int)功能
// 为数组,map 赋值
arr[i] = i^2
mp[string(str[i])] = i
}
fmt.Println("遍历string")
for idx, ch := range str{
fmt.Println(idx,string(ch))
}
fmt.Println("遍历array")
for idx,val := range arr{
fmt.Println(idx,val)
}
fmt.Println("遍历slice")
for idx,val := range sls{
fmt.Println(idx,val)
}

fmt.Println("遍历map")
for key,val := range mp{
fmt.Println(key,val)
}
}

函数

Go函数借鉴了动态语言中的一些特性

  • 无需前置声明
  • 不支持重载
  • 不支持默认参数,不支持命名实参,(只能通过位置来确定参数)
  • 不支持命名嵌套定义(nested) 即不能嵌套函数
  • 支持匿名函数和闭包
  • 支持不定长参数
  • 支持多返回值
  • 支持命名返回值
1
2
3
func function_name( [parameter list] ) [return_types] {  // 左花括号不可另起一行
函数体
}

从函数中返回局部变量指针是安全的,编译器会通过”逃逸分析”(escape analysis)来决定是否在堆上分配内存

不定长参数

不定长参数本质上是个slice切片

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import "fmt"

func test(s string, a ...int){
fmt.Println(s)
// 显示类型和值
fmt.Printf("%T, %v \n",a, a)
}
func main() {
test("hello world", 1,2,3,4,5) //[]int, [1 2 3 4 5]

}

多返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 一般用于返回函数值和函数运行状态(异常或正常)
func test(s string, a ...int)(int, error){
fmt.Println(s)
fmt.Printf("%T, %v \n",a, a) // 显示类型和值
return 0, errors.New("normal")
}
// 函数返回值也可以命名, 返回值当做局部变量使用, 利用return做隐式返回
func test(s string, a ...int)(val int, err error){
fmt.Println(s)
fmt.Printf("%T, %v \n",a, a) // 显示类型和值
// 为返回值赋值
val = 0
err = errors.New("normal")
return
}

匿名函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import (
"fmt"
)
// 匿名函数作为参数传入
func test(f func(s string)){
f("hello world")
}

func main() {
// 匿名函数赋值给变量
pf := func (s string){
fmt.Println(s)
}
pf("hello world")
// 调用test函数 传入函数
test(pf)
}

延迟调用 defer

defer 常用于资源释放,错误处理,接触锁定等,它会在所在函数结束前调用,即使函数出现异常提前退出

延迟调用,注册的是调用, 因此,如果后面跟得是一个函数,需要提供所需的参数,以便后续调用

实现过程大致为 : 注册 - - > 缓存调用参数等 - - > 调用

具有一定的开销

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import (
"fmt"
)

func main() {
defer fmt.Println("资源释放。。。")
fmt.Println("do something")
}
// 输出顺序为:
// do something
// 资源释放。。。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import (
"fmt"
)

func main() {
// 延迟函数调用
defer func(s string){
fmt.Println(s)
}("资源释放")
//defer fmt.Println("资源释放。。。")
fmt.Println("do something")
}

多个延迟按照 FILO(栈)顺序调用

return和panic语句都会终止当前函数,引发延迟调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import (
"fmt"
)

func test()(i int){
defer func(){
fmt.Println(i)
i++
}()
return 100
}
func main() {
fmt.Println(test())
}
// 100
// 101

可知 test函数执行流程为:

  • 先 return 100(返回值i为100)
  • 延迟调用匿名函数, i自加一
  • 因此,最终的返回值为101

error

官方推荐通过函数返回的错误变量来判断函数是否正常运行

1
2
3
func func_name ()(val string, err error){  // 返回错误变量
...
}

官方将error定义为接口类型

1
2
3
type error struct{
Error() string
}
1
2
// 通过 errors.New(s string) 定义简单错误文本的error对象
err := errors.New("something error")

panic (恐慌) 和 recover

1
2
func panic(v interface())  // 传入参数空接口类型
func recover() interface() // 返回错误类型
  • panic 抛出的错误对象, recover接收错误对象
  • panic 会中断当前程序执行,执行延迟调用
1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import "log"

func main() {
defer func() {
if err := recover(); err != nil{ // 接收异常,并打印
log.Fatalln(err)
}
}()
panic("something error") // 抛出异常
println("exit") // 不执行
}

panic参数是空接口类型, 因此可以使用任何对象作为传入参数

结构(struct)

定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
type Address struct {
province string
city string
}

type Student struct {
name string
age int
address Address // 嵌套定义

}

type Student_2 struct {
name string
age int
address struct{ // 匿名struct方式定义
province string
city string
}
}

初始化

初始化有两种方式,一种按照参数名字,一种按照变量位置初始化,推荐使用第一种

1
2
3
4
5
6
7
8
9
10
type Student struct {
name string
age int
address Address // 嵌套定义

}
s1 := Student{
name:"世界",
age:24,
address: Address{province:"福建", city: "福州"}} // 如果该嵌入类型没有对应的变量名,则address 改为 类型名 Address

匿名字段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type Address struct {
province string
city string
}
type Student_2 struct {
name string
age int
Address // 匿名字段,没有变量名,只有类型
}
func main() {
s := Student_3{
name:"hello",age:24,
Address: Address{province:"福建", city:"福州"}} // 注意匿名字段的初始化方式
fmt.Println(s)
}

定义方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package main

import "fmt"
// 定义结构体,名字为Student
type Student struct {
name string
age int
}
// 定义struct的成员函数
// func 后面的括号用来传入实例
func (s Student) set_name_magic(name string){ // 参数传递方式是传值, 因此,不会修改传入的实参对象的值
s.name = name
}
// 定义struct的成员函数
func (s *Student) set_name (name string){ // 实例通过指针传递, 直接修改指针所指向对象
(*s).name = name
}
// 定义struct的成员函数
func (s *Student) detail (){
fmt.Println(*s)
}
func main() {
s := Student{name: "cocofe",age:25} // 创建student实例,并初始化
s.detail() // {cocofe 25}
s.set_name("hello world")
s.detail() // {hello world 25}
s.set_name_magic("can`t modify name")
s.detail() // {hello world 25}
}

对于方法来说如何选择实例的传入方式(拷贝指针/拷贝值)?

编译器会拷贝函数的传入参数(实参),如果传入指针,则拷贝指针,传入实例,则整个拷贝这个实例(这可能会有一定的开销),传引用与传值本质上是拷贝指针还是拷贝实例的区别,

绝大部分情况,优先使用传入指针的方式,但这种方式有个特别需要注意的地方- -> 如果以传入指针方式, 则该指针所指向的实例可能会被修改,因此,如果想避免这种情况,则可以考虑传值.

引用类型,字符串,函数等指针包装对象, 可以直接传入值(因为该值就是指针)

特殊点

  • 只有i++没有++i
  • 没有while语句
  • 不可重载

参考链接

学习Golang语言(3):类型

Go Channel 详解

本文标题:Go Learning Notes

文章作者:定。

发布时间:2018年6月5日 - 15时06分

本文字数:10,502字

原始链接:http://cocofe.cn/2018/06/05/go-learning-notes/

许可协议: Attribution-NonCommercial 4.0

转载请保留以上信息。