我们就不从安装和hello world开始了,首先来看下Go的变量和内置数据类型都有哪些。
变量声明
Go 语言与其他语言显著不同的一个地方在于,Go 语言的类型在变量后面。
方法一:指定变量类型,如果没有初始化,则变量默认为零值。
var a = "Agong"var b intvar c boolvar d string
这里的零值指的是:数值类型为0、布尔类型为false、空字符串等。
方法二:根据值自行判定变量类型。
var d = true
方法三:在函数体内需要初始化声明时使用:=(不可以用于全局变量的声明与赋值)
msg := "Hello World!"
数据类型
空值:nil 以下几种类型为nil
var a *intvar a []intvar a map[string] intvar a chan intvar a func(string) intvar a error // error 是接口
整型类型:int
浮点数类型:float32、float64
字节类型:byte
字符串类型:string
布尔值类型:bool(true或false)
在Go语言中,字符串使用UTF8编码,如果是英文每个字符占1byte;如果是中文,一般占3字节。string是一个不可变的byte切片。
package string_testimport ("reflect""testing")func TestString(t *testing.T) {var s string // 空字符串t.Log(s)s = "hello你好"t.Log(s, len(s)) // hello你好 11s = "中"t.Log(len(s)) // 3 是byte数//s[1] = '3'// string 是一个不可变的byte切片c := []rune(s) // 将string字符串转为rune数组t.Log(len(c)) // 1t.Logf("中 unicode %x", c[0]) // 中 unicode 4e2dt.Logf("中 UTF8 %x", s) // 中 UTF8 e4b8adt.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)) // 0fmt.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"] = 20t.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 mainimport "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 mainimport "fmt"func add(num int) {num += 1}func realAdd(num *int) {*num += 1}func main() {num := 100add(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 intvar ptr [MAX] *intfor 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 := 18if age < 18 { //可以简写为 if age := 18; age < 18{}t.Log("Kid")}else { // 注意else需要和if的}在同一行t.Log("Adult")}}
switch
func TestSwitch(t *testing.T) {type Gender int8const (MALE Gender = 1FEMALE Gender = 2)gender := MALEswitch gender {case MALE:t.Log("Male")//fallthroughcase FEMALE:t.Log("Female")//fallthroughdefault: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 := 0for 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 intvar i1, i2 intselect {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 mainimport "fmt"func max(num1, num2 int) int {/*声明局部变量*/var result intif num1 > num2{result = num1}if num1 < num2{result = num2}return result}func main() {/*定义局部变量*/var a int = 100var b int = 200var 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 mainimport ("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 mainimport "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 mainimport "fmt"type Student struct {name stringage 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 mainimport "fmt"type Person interface {getName() string}type Student struct {name stringage int}func (stu *Student) getName() string {return stu.name}func (stu *Student) getAge() int {return stu.age}type Worker struct {name stringgender 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"] = 24m["scores"] = [3] int{90, 97, 92}fmt.Println(m) // map[age:24 name:Jack scores:[90 97 92]]}
并发编程(goroutine)
goroutine 是轻量级线程,通过 go 关键字即可开启一个新的运行期线程 goroutine 。同一个程序中的所有 goroutine 共享同一个地址空间。
package mainimport ("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 mainimport ("fmt""sync""time")var wg sync.WaitGroupfunc 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 发送到通道 chv := <-ch // 从 ch 接收数据// 并把值赋给 v
声明通道
ch := make(chan int)
默认情况下,通道是不带缓冲区的。
package mainimport "fmt"func sum(s [] int, c chan int) {sum := 0for _, 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 <- 1a := <-chch <- 2ch <- 3fmt.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
CSDN这边更新可能会慢一些。可移步个人日更公众号:才浅的每日python。欢迎各位来交流~


524

被折叠的 条评论
为什么被折叠?



