我们就不从安装和hello world开始了,首先来看下Go的变量和内置数据类型都有哪些。
变量声明
Go 语言与其他语言显著不同的一个地方在于,Go 语言的类型在变量后面。
方法一:指定变量类型,如果没有初始化,则变量默认为零值。
var a = "Agong"
var b int
var c bool
var d string
这里的零值指的是:数值类型为0、布尔类型为false、空字符串等。
方法二:根据值自行判定变量类型。
var d = true
方法三:在函数体内需要初始化声明时使用:=(不可以用于全局变量的声明与赋值)
msg := "Hello World!"
数据类型
空值:nil 以下几种类型为nil
var a *int
var a []int
var a map[string] int
var a chan int
var a func(string) int
var a error // error 是接口
整型类型:int
浮点数类型:float32、float64
字节类型:byte
字符串类型:string
布尔值类型:bool(true或false)
在Go语言中,字符串使用UTF8编码,如果是英文每个字符占1byte;如果是中文,一般占3字节。string是一个不可变的byte切片。
package string_test
import (
"reflect"
"testing"
)
func TestString(t *testing.T) {
var s string // 空字符串
t.Log(s)
s = "hello你好"
t.Log(s, len(s)) // hello你好 11
s = "中"
t.Log(len(s)) // 3 是byte数
//s[1] = '3'// string 是一个不可变的byte切片
c := []rune(s) // 将string字符串转为rune数组
t.Log(len(c)) // 1
t.Logf("中 unicode %x", c[0]) // 中 unicode 4e2d
t.Logf("中 UTF8 %x", s) // 中 UTF8 e4b8ad
t.Log(reflect.TypeOf(s).Kind()) //string
}
reflect.TypeOf().Kind()可以知道某个变量的类型;
[]rune表示将string转为rune数组。
数组(array)和切片(slice)
声明数组
var arr [10] int // 一维
var arr1 [5][5] int // 二维
初始化声明
var arr = [5]int{1, 2, 3, 4, 5}
// 或 arr := [5]int{1, 2, 3, 4, 5}
使用索引修改数组
func main() {
arr := [5] int{1, 2, 3, 4, 5}
for i := 0; i < len(arr); i++{
arr[i] += 100
}
fmt.Println(arr) // [101 102 103 104 105]
}
由于数组长度不能改变如果想拼接2个数组,或是获取子数组,需要使用切片。
切片使用数组作为底层结构,包含三个组件:容量,长度和指向底层数组的指针,切片可以随时进行扩展。
声明切片
func TestSlice(t *testing.T) {
slice1 := make([]float32, 0) // 长度为0的切片
slice2 := make([]float32, 3, 5) // [0 0 0] 长度为3容量为5的切片
fmt.Println(len(slice1)) // 0
fmt.Println(len(slice2), cap(slice2)) // 3 5
}
使用切片
func TestSlice(t *testing.T) {
slice2 := make([]float32, 3, 5) // [0 0 0] 长度为3容量为5的切片
slice2 = append(slice2, 1, 2, 3, 4)
t.Log(len(slice2), cap(slice2), slice2) // 7 12 [0 0 0 1 2 3 4]
// 子切片
sub1 := slice2[3:] //[1 2 3 4]
sub2 := slice2[:3] //[0 0 0]
// 合并切片
combined := append(sub1, sub2...)
t.Log(combined) // [1 2 3 4 0 0 0]
}
sub2... 表示将切片解构为 N 个独立的元素。
字典(键值对,map)
map类似于Python的dict,是一种存储键值对(Key-Value)的数据结构。
func Test(t *testing.T) {
// 仅声明
m1 := make(map[string]int)
// 声明时初始化
m2 := map[string]string{
"Jack": "Male",
"Rose": "Female",
}
// 赋值/修改
m1["Tom"] = 20
t.Log(m1, m2) // map[Tom:20] map[Jack:Male Rose:Female]
}
遍历map
func TestTravelMap(t *testing.T) {
m1 := map[int]int{1: 2, 2: 4, 3: 9}
for k, v := range m1{
t.Log(k, v)
}
}
//3 9
//1 2
//2 4
指针(pointer)
一个指针变量指向了一个值的内存地址,声明时使用符号*指明该变量为指针;对于已存在的变量,使用符号&获取该变量地址。
package main
import "fmt"
func main() {
var a int = 20 //声明实际变量
var ip *int // 声明指针变量
ip = &a // 指针变量的存储地址
fmt.Printf("a 变量的地址是:%x\n", &a)
/*指针变量的存储地址*/
fmt.Printf("ip变量存储的指针地址:%x\n", ip)
/*使用指针访问值*/
fmt.Printf("*ip变量的值:%d\n", *ip)
}
//a 变量的地址是:c00000a098
//ip变量存储的指针地址:c00000a098
//*ip变量的值:20
一般来说,指针通常在函数传递参数,或者给某个类型定义新的方法时使用。Go语言中参数是按值传递的,如果不使用指针,函数内部会拷贝一份参数的副本,对参数的修改并不会影响到外部变量的值。如果使用指针,则会影响外部变量的值。
package main
import "fmt"
func add(num int) {
num += 1
}
func realAdd(num *int) {
*num += 1
}
func main() {
num := 100
add(num)
fmt.Println(num) // 100 num没有变化
realAdd(&num)
fmt.Println(num) // 101 指针传递 num被修改
}
在某种情况下,我们需要保存数组,我们就需要用到指针。
const MAX = 3 // MAX为常量
func main() {
a := []int{10, 100, 200}
var i int
var ptr [MAX] *int
for i = 0; i < MAX; i++{
ptr[i] = &a[i] // 整数地址赋值给指针数组
}
for i = 0; i < MAX; i++{
fmt.Printf("a[%d] = %d\n", i, *ptr[i])
}
}
//a[0] = 10
//a[1] = 100
//a[2] = 200
流程控制(if, for, switch, select)
条件语句if else
func TestIfElse(t *testing.T) {
age := 18
if age < 18 { //可以简写为 if age := 18; age < 18{}
t.Log("Kid")
}else { // 注意else需要和if的}在同一行
t.Log("Adult")
}
}
switch
func TestSwitch(t *testing.T) {
type Gender int8
const (
MALE Gender = 1
FEMALE Gender = 2
)
gender := MALE
switch gender {
case MALE:
t.Log("Male")
//fallthrough
case FEMALE:
t.Log("Female")
//fallthrough
default:
t.Log("Unknown")
}
}
// Male
这里使用了type关键字定义了一个新的类型Gender,使用const定义了MALE 和 FEMALE 2 个常量。Go 语言的 switch 不需要 break,匹配到某个 case,执行完该 case 定义的行为后,默认不会继续往下执行。如果需要继续往下执行,需要使用 fallthrough。
case后还支持多种条件:
func TestSwitchMultiCase(t *testing.T) {
for i:=0; i<5; i++{
switch i {
case 0, 2: //case后支持多项判断
t.Log(i, "It is Even")
case 1, 3:
t.Log(i, "It is Odd")
default:
t.Log(i, "It is not in 0-3")
}
}
}
//0 It is Even
//1 It is Odd
//2 It is Even
//3 It is Odd
//4 It is not in 0-3
for
对数组(arr)、切片(slice)、字典(map) 使用 for range 遍历:
func TestForRange(t *testing.T) {
// 遍历数组
nums := []int{10, 20, 30, 40}
for _, num := range nums{
t.Log(num)
}
//10
//20
//30
//40
// 遍历字典
m2 := map[string]string{
"Jack": "Male",
"Rose": "Female",
}
for k, v := range m2{
t.Log(k, v)
}
//Jack Male
//Rose Female
}
由于在Go语言中只有用for关键字来控制循环,那如何实现while循环的效果呢
func TestWhileLoop(t *testing.T) {
n := 0
for n < 5 {
t.Log(n)
n++
}
}
//0
//1
//2
//3
//4
select
select是Go中的一个控制语句,每个case必须是一个通信操作,要么是发送要么接收。select 会循环检测条件,如果有满足则执行并退出,否则一直循环检测。(以下例子中的channel之后会说到)
func TestSelect(t *testing.T) {
var c1, c2, c3 chan int
var i1, i2 int
select {
case i1 = <-c1:
fmt.Print("received", i1, "from c1\n")
case c2 <- i2:
fmt.Print("sent", i2, "to c2\n")
case i3, ok := <-c3:
if ok{
fmt.Print("receive", i3, "from c3\n")
} else {
fmt.Print("c3 is closed\n")
}
default:
fmt.Print("no communication\n")
}
}
//no communication
函数(func)
一个函数使用关键字func定义,参数可以有多个,返回值也可以有多个。package main中的func main()约定为可执行程序的入口。
func funcName(param1 Type1, param2 Type2, ...) (return1 Type3, ...) {
// body
}
package main
import "fmt"
func max(num1, num2 int) int {
/*声明局部变量*/
var result int
if num1 > num2{
result = num1
}
if num1 < num2{
result = num2
}
return result
}
func main() {
/*定义局部变量*/
var a int = 100
var b int = 200
var res int
/*调用函数并返回最大值*/
res = max(a, b)
fmt.Printf("最大值是:%d\n", res)
}
//最大值是:200
错误处理
函数在实现过程中,如果出现不能处理的错误,可以返回给调用者处理,比如调用标准库的os.open读取文件,os.open有2个返回值,第一个是*file,第二个是error,如果调用成功error的值是nil;如果失败,例如文件不存在,可以通过error知道具体的错误信息。
error类型是一个接口类型,以下为error定义:
type error interface {
Error() string
}
package main
import (
"fmt"
"os"
)
func main() {
_, err := os.Open("filename.txt")
if err != nil{
fmt.Println(err)
}
}
//open filename.txt: The system cannot find the file specified.
使用error.New返回自定义的错误
func hello(name string) error {
if len(name) == 0{
return errors.New("error: name is null")
}
fmt.Println("Hello", name)
return nil
}
func main() {
if err := hello(""); err != nil{
fmt.Println(err)
}
}
//error: name is null
error往往是能预知的错误但是也可能出现一些不可预知的错误,例如数组越界,这种错误可能会导致程序非正常退出,在 Go 语言中称之为 panic。
package main
import "fmt"
func get(index int) int {
arr := [3]int{2, 3, 4}
return arr[index]
}
func main() {
fmt.Println(get(5))
fmt.Println("finished")
}
//panic: runtime error: index out of range [5] with length 3
//goroutine 1 [running]:
//Process finished with the exit code 2
panic用于主动抛出错误,recover用来捕获panic抛出的错误。发生panic后,程序会从调用panic的函数位置或发生panic的地方立即返回,逐层向上执行函数的defer语句,然后逐层打印函数调用堆栈,直到被recover捕获或运行到最外层函数。defer 和 recover类似于Python中的try...catch。
recover用来捕获panic,阻止panic继续向上传递。recover()和defer一起使用,但是defer只有在后面的函数体内直接被掉用才能捕获panic来终止异常,否则返回nil,异常继续向外传递。
func get(index int) (ret int) {
defer func() {
if r := recover(); r != nil {
fmt.Println("Some error happened!", r)
ret = -1
}
}()
arr := [3] int{2, 3, 4}
return arr[index]
}
func main() {
fmt.Println(get(5))
fmt.Println("finished")
}
//Some error happened! runtime error: index out of range [5] with length 3
//-1
//finished
-
在 get 函数中,使用 defer 定义了异常处理的函数,在协程退出前,会执行完 defer 挂载的任务。因此如果触发了 panic,控制权就交给了 defer。
-
在 defer 的处理逻辑中,使用 recover,使程序恢复正常,并且将返回值设置为 -1,在这里也可以不处理返回值,如果不处理返回值,返回值将被置为默认值 0。
结构体,方法和接口
结构体(struct)
结构体类似于其他语言的class,是一个由相同类型或不同类型的数据构成的数据集合。
package main
import "fmt"
type Student struct {
name string
age int
}
func (stu *Student) hello(person string) string {
return fmt.Sprintf("Hello %s, I am %s", person, stu.name)
}
func main() {
stu := &Student{
name: "Jack",
} // 实例化
msg := stu.hello("Rose") // 调用方法:实例名.方法名(参数)
fmt.Println(msg)
}
// Hello Rose, I am Jack
实现方法与实现函数的区别在于,func 和函数名hello 之间,加上该方法对应的实例名 stu 及其类型 *Student,可以通过实例名访问该实例的字段name和其他方法了。
此外,还可以通过new实例化
func main() {
stu2 := new(Student)
fmt.Println(stu2.hello("Alice")) // hello Alice, I am , name 被赋予默认值""
}
接口(interfaces)
一般而言,接口定义了一组方法的集合,接口不能被实例化,一个类型可以实现多个接口。Go 语言中,并不需要显式地声明实现了哪一个接口,只需要直接实现该接口对应的方法即可。
package main
import "fmt"
type Person interface {
getName() string
}
type Student struct {
name string
age int
}
func (stu *Student) getName() string {
return stu.name
}
func (stu *Student) getAge() int {
return stu.age
}
type Worker struct {
name string
gender string
}
func (w *Worker) getName() string {
return w.name
}
func main() {
var p Person = &Student{
name: "Jack",
age: 24,
}
fmt.Println(p.getName()) //Jack
// 实例可以强制类型转换为接口,接口也可以强制类型转换为实例。
stu := p.(*Student)
fmt.Println(stu.getAge()) //24
}
为确保某个类型实现了某个接口的所有方法,可以使用下面的方法进行检测,如果实现不完整,编译期将会报错。
var _ Person = (*Student)(nil)
var _ Person = (*Worker)(nil)
-
将空值 nil 转换为 *Student 类型,再转换为 Person 接口,如果转换失败,说明 Student 并没有实现 Person 接口的所有方法。
空接口
如果定义了一个没有任何方法的空接口,那么这个接口可以表示任意类型。
func main() {
m := make(map[string]interface{})
m["name"] = "Jack"
m["age"] = 24
m["scores"] = [3] int{90, 97, 92}
fmt.Println(m) // map[age:24 name:Jack scores:[90 97 92]]
}
并发编程(goroutine)
goroutine 是轻量级线程,通过 go 关键字即可开启一个新的运行期线程 goroutine 。同一个程序中的所有 goroutine 共享同一个地址空间。
package main
import (
"fmt"
"time"
)
func say(s string) {
for i := 0; i < 5; i++{
time.Sleep(100 * time.Millisecond)
fmt.Println(s)
}
}
func main() {
go say("world")
say("hello")
}
//hello
//world
//world
//hello
//hello
//world
//hello
//world
//hello
执行以上代码,你会看到输出的 hello 和 world 是没有固定先后顺序。因为它们是两个 goroutine 在执行。
Go 语言提供了 sync 和 channel 两种方式支持协程(goroutine)的并发。
例如我们希望并发下载 N 个资源,多个并发协程之间不需要通信,那么就可以使用 sync.WaitGroup,等待所有并发协程执行结束。
package main
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')) //启动新的协程并发执行 download 函数
}
wg.Wait() //等待所有的协程执行结束
fmt.Println("Done!")
}
//start to download a.com/2
//start to download a.com/1
//start to download a.com/0
//Done!
通道(channel)
通道可用于两个 goroutine 之间通过传递一个指定类型的值来同步运行和通讯。操作符 <-
用于指定通道的方向,发送或接收。如果未指定方向,则为双向通道。
ch <- v // 把 v 发送到通道 ch
v := <-ch // 从 ch 接收数据
// 并把值赋给 v
声明通道
ch := make(chan int)
默认情况下,通道是不带缓冲区的。
package main
import "fmt"
func sum(s [] int, c chan int) {
sum := 0
for _, v := range s{
sum += v
}
c <- sum //把sum发送到通道c
}
func main() {
s := []int{7, 2, 8, -9, 4, 0}
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, x+y) //-5 17 12
}
加入通道缓冲区
ch := make(chan int, 100)
通道遵循先进先出原则。
不带缓冲区的通道是同步的,即在向通道发送值时,必须及时接收,且必须一次接收完成。
而带缓冲区的通道是异步的,它仅会以缓冲区满而阻塞,直到先塞发送到通道的值被从通道中接收才可以继续往通道传值。
func main() {
ch := make(chan int, 2)
ch <- 1
a := <-ch
ch <- 2
ch <- 3
fmt.Println(<-ch)
fmt.Println(<-ch)
fmt.Println(a)
}
//2
//3
//1
如上面的例子,最多只能让同时在通道中停放2个值,想多传值,就需要把前面的值提前从通道中接收出去。
我们再将下载资源的例子实现协程之间阻塞等待并发协程返回消息:
var ch = make(chan string, 10) // 创建大小为 10 的缓冲通道
func download(url string) {
fmt.Println("start to download", url)
time.Sleep(time.Second)
ch <- url // 将 url 发送给通道ch
}
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!")
}
//start to download a.com/2
//start to download a.com/1
//start to download a.com/0
//finish a.com/2
//finish a.com/0
//finish a.com/1
//Done!
包(Package)和模块(Modules)
Go语言没有像其它语言一样有public、protected、private等访问控制修饰符,它是通过字母大小写来控制可见性的。区分粒度是包(package)。如果定义的常量、变量、类型、接口、结构、函数等的名称是大写字母开头表示能被其它包访问或调用(相当于public),非大写开头就只能在包内使用(相当于private,变量或常量也可以下划线开头)。
参考:
https://golang.org/
https://geektutu.com/post/quick-golang.html
https://www.runoob.com/go/go-tutorial.html
优快云这边更新可能会慢一些。可移步个人日更公众号:才浅的每日python。欢迎各位来交流~