前言这是我学习Go语法的笔记。由于有C和Python的基础，上手Go很快。笔记很粗糙，好在自己够用。此篇包括了Go相关的：并发编程，信号通道，定时器等。涉及关键字go select。goroutineGo天生优秀，利用关键字go可以很容易的实现并发编程。如一个并发示例：func print_a() { for { fmt.Println("a") time.Sleep(time.Second) // 睡1s } } func print_b() { for { fmt.Println("b") time.Sleep(time.Second) // // 睡1s } } func main() { go print_a() // 打印 a go print_b() // 打印 b fmt.Println("等待线程执行") for { // 死循环 } }运行上述代码，可看到程序交替打印ab。如果不出意外，程序会先打印“等待线程执行”，然后打印ab。所需注意两点：以go方式调用的函数都是异步执行的。也就是说，不会等到被调函数返回结果才往下执行；主协程退出，它的子协程跟着退出。因此上述demo中设置死循环，是为避免程序退出，导致两个协程跟着退出。runtime以下简单介绍runtime包的使用，其中包括了Gosched，Goexit和GOMAXPROCS。GoschedGosched表示让出时间片，可以让本应该执行的协程稍后执行。func main() { go func() { fmt.Println("this is a") }() fmt.Println("this is b") } // 输出： this is b上述代码中匿名函数的功能是打印语句“this is a”，以协程的方式执行。然而，在子协程开始之前，主协程已经往下执行并执行结束，所以demo的运行结果是：只打印了“this is b”。马上来试试Gosched的效果：func main() { go func() { fmt.Println("this is a") }() runtime.Gosched() fmt.Println("this is b") } // 输出： this is a this is b现在demo的执行结果是先打印a然后打印b。这是因为程序遇到runtime.Gosched()后会主动让出执行权，此时子协程也排队等着了，一看到“有空位”就立马钻进去执行，才有了我们看到的结果。GoexitGoexit能够终止一个正在执行的协程，其作用效果跟return类似。有一点需要额外注意：Goexit不能在主函数中使用，否则程序panic。子协程退出时，依然能够执行defer修饰的语句。func test() { defer fmt.Println("three") runtime.Goexit() // 等同return fmt.Println("four") // 这句话不会被执行 } func main() { fmt.Println("one") go test() fmt.Println("two") for{ } } // 输出： one two threeGOMAXPROCSGo语言默认单核运行，通过runtime.GOMAXPROCS()设置参与运行的CPU个数。runtime.NumCPU()用来获取计算机CPU数。查阅网上部分资料，发现有人说设置最大CPU数最好不要超过实际CPU数，但也有人说在一定范围内超过效果最佳。这里存疑。func main() { ... runtime.GOMAXPROCS(2) // 设置最大2核处理，函数返回计算机实际最大核数 ... }channelgoroutine运行在相同的地址空间上，因此访问内存必须做好同步。goroutine通过通信共享内存，而不是共享内存来通信。在函数传递中，channel引用传递。不同角度可对channel做不同分类：第一种可以是：有缓存channel和无缓存channel；第二种可以是：单向channel和双向channel。无缓存与有缓存创建channel：make(chan Type) // 无缓存，等价于make(chan Type, 0) make(chan Type, capacity) // 有缓存当capacity=0时，channel无缓冲阻塞读写；当capacity>0时，channel有缓冲非阻塞，直到写满capacity个元素才阻塞写入。channel通过操作符<-接收、发送数据。如ch<-表示发出数据到ch中，<-ch表示从ch中接收数据。常用操作如下：channel<- value // 发送value到channel <-channel // 接收并丢弃 x := <-channel // 从channel中接收数据，并赋值给x x, ok := <-channel // 功能同上，同时检查通道是否已关闭或者是否为空无缓存与有缓存的示例对比：var ch = make(chan int) func producer() { for { randomNum := rand.Intn(100) // 生产随机数 ch<- randomNum // 发送channel中 fmt.Println("生产一个数据为：", randomNum) } } func comsumer() { for { data := <-ch // 从channel中接收数据 fmt.Println("处理一个数据为：", data) } } func main() { go producer() go comsumer() for{ } }运行上述代码，总能看到生产者与消费者成对出现。这正是由于无缓冲channel在接收一个数据后会立马阻塞协程，等到消费者从channel中拿走数据再继续向下执行。// 输出： ... 处理一个数据为： 14 生产一个数据为： 14 生产一个数据为： 16 处理一个数据为： 16 处理一个数据为： 23 生产一个数据为： 23 生产一个数据为： 57 处理一个数据为： 57 ...现将channel修改为有缓冲，缓冲为10。即：var ch = make(chan int， 10)，观察运行结果：... 处理一个数据为： 45 处理一个数据为： 23 处理一个数据为： 44 生产一个数据为： 5 生产一个数据为： 30 生产一个数据为： 34 处理一个数据为： 86 ...生产者与消费者没有成对出现。因为channel“满载”前，程序不会被阻塞。len(ch) 缓冲区数据个数； cap(ch)缓冲区大小。单向channel前面提到的channel都是双向的，既可写也可读。Go允许做一个单向channel，其职责是：要么写、要么读。这有利于程序员对程序的控制，以及程序出错时方便排查。单向channel是由双向channel隐式转换来的：ch := make(chan int) var onlyWrite chan<- int = ch // 只写不读 var onlyRead <-chan int = ch // 只读不写然而单向不能转双向。这种用法更多是在函数传参时使用，场景为消费者与生产者：func provider(ch chan<-int) { ... } func comsumer(ch <-chan int) { ... } func main() { ch := make(chan int) go provider(ch) go comsumer(ch) ... }关闭channel可利用close()关闭channel，关闭后，不可以再向channel发送数据（会引发panic），但可以从channel中读取之前已有的数据。检查channel是否关闭可以：if num, ok := <-ch; ok == true { ... }可利用range对channel遍历：func get_data(onlyRead <-chan int) { for value := range onlyRead { fmt.Println(value) } } func main() { ch := make(chan int, 0) go get_data(ch) for i:=0; i<10; i++ { ch<- i }定时器定时器分Timer与Ticker，前者会在指定的时间上作用一下，后者则是循环作用。Timer简单示例：func main() { fmt.Println("当前时间", time.Now()) // 打印当前时间 timer := time.NewTimer(2 * time.Second) // 创建一个timer，设置为2s后往timer.C管道中写数据 // 写的数据是，这一刻的时间 fmt.Println("程序继续向下执行") fmt.Println("程序被阻塞...") fmt.Println("管道中的内容：", <-timer.C) // 程序在此阻塞，直到管道中有数据了，取出数据 fmt.Println("当前时间", time.Now()) // 打印当前时间 } // 输出： 当前时间 2019-03-02 10:44:46.4775098 +0800 CST m=+0.013954001 程序继续向下执行 程序被阻塞... 管道中的内容： 2019-03-02 10:44:48.5120856 +0800 CST m=+2.048529801 当前时间 2019-03-02 10:44:48.5120856 +0800 CST m=+2.048529801time包的一些其他用法：After：time.After(2* time.Second) // 定时2s，2s后向管道写入内容 // 由于该函数会返回一个管道类型的数据，因此可以直接对它做读操作 // eg: `<-time.After(2* time.Second)` 这种用法会把程序阻塞，类似time.Sleep()Stop：timer := time.NewTimer(2 * time.Second) timer.Stop() // 停止定时器，若在停止前定时器没有写入数据，以后也不会写入了Reset：fmt.Println("当前时间：", time.Now()) timer := time.NewTimer(10 * time.Second) // 10s后写入数据 timer.Reset(time.Second) // 重置时间，指定1s后写入数据 fmt.Println("输出内容：", <-timer.C) // 输出： 当前时间： 2019-03-02 11:10:26.3631141 +0800 CST m=+0.014000901 输出内容： 2019-03-02 11:10:27.3977497 +0800 CST m=+1.048636501Tickerfunc main() { ticker := time.NewTicker(1 * time.Second) for { fmt.Println(<-ticker.C) } } // 输出： 2019-03-02 12:50:32.005107 +0800 CST m=+1.011311801 2019-03-02 12:50:33.0049539 +0800 CST m=+2.011158701 2019-03-02 12:50:34.0057679 +0800 CST m=+3.011972701 2019-03-02 12:50:35.0052581 +0800 CST m=+4.011462901 2019-03-02 12:50:36.0057503 +0800 CST m=+5.011955101 ...selectselect是Go中的关键字，用于监听IO操作，当IO操作发生时，触发对应的动作。select的使用与switch相似，区别在于，select中的case必须是IO操作。... func main() { var ( chApha = make(chan string) chNum = make(chan int) ) ... select { case <-chApha: // 如果`<-chApha`可以先执行，就打印"处理一个字母" fmt.Println("处理一个字母") case <-chNum: // 如果`<-chNum`可以先执行，就打印"处理一个数字" fmt.Println("处理一个数字") default: // 如果前面都不能执行，就打印"等不到" fmt.Println("等不到了") } }说明：在select中使用default，属于少数情况。
前言这是我学习Go语法的笔记。由于有C和Python的基础，上手Go很快。笔记很粗糙，好在自己够用。此篇包括了Go相关的：错误处理，字符串处理，正则，JSON，文本操作。异常处理error官方errors包源码如下：// Package errors implements functions to manipulate errors. package errors // New returns an error that formats as the given text. func New(text string) error { return &errorString{text} } // errorString is a trivial implementation of error. type errorString struct { s string } func (e *errorString) Error() string { return e.s }即New()函数是返回一个errorString类型的变量，这种类型的变量拥有Error()方法，用法如下：err := errors.New("this is a err") fmt.Println(err.Error()) // 输出： this is a err另一种error来自于fmt包：err := fmt.Errorf("%s", "this is error in fmt") fmt.Println(err) // 输出： this is error in fmt一个完整的errors的使用：func myDiv(a, b int) (result int, err error) { if b == 0 { err = errors.New("除数不能为0") } else { result = a / b } return } func main() { result, err := myDiv(10, 0) if err != nil { fmt.Println(err.Error()) } else { fmt.Println("结果为：", result) } }panic致命错误panic()recover必须放在defer后面使用。恢复程序。func testb(x int) { defer func() { if err := recover(); err != nil { // 只在出现panic错误的起作用 fmt.Println(err) } }() var a[10] int a[x] = 111 // 当x>=10之后，属于越界访问 } func main() { testb(100) } // 输出： runtime error: index out of range当没有错误时，recover()函数返回nil，如果有错误，则返回错误信息。字符串处理字符串操作（strings）Contains// 前者（str）是否包含后者（substr） fmt.Println(strings.Contains("hello world", "hello")) // true fmt.Println(strings.Contains("hello world", "abc")) // falseJoin// 组合，类似Python中的 "".join() s := []string{"abc", "zty", "guan", "suanle"} fmt.Println(strings.Join(s, ",")) // abc,zty,guan,suanleIndex// 子字符串在字符串中出现的位置，第一个字符的索引，不存在返回-1 fmt.Println(strings.Index("helloworld", "wor")) // 5 fmt.Println(strings.Index("helloworld", "go")) // -1Repeat// 重复次数 buf := strings.Repeat("zty", 3) fmt.Println(buf) // ztyztyztyReplace// 替换 fmt.Println(strings.Replace("abcbe", "b", "!", 2)) // a!c!eSplit// 按指定sep拆分，返回切片 str := "zty-zty-zty" result := strings.Split(str, "-") fmt.Println(result) // [zty zty zty]Trim// 去掉两头 words := "fre--fre--fre" fmt.Println(strings.Trim(words, "fre")) // --fre--Fields// 去除字符串中的所有空格，返回切片 fmt.Println(strings.Fields(" ef er 3 op")) // [ef er 3 op]字符串转换（strconv）Append// 转换成字符串后追加到字节数组 func main() { // 定义一个切片 slice := make([]byte, 0, 1024) // 追加一个bool类型的变量 slice = strconv.AppendBool(slice, true) // 追加一个1234的值，10进制 slice = strconv.AppendInt(slice, 1234, 10) // 追加一个字符串 slice = strconv.AppendQuote(slice, "hello") fmt.Printf("%s", slice) // 输出：true1234"hello" }Format// 其他类型转化成字符串 // prec控制精度（排除指数部分）： // 对'f'、'e'、'E'，它表示小数点后的数字个数； // 对'g'、'G'，它控制总的数字个数。 // 如果prec 为-1，则代表使用最少数量的、但又必需的数字来表示f。 str := strconv.FormatFloat(3.14, 'f', -1, 64) fmt.Printf("内容：%s, 类型：%T", str, str) // 输出：内容：3.14, 类型：string// 整型转字符串 num := 512 numStr := strconv.Itoa(num) fmt.Printf("numStr的类型是 %T\n", numStr) // 输出：numStr的类型是 stringParse// 字符串转换成其他类型 // 字符串转bool flag, _ := strconv.ParseBool("true") fmt.Printf("flag = %v，其类型为：%T\n", flag, flag) // 字符串转整型 // base: 取值2-36，表示进位制。如果base=0，根据字符串前缀判断 num, _ := strconv.ParseInt("512", 10, 64) fmt.Printf("num = %d，其类型为：%T\n", num, num) // num = 512，其类型为：int64 anotherNum, _ := strconv.ParseInt("0xA", 0, 64) fmt.Printf("anotherNum = %d，其类型为：%T\n", anotherNum, anotherNum) // anotherNum = 10，其类型为：int64 // 另一种字符串转整型 newNum, _ := strconv.Atoi("512") fmt.Printf("newNum = %d，其类型为：%T\n", newNum, newNum) // newNum = 512，其类型为：int正则package main import ( "fmt" "regexp" // 正则包 ) func main() { str := "12334dew32kor435md.fer" // 生成规则 reg := regexp.MustCompile( `\d+`) // reg为空时表示失败 if reg != nil { fmt.Println(reg) } // -1表示匹配所有，1表示只匹配一个 res := reg.FindAllString(str, -1) // 匹配 fmt.Println(res) // 输出：[12334 32 435] }JSON编码通过结构体生成JSON：package main import ( "encoding/json" "fmt" ) // 成员变量名首字母必须大写 type IT struct { Company string Subject []string IsOK bool Price float64 } func main() { ajson := IT{"alibaba", []string{"C++", "Go", "Python"}, true, 512} // 编码。生成json格式的文本 buf, _ := json.MarshalIndent(ajson, "", " ") // 保留缩进 // buf, _ := json.Marshal(ajson) // 无缩进信息 fmt.Println(string(buf)) } // 输出： { "Company": "alibaba", "Subject": [ "C++", "Go", "Python" ], "IsOK": true, "Price": 512 }// 二次编码 type IT struct { Company string `json:"-"` // 不要输出到屏幕 Subject []string `json:"subject"` // 变量名以subject的方式显示 IsOK bool `json:",string"` // 键值以字符串的格式显示 Price float64 `json:",string"` } // 对应输出： { "subject": [ "C++", "Go", "Python" ], "IsOK": "true", "Price": "512" }通过map生成JSON：func main() { ajson := make(map[string]interface{}, 4) ajson["company"] = "alibaba" ajson["subject"] = []string{"C++", "Go", "Python"} ajson["isok"] = true ajson["price"] = 512 res, _ := json.Marshal(ajson) fmt.Println(string(res)) } // 输出： {"company":"alibaba","isok":true,"price":512,"subject":["C++","Go","Python"]}解码对于结构体：type IT struct { Company string `json:"company"` Subject []string `json:"subject"` Isok bool `json:"isok"` Price float64 `json:"price"` } func main() { jsonstr := `{"company":"alibaba","isok":true,"price":512,"subject":["C++","Go","Python"]}` var it IT // 第二个参数必须是指针，这样才能改变里面的值 if err := json.Unmarshal([]byte(jsonstr), &it); err != nil { fmt.Println(err) } else { fmt.Println(it) } }对于map：func main() { jsonstr := `{"company":"alibaba","isok":true,"price":512,"subject":["C++","Go","Python"]}` jsonmap := make(map[string]interface{}, 4) // 切片 if err := json.Unmarshal([]byte(jsonstr), &jsonmap); err != nil { fmt.Println(err) } else { fmt.Println(jsonmap) } var ( str string slice []interface{} ) // 通过类型断言（反推类型），取出每个字段的内容 for _, value := range jsonmap{ switch data := value.(type) { case string: str = data case []interface{}: // 注意为空类型的切片，而不是字符串切片 slice = data } } fmt.Println(str) // 输出：alibaba fmt.Println(slice) // 输出：[C++ Go Python] }文本操作写内容：func WriteToFile(path string) { f , _ := os.Create(path) // 创建一个文件 for i := 0; i < 10; i++ { s := fmt.Sprintf("this is %d\n", i) _, err := f.WriteString(s) // 写入字符串 if err != nil { fmt.Printf("error: %v", err) return } } defer f.Close() }读内容：func ReadFromFile(path string) { f, _ := os.Open(path) // 打开一个文件 content := make([]byte, 1024 * 2) // 创建一个切片，存放读取内容 // n 表示读取了多少个字节 n, err := f.Read(content) // 读取内容 if err != nil { fmt.Println(err) return } fmt.Println(string(content[:n])) // content[:n] 只打印有内容的部分 defer f.Close() }注意：os.Create()与os.Open()都是对os.OpenFile()做了封装。结合bufio一行一行的读取：f, _ := os.Open(path) bf := bufio.NewReader(f) for { // 以`\n`来读 // buf存储读取到的内容，包含`\n` buf, err := bf.ReadBytes('\n') if err != nil { // 文件内容读取完 if err == io.EOF { break } } fmt.Printf(string(buf)) } defer f.Close()
前言这是我学习Go语法的笔记。由于有C和Python的基础，上手Go很快。笔记很粗糙，好在自己够用。此篇包括了Go相关的：匿名组合，方法，接口等。涉及关键字interface。匿名组合Go语言没有封装、继承、多态的概念，但可以通过别的方式实现这些特性：封装：通过方法实现。继承：通过匿名字段实现。多态：通过接口实现。匿名字段理解Go语言中匿名字段不妨类比其他的语言的“类和继承”的概念，这里我用Python举例。当我们需要避免一些重复工作时——比方现在我们需要两种人，一种学生，一种上班族，代码可以如下：# 学生 class Student(object): name = "" age = 0 gender = 1 # 1表示男生 school = "" # 上班族 class OfficeWorker(object): name = "" age = 0 gender = 1 company = ""很好，代码写完了……欸卧槽？！怎么重复了name、age、gender？事实上，但凡一个人，都有应该有名字年纪和性别。所以“学生”与“上班族”之间存在相同的成员变量不足为奇，而程序员为了“偷懒”，一拍脑袋想出了继承这一概念，于是：class Person(object): name = "" age = 0 gender = 1 class Student(Person): school = "" class OfficeWorker(Person): company = ""我们称Student和OfficeWorker是Person的派生类，它们继承自Person。现在Go语言的程序员也想偷懒，因此有了匿名字段，上述内容用Go实现如下：type Person struct { name string age int gender byte } type Student struct { Person // 结构体匿名字段 school string } type OfficeWorker struct { Person company string }此时结构体Person就作为结构体Student的匿名字段。很显然，由于Person充当的是一个数据类型（声明一个Person类型的结构体语法：var p Person），所以对于结构体Student来说，数据类型为Person的变量名并不存在，那么，就“匿名”了！且Student包含Person中的所有字段。结构体匿名字段也就是说，这个匿名变量，其类型为结构体。因此，前面说的Person，在Student中就起到了辅助匿名变量的任务。初始化赋值：对于初始化一个Student类型的变量，其方法可以如下几种：var stu = Student{Person{"ty", 22, 'm'}, "XHUnivercity" } // 顺序初始化 var stu = Student{Person{age:22}, "XHUnivercity"} // 对于Person来说是指定初始化，对整个Student来说，是顺序初始化 var stu = Student{Person:Person{age:22}} // 整个Student为指定初始化 stu := Student{Person{"ty", 22, 'm'}, "XHUnivercity" } // 简短声明当匿名字段的数据类型为指针时，需要加上取地址符（&）：type Student struct { *Person // 结构体匿名字段 school string } var stu = Student{&Person{"ty", 22, 'm'}, "XHUnivercity" } // &Person或者用new()提前分配空间，再用点操作访问成员变量：var stu Student stu.Person = new(Person) stu.name = ... ...非初始化赋值：非初始化赋值有三种方法：type Person struct { name string age int gender byte } type Student struct { Person school string } func main() { var stu1, stu2, stu3 Student // 第一种方法 stu1.name = "ty" stu1.age = 22 stu1.gender = 'm' stu1.school = "XHUnivercity" fmt.Printf("%+v\n", stu1) // 第二种方法 stu2.Person.name = "ty" stu2.Person.age = 22 stu2.Person.gender = 'm' stu2.school = "XHUnivercity" fmt.Printf("%+v\n", stu2) // 第三种方法 stu3.Person = Person{"ty", 22, 'm'} stu3.school = "XHUnivercity" fmt.Printf("%+v\n", stu3) } // 输出： {Person:{name:ty age:22 gender:109} school:XHUnivercity} {Person:{name:ty age:22 gender:109} school:XHUnivercity} {Person:{name:ty age:22 gender:109} school:XHUnivercity}同名字段在两个具有“父子”关系的类中，如果出现相同名字的成员变量，子类的实例化对象会优先访问子类中成员变量。这样的结论也适用于Go语言。首先，当一个结构体中的使用了另一个结构体，且两个结构体中存在相同名称的成员变量，我们称这个变量为同名字段。就像下边的salary一样：type Boyfriend struct{ salary int } type Girlfrien struct{ Boyfriend salary int } func main() { var girl = Girlfrien{Boyfriend{30000}, 30000} fmt.Printf("%+v\n", girl) var bagPrice = 4000 girl.salary -= bagPrice // 女生要买包包了（直接点操作） fmt.Printf("%+v\n", girl) } // 输出： {Boyfriend:{salary:30000} salary:30000} {Boyfriend:{salary:30000} salary:26000}这是可以理解的：尽管男友每个月上交工资，但女生仍要清楚，自己仅仅拥有随意花自己钱的权利。所以直接点操作访问的是自己的salary，如果需要用男友的钱买包包，我建议经过男友的同意比较好：girl.Boyfriend.salary -= bagPrice // 使用 Boyfriend 中的salary非结构体匿名字段并非匿名字段的数据类型只能是结构体，基础类型、自定义类型也可以：type mystr string type Person struct{ name string } type Student struct { Person // 结构体匿名字段 int // 基础类型匿名字段 mystr // 自定义类型匿名字段 } func main() { stu := Student{Person{"ty"}, 22, "XHUnivercity"} fmt.Printf("%+v\n", stu) // 也可以 fmt.Println(stu.Person, stu.int, stu.mystr) // 只有匿名字段可以 点 + 数据类型 } // 输出： {Person:{name:ty} int:22 mystr:XHUnivercity} {ty} 22 XHUnivercity需要注意两点：只有匿名字段可以 点 + 数据类型方式的访问；对匿名字段的要求：一个结构体中不应该存在相同类型的匿名字段。方法什么是方法大部分语言中方法和函数是相同的概念，Go语言却不是，快速入门Go2 中提到的是函数，格式如：func funcName(paramsList) (returnList) {}而方法格式为：func (receiver ReceiverType) funcName(paramsList) (returnList) {}Go语言中，可以给任意自定义类型添加相应的方法。比方说：type zty string func (z zty) greet(words string) { fmt.Printf("%s\n", words) } func main() { var z zty z.greet("hello world") // 对于类型为zty的变量来说，它们都拥有了greet()这个方法 } // 输出： hello world如果方法配合结构体一起使用，有没有很像其他语言中的类呢！关于方法的使用需要注意以下几点：参数receiver可以任意命名，如果方法中未曾使用，可省略参数名。参数receiver类型可以是T或*T。基类型T不能是接口或指针。不支持重载。（只要接收者类型不一样，此时认为T与*T是同一种类型，这个方法就算同名也是不同的方法，不会出现重复定义函数的错误。）值语义和引用语义值语义表示方法的接收者为普通变量，非指针。即，通过拷贝方式传入：type student struct{ name string age int id int64 } func (s student) setInfoValueByValue(name string, age int, id int64) { s.name = name s.age = age s.id = id } func main() { var s student s.setInfoValueByValue("ty", 22, 201431060011) fmt.Printf("name = %s, age = %d, id = %d", s.name, s.age, s.id) } // 输出： name = , age = 0, id = 0引用语义表示接收者为指针类型的变量，利用引用传递：type student struct{ name string age int id int64 } func (s *student) setInfoValueByPointer(name string, age int, id int64) { s.name = name s.age = age s.id = id } func main() { var s student p := &s p.setInfoValueByPointer("ty", 22, 201431060011) fmt.Printf("name = %s, age = %d, id = %d", s.name, s.age, s.id) } // 输出： name = ty, age = 22, id = 201431060011所以二者最大的区别在于：能不能通过方法来改变自身的值。方法集即：可以被该类型的值调用的所有方法的集合。由于Go语言中的指针与普通变量使用起来很灵活，所以可以通过转换调用彼此的方法：type me string func (m me) PrintAlpha() { fmt.Println("a, b, c") } func (m *me) PrintDigit() { fmt.Println("1, 2, 3") } func main() { var m me m.PrintAlpha() (&m).PrintDigit() fmt.Println("-------- 分割线 -------- ") p := new(me) (*p).PrintAlpha() p.PrintDigit() } // 输出： a, b, c 1, 2, 3 -------- 分割线 -------- a, b, c 1, 2, 3事实上编译器可以自动识别并做类型转换，所以上述代码可以修改两句：(&m).PrintDigit() // ... (*p).PrintAlpha() // 改为 m.PrintDigit() // ... p.PrintAlpha()方法的继承当结构体B中包含了结构体A，那么结构体B就拥有了结构体A中的所有方法。方法的重写当结构体B中包含了结构体A，为结构体B绑定的方法恰好与结构体A中的方法同名，此时认为重写了这个方法。在调用过程中，结构体B类型的变量会优先调用结构体B上绑定的方法，如果没有，才会去结构体A中寻找。方法表达式即：记录了指定方法的地址的指针。使用方法表达式时，第一个参数一定是这个方法的接收者类型的变量。func (me) greet(words string) { fmt.Println(words) } func main() { var m me p := (me).greet // p为方法表达式 p(m, "hello world") // 使用方法表达式，第一个参数一定是这个方法的接收者类型的变量 } // 输出： hello world接口Go语言中，接口是一个自定义类型，作为描述一系列方法的集合。关于接口需要注意以下几点：接口命名习惯以er结尾。接口只有方法声明，没有实现，没有数据字段。接口可以匿名嵌入其他接口，或嵌入到结构中。接口的定义接口的定义类似结构体定义：type Humaner interface { // ... }接口的使用利用接口，可以很好的实现多态：type Humaner interface { sayWords() // 方法声明 } type ( personOne string personTwo string ) func (personOne) sayWords() { fmt.Printf("我是 personOne，很高兴认识大家\n") } func (personTwo) sayWords() { fmt.Printf("在下 personTwo，庆幸相遇\n") } // 注意参数类型 func sayWords(i Humaner) { i.sayWords() } func main() { var ( one personOne two personTwo ) // 同一个函数，实现不同的功能 sayWords(one) sayWords(two) } // 输出： 我是 personOne，很高兴认识大家 在下 personTwo，庆幸相遇接口的继承与结构体相似，接口继承又被称为接口嵌用。接口的转换Go语言中，作为超集的接口可以转化为子集接口，反之却不行。我们对超、子集接口有如下定义：type Oner interface { // 子集 sayWords() } type Twoer interface { // 超集，比子集 Oner 多了一个sing()方法 Oner // 匿名嵌入 sing() }所以此时可以超集转子集：var one Oner one = me("one") var two Twoer two = me("two") one = two // 正确 one.sayWords()绝不可以子转超：two = one // 错误 two.sayWords()空接口你可以将空接口这个概念认为是C语言中的void *，因为空接口可以存储任意类型的值（interface{}）。func main() { var i interface{} = 1 fmt.Printf("类型：%T, 值：%v\n", i, i) i = "abc" fmt.Printf("类型：%T, 值：%v\n", i, i) i = 5.12 fmt.Printf("类型：%T, 值：%v\n", i, i) } // 输出： 类型：int, 值：1 类型：string, 值：abc 类型：float64, 值：5.12因为空接口可以存储任意类型的变量，而有时候我们又需要知道变量的类型，此时就有了类型断言的概念。一般而言有两种方法，第一种利用if ... else if...，第二种switch。// if ... else if ... func main() { var i interface{} = 1 if value, ok := i.(string); ok { fmt.Printf("i 的类型是string, i 的值是 %s", value) } else if value, ok := i.(int); ok { fmt.Printf("i 的类型是int, i 的值是 %d", value) } }func main() { var i interface{} = 1 switch value := i.(type) { // .(type) 这样的操作只能在switch中使用 case int: fmt.Printf("i 的类型是int, i 的值是 %d", value) case string: fmt.Printf("i 的类型是string, i 的值是 %s", value) } }
前言这是我学习Go语法的笔记。由于有C和Python的基础，上手Go很快。笔记很粗糙，好在自己够用。此篇包括了Go相关的：指针，数组，切片，字典，结构体等。涉及关键字struct。指针Go语言中的指针与C稍异。如C中，声明一个int类型的指针语句：int* p ，所以p的类型就为int*。而Go语言中，*在前面：func main() { num := 512 var p *int // Go语言中‘*’在前面 p = &num fmt.Printf("address of num is %p", p) }由于Go语言在声明变量的时候会自动初始化值，对于指针，会指向nil，而不是NULL（Go中没有NULL）：func main() { var p *int fmt.Printf("address of pointer is %p, and its content is %v", p, p) } // 输出： address of num is 0x0, and its content is <nil>Go语言不允许操作没有合法指向的内存：func main() { var p *int *p = 512 // 对p指针指向的内存赋值 fmt.Printf("address of pointer is %p, and its content is %v", p, p) } // 抛异常： panic: runtime error: invalid memory address or nil pointer dereference [signal 0xc0000005 code=0x1 addr=0x0 pc=0x490056]newnew()函数是用来分配内存的内建函数，用法为new(T)（T是类型），会将分配的内存置为0值，同时返回该内存的地址：func main() { p := new(int) // 返回*int指针 fmt.Printf("type of pointer is %T\n", p) fmt.Printf("content of memory is %v\n", *p) } // 输出： type of pointer is *int content of memory is 0此时你可以正确操作指针p指向的内存，比如正确的赋值：*p = 512 // 不可以赋值字符串从C语言角度理解这个过程，可以近似为：int main() { int *p = (int*)malloc(sizeof(int)); // 申请内存 memset(p, 0, sizeof(int)); // 内存置0操作 printf("address of memory is %p, content of memory is %d", *p, *p); free(p); // 释放操作 return 0; }重点在于，Go语言有完善的GC机制，不需要手动释放内存；而C中，每一个malloc后，都应该有对应的free操作。数组Go中的数组声明与C也不同：// Go var array [3]int // 声明一维数组 var array [2][3]int // 声明二维数组 var array = [3]int{1, 2, 3} // 对一维数组赋值 array := [3]int{1, 2, 3} // 简短声明一维数组 var array = [2][3] // 对二维数组赋值 array := [2][3]int{{1, 2, 3}, {3, 2, 1}} // 简短声明一维数组 var arr = [...]int{1, 2, 3, 4} // 允许使用`...`自动计算长度 arr := [...]int{1, 2, 3, 4} // C int array[3]; // 声明一维数组 int array[2][3]; // 声明二维数组 int array[3] = {1, 2, 3}; int array[2][3] = {{1, 2, 3}, {3, 2, 1}};对Go中的数组，有以下几点需要注意：数组声明时不可以用变量表示元素个数，如：// 错误操作 n := 10 array := [n]int数组可以指定元素初始化（未指定元素默认0值），如：func main() { array := [5]int{2: 512, 4: 1024} // 冒号前边的数表示索引值 for i := range array { fmt.Printf("%d ", array[i]) } }0 512 0 1024数组支持==或!=运算，前提是数组类型要一样，即[x][y]type（eg：2int） 一致：func main() { arr1 := [2][3] int {{1, 2, 3}, {3, 2, 1}} arr2 := [2][3] int {{1, 2, 3}, {3, 2, 1}} fmt.Println("arr 1 == arr2 ?", arr1 == arr2) arr3 := [2][3] int {{2, 1, 3}, {3, 2, 1}} arr4 := [2][3] int {{1, 2, 3}, {3, 2, 1}} fmt.Println("arr 3 == arr4 ?", arr3 == arr4) } // 输出 arr 1 == arr2 ? true arr 3 == arr4 ? false同类型的数组允许相互赋值：var arr1 = [2]int {1, 2} var arr2 [2]int arr2 = arr1Go语言中，传递数组是值传递：func change(arr [2]int) { arr[0] = 512 } func main() { arr := [2]int{} fmt.Println("before call change(): ", arr[0]) change(arr) fmt.Println("after call change(): ", arr[0]) } // 输出： before call change(): 0 after call change(): 0 // 值未被改变然而C语言中，传递数组是指针传递：void change(int arr[]) { arr[0] = 512; } int main() { int arr[2] = {0}; printf("before call change(): %d\n", arr[0]); change(arr); printf("after call change(): %d\n", arr[0]); } // 输出： before call change(): 0 after call change(): 512 // 值被改变切片切片与数组的区别区别1：声明数组时，中括号写明了数组的长度或使用...自动计算长度，声明slice时，中括号内没有任何字符，如：var s = []int或者s := []int{}。区别2：array静态数组拥有固定的长度，达到容量限制之后就无法再添加元素；Go语言中的切片与Python的list概念很相似，可以通过扩容的方式突破容量限制（也可以认为因为扩容而使得容量值一直在改变）。区别3：作为函数的参数传递时，数组是值传递，切片为引用传递。创建切片的方式var方式var s = []int简短声明s := []int{}make方式s := make([]int, 3, 5) fmt.Printf("切片的长度：%d，切片的容量：%d\n", len(s), cap(s)) // 输出： 切片的长度：3，切片的容量：5make的第一个参数表示切片类型，第二个参数表示切片的长度，第三个参数表示切片的容量。第四种：对一个现有数组切片var arr = [5]int{1, 2, 3, 4, 5} s := arr[1:3:5]切片的分析切片由三部分组成：指针，长度，容量。指针指向slice对应的底层数组（中的某个元素），长度表示slice中一共存放了多少个元素，容量表示slice最多可以存放多少个元素。当slice中的元素数量超过容量时，会重新开辟一个大于当前元素数量的空间来存放现有元素。不妨先从元素个数大于容量开始说起：func main() { var s []int // 声明一个切片 fmt.Printf("切片的首地址：%p，切片的长度：%d，切片的容量：%d\n", s, len(s), cap(s)) s = append(s, 1, 2, 3) // 向切片里追加1，2，3 fmt.Printf("切片的首地址：%p，切片的长度：%d，切片的容量：%d\n", s, len(s), cap(s)) s = append(s, 4) fmt.Printf("切片的首地址：%p，切片的长度：%d，切片的容量：%d\n", s, len(s), cap(s)) s = append(s, 5) fmt.Printf("切片的首地址：%p，切片的长度：%d，切片的容量：%d\n", s, len(s), cap(s)) } // 输出 切片的首地址：0x0，切片的长度：0，切片的容量：0 切片的首地址：0xc0000580c0，切片的长度：3，切片的容量：4 切片的首地址：0xc0000580c0，切片的长度：4，切片的容量：4 切片的首地址：0xc000088100，切片的长度：5，切片的容量：8可以看到，当往切片里存放三个元素后（1，2，3），切片s的长度为3，容量为4；然后添加一个元素（4），s长度为4，容量为4，此时前后两次，s的地址是相同的。再往切片追加一个元素（5），超过了切片原有容量，于是s重新找一块地“占山为王”，新的“山头”可以容纳更多人，因此你看到了最后一次输出结果：s有了新的地址，也有了新的容量值。以上方式创建的切片会让我们察觉不到底层数组的存在，因此我们可以换一种方式创建切片来感受它与数组间的关联： arr := [5]int{1, 2, 3, 4, 5} s := arr[1:3:4] // 重点看这一句如果你会Python，你可能感到熟悉，但这并非我们熟悉的Python，这是Go语言。其中1表示从数组arr的下标为1的元素起（也就是说，切片的指针指向该数组的第二个元素）；3表示对数组arr的坐标取值到3-1的位置（这一点倒与Python的切片一样）；最后一个4表示设置切片的容量，容量为：4-1。因此这里有一个格式：s := arr[low:high:max]。切片s长度为：high - low，容量为：max - low。而max需要小于等于数组arr的长度。func main() { arr := [5]int{1, 2, 3, 4, 5} s := arr[1: 3: 4] fmt.Printf("数组第2个元素的地址：%p\n", &arr[1]) fmt.Printf("切片的首地址：%p，切片的长度：%d，切片的容量：%d\n", s, len(s), cap(s)) fmt.Printf("切片的内容：%v", s) } // 输出： 数组第2个元素的地址：0xc00006e068 切片的首地址：0xc00006e068，切片的长度：2，切片的容量：3 切片的内容：[2 3]切片作为底层数组引用的存在，能够影响数组的值。现在我们的切片s就引用了数组arr，改动s就是在改动arr：fmt.Println("数组：", arr) fmt.Println("切片：", s) s[0] = 512 // 改变切片 fmt.Println("数组：", arr) fmt.Println("切片：", s) // 输出： 数组： [1 2 3 4 5] 切片： [2 3] 数组： [1 512 3 4 5] 切片： [512 3]需要记住的是，只有在s还是arr的引用时才有这样的作用。比方如果往s添加元素以致超过了切片原有容量，切片会重新划分空间，s就不再是arr的引用，不会再影响arr：func main() { arr := [5]int{1, 2, 3, 4, 5} s := arr[1:3:4] fmt.Printf("数组的内容：%v\n", arr) fmt.Printf("切片的内容：%v\n", s) fmt.Printf("切片的首地址：%p，切片的容量：%d\n", s, cap(s)) s = append(s, 10,) // 切片追加1个元素，此时还未超过切片的容量 fmt.Println() fmt.Printf("数组的内容：%v\n", arr) fmt.Printf("切片的内容：%v\n", s) fmt.Printf("切片的首地址：%p，切片的容量：%d\n", s, cap(s)) s = append(s, 11,) // 再追加1个元素，超过切片原有容量 fmt.Println() fmt.Printf("数组的内容：%v\n", arr) fmt.Printf("切片的内容：%v\n", s) fmt.Printf("切片的首地址：%p，切片的容量：%d\n", s, cap(s)) } // 输出： 数组的内容：[1 2 3 4 5] 切片的内容：[2 3] 切片的首地址：0xc00000c368，切片的容量：3 数组的内容：[1 2 3 10 5] // 数组值被影响 切片的内容：[2 3 10] 切片的首地址：0xc00000c368，切片的容量：3 数组的内容：[1 2 3 10 5] // 数组值未被影响 切片的内容：[2 3 10 11] 切片的首地址：0xc00000c3f0，切片的容量：6切片的用法操作含义s[n]切片 s 中索引位置为 n 的元素s[:]从切片 s 的索引位置 0 到 len(s)-1 处获得的切片s[low:]从切片 s 的索引位置 low 到 len(s)-1 处获得的切片s[:high]从切片 s 的索引位置 0 到 high 处获得的切片, len=highs[low:high]从切片 s 的索引位置 low 到 high 处获得的切片，len=high-lows[low:high:max]从切片 s 的索引位置 low 到 high 处获得的切片，len = high-low, cap=max-lowlen(s)切片 s 的长度，总是<=cap(s)cap(s)]切片 s 的长度，总是>=len(s)切片的两个方法append：追加效果s = append(s, x, y, z, ...) // 切片 s 中追加x, y, z 等元素 s = append(s, n...) // 切片 s 中追加切片 n 中的元素copy：实现覆盖效果x_slice := []int{1, 2, 3} y_slice := []int{10, 20, 30, 40} copy(y_slice, x_slice) fmt.Println(y_slice) // 输出： [1 2 3 40]x_slice中三个元素覆盖掉了y_slice中的前三个元素。切片的其他说明切片仅允许与nil使用==、!=，即便是类型相同的两个切片之间也不能使用比较运算符；同类型的切片之间允许相互赋值（s1）。map键值对数据类型，与Python中的字典类型（dict_ = {}）相似。创建map的方式var方式var m map[int]string // 无自定义初始化值 var m = map[int]string{1:"bobby"} // 有自定义初始化值简短声明m := map[int]string{} m := map[int]string{1: "boby"}make方式m := make(map[int]string) m := make(map[int]string, 2) // 指定容量大小中括号（[]）内为键类型，中括号的右边为值类型。需要注意以下几点：不允许向一个nil值的map存入元素，会引发panic；var m map[int]string fmt.Println(m == nil) m[1] = "bobby" fmt.Println(m) // 输出： true panic: assignment to entry in nil map解决方法，用以下方式创建map:var m = map[int]string{} m := map[int]string{} m := make(map[int]string)当有m[1] = "boby"语句时，如果m中没有关键字1，就会动态创建一个关键字为1值为"boby"的键值。如果没有指定map容量，map可能会频繁扩容来满足添加元素的需求，效率会降低，所以建议用m := make(map[int]string, capValue)方式创建字典，指明对map的预期容量（尽管如此，却不能对map类型使用cap()方法）；map类型的键值应该是确定并且唯一。确定：不应该用切片、函数等作为键值；唯一：同一个map中，不会有相同的键值；map类型的变量作为参数实参传递时，用引用传递方式；map类型的变量内部是无序的（与Python字典无序一样）。判断键值是否存在func main() { m := map[int]string{ 0: "bobby", 1: "nike", } value, ok := m[1] // 重点 if ok { fmt.Printf("m[1]的值为：%v\n", value) fmt.Printf("ok 的值为：%v\n", ok) } else { fmt.Printf("不存在m[1]") } }如果键值1存在，则ok为true，否则false。删除键值对delete(m, 1)第一个参数为map类型的变量，第二个参数为键值。无论删除的键值是否存在m中，都不会引发panic。map的遍历for key, value := range m{ // ... }结构体定义一个结构体结构体的声明方式：type StructName struct { // ... }此时StructName是一种数据类型。放一个完整示例：func main() { type student struct { name string age int id uint64 } // 定义一个名为 student 的结构体 var s student // 声明一个 student 类型的变量 s s.name = "zty" s.age = 22 s.id = 201431060011 fmt.Println(s) } // 输出： {zty 22 201431060010}通过.操作访问成员变量。这与C语言中的结构体仅仅存在细微差异：int main() { typedef struct { char* name; int age; unsigned long long id; } student; student s; s.name = "zty"; s.age = 22; s.id = 201431060010; printf("%s %d %lld", s.name, s.age, s.id); } // 输出： zty 22 201431060010结构体变量的声明var s = student{"zty", 22, 201431060010} s := student{"zty", 22, 201431060010} s := new(student) // 注意，此时s是指针类型与C中结构体存在最大的差别是：Go语言中，指向结构体的指针访问成员时使用.而不是->C语言如下：student stu; // stu 是一个结构体 student* s = &stu; // s 是指向 stu 结构体的指针 s->name = "zty"; // `->` 的访问方式 s->age = 22; s->id = 201531060010;而Go语言：var stu student s := &stu s.name = "zty" s.age = 22 s.id = 201431060010 fmt.Println(s) // 输出： &{zty 22 201431060010} // 注意这个取地址符关于结构体变量初始化值也是有讲究的。要求顺序初始化必须对每个成员初始化：s := student{"zty", 22, 20140010} // 正确 s := student{"zty"} // 错误而指定成员初始化中，允许不对每个成员初始化（未初始化的成员默认0值）：s := student{age:22} fmt.Println(s) // 输出： { 22 0} // 这里面有三个值，第一个空字符串（name），第二个22（age），第三个0（id）结构体的其他说明允许结构体参与==和!=运算，但不能>或<；同类型的结构体变量可以指相互赋值（s1 = s2）；结构体作为函数实参传递时，使用值传递方式；结构体需要对外部可见时，无论是类型名还是成员名都必须首字母大写（type Student struct { Name string}）。随机数Go中获取随机数分两步骤：设置随机种子；获取随机数。import ( "fmt" "math/rand" "time" ) func main() { rand.Seed(time.Now().UnixNano()) // 设置随机种子 // time.Now().UnixNano() 表示系统时间，因为时间总是在变化，借此得到不同的随机种子 for i:=0; i < 3; i++ { fmt.Println(rand.Intn(100)) } }rand.Intn(n)表示在0~n范围内获取随机数。
前言这是我学习Go语法的笔记。由于有C和Python的基础，上手Go很快。笔记很粗糙，好在自己够用。此篇包括了Go相关的：函数，闭包，类型命名，项目管理等。涉及关键字type defer。函数无参无返回值函数func myfun() { fmt.Println("this is a func") }被调函数放在调用函数的前后都允许，不需要像C那样放在后面需要前置声明。有参无返回值函数func myfun(str string) { fmt.Println(str) } // 形参同类型时 func myfun(str1, str2 string) { // ... } // 形参不同类型时 func myfun(num int, str string) { // ... }需要在参数列表中指明参数的类型。与C语言的区别是：形参名在前，类型在后。不定长参数函数func myfun(str ...string) { // ... }在函数类型前加美式省略号（...）可实现不定长参数传参。func myfun(str ...string) { for _, s := range str { fmt.Printf("%s\n", s) } } func main() { myfun("hello", "guan", "ying") } // 输出 hello guan ying需要注意的是：当参数列表中有多个参数时，不定长参数必须在固定参数的后面。func myfun(num int, str ...string) {/* ... */} // 对 func myfun(str ...string, num int) {/* ... */} // 错当我们需要把一个函数中的不定长参数传给另一个函数时，也需要用到...：func herfun(num ...int) { for _, n := range num { fmt.Println(n) } } func myfun(num ...int) { herfun(num...) // 如果只需要传最后两个参数，可以 // herfun(num[1:]...) } func main() { myfun(1, 2., 3) }有返回值函数一般来说，让人习惯的用法是：func myfun() int {/* ... */} // 表示return了一个int类型 func myfun(str string) (byte, byte, byte) {/* ... */} // 表示return了三个byte类型此时返回的格式与Python格式相同。如第一个函数可以return 512；第二个函数可以return 'z', 't', 'y'。像下面这样：func myfun(str string) (byte, byte, byte) { return str[0], str[1], str[2] } func main() { str := "zty" z, t, y := myfun(str) fmt.Printf("%c, %c, %c", z, t, y) }但是，这并不是官方推荐的返回方式。官方更建议一种酷炫、也更一目了然的方法——返回列表中写明返回的变量的名字：func myfun(str string) (z byte, t byte, y byte) { z, t, y = str[0], str[1], str[2] return }看仔细！在myfun()函数中，我们并没有声明变量z、t、y，但它们确实、已然可用。而return的时候，return就够了。自然与参数列表一样，当返回值类型相同时我们可以简化代码：func myfun(str string) (z , t, y byte) {/* ... */}type变量类型关键字type可以为数据类型重命名，让类型有了语义。func main() { type alpha byte // 类型alpha的底层数据类型是byte var z alpha = 'z' fmt.Printf("%c 的类型是 %T", z, z) } // 输出 z 的类型是 main.alpha如果我们用alpha来声明一个变量，我们很显然可以知道这个变量企图存放一个字母。这里需要说明一下，Python中获取一个变量的类型通过type()函数；而Go语言是通过格式占位符%T来实现。通过type命名的数据类型默认有一个方法，可以显式地转换变量的类型：func main() { type positive string type negative string var pWords positive = "beautiful" var nWords negative = "ugly" fmt.Printf("%s是%T类型的词语\n", pWords, pWords) fmt.Printf("%s是%T类型的词语\n", nWords, nWords) // 输出： // beautiful是main.positive类型的词语 // ugly是main.negative类型的词语 fmt.Printf("%s是%T类型的词语\n", nWords, positive(nWords)) // 显式地转换类型 // 输出： // ugly是main.positive类型的词语 }这样的转换并非万能，只在允许的情况下使用。比如字符串类型就不能转整数型。所以基于这两个类型之上的类型的变量就不可以相互显式转换：type ( str string num int ) var greet str = "hello" fmt.Printf("%d\n", num(greet)) // 错误的使用方式报错：cannot convert greet (type str) to type num。函数类型Go语言也允许对一个函数类型命名，通过这种方式可以实现多态。具体使用方式如下：func add(a int, b int) int { return a + b } func sub(a int, b int) int { return a - b } func main() { type ComputeFunc func(int, int) int // 格式：type 类型名 函数类型 var compfun ComputeFunc compfun = add // 此时compfun()当于add()函数 resultAdd := compfun(10, 20) fmt.Printf("result = %d\n", resultAdd) // 输出：result = 30 compfun = sub // 此时compfun()相当于sub()函数 resultSub := compfun(10, 20) fmt.Printf("result = %d\n", resultSub) // 输出：result = -10 }匿名函数与闭包Go语言的匿名函数与JS的相似。不得不说，功能强大。然而Python的匿名函数虽然有些弱鸡，但胜在优雅。我这种外貌协会更偏爱Python风。匿名函数像下面这样：func main() { resutl := func (a int, b int) int { return a + b } (10, 20) fmt.Printf("%d\n", resutl) }上述代码的坏处是匿名函数会立即执行，有时候我们期望它能够在我们需要的时候执行——虽然我们早早就定义好了。解决方式是丢弃匿名函数最后的“小尾巴”：func main() { f := func (a int, b int) int { return a + b } fmt.Printf("我还不想执行1\n") fmt.Printf("我还不想执行2\n") fmt.Printf("现在想执行了，那就执行吧！\n") result := f(12, 21) // 执行匿名函数 fmt.Printf("%d\n", result) } // 输出： 我还不想执行1 我还不想执行2 现在想执行了，那就执行吧！ 33所以匿名的两种格式：f := func () int {} // 不会立即执行 f() // 此时才执行 // ---------分割线-------------- func () int {} () // 立即执行Python的匿名函数与闭包大多时候是一种概念，只是关键字lambda的存在让其看起来不一样。而Go语言里二者的“外貌”比较统一。区别这个函数是不是闭包的要点在于：这个函数是否使用了与它同一作用域的变量或常量。func main() { alpha := 'z' num := 512 func() { alpha = 't' num = 1024 }() // alpha 与 num 值已经发生了改变 fmt.Printf("alpha = %c, num = %d\n", alpha, num) // 输出：alpha = t, num = 1024 }Python处理上面状况的时候存在一些问题。Py2不允许闭包函数直接修改外界变量的值，而Py3中，当你企图修改外界变量时需要用nonlocal关键字说明：def main(): alpha = "z" num = 512 def package(): nonlocal alpha, num # 对变量进行说明 alpha = "t" num = 1024 package() print("alpha = %s, num = %d" % (alpha, num)) # 输出：alpha = t, num = 1024 main()闭包的另一个特点是不会被它的作用域限制，只要闭包还在使用，它引用的变量就会一直存在：func myfun() func() int { var x int return func() int { x++ return x * x } } func main() { f := myfun() fmt.Println(f()) // 输出：1 fmt.Println(f()) // 输出：4 fmt.Println(f()) // 输出：9 // 尽管myfun()的生命周期已经结束 // 但储存其闭包的内存空间还在 }defer常规使用特点：（1）延迟调用（2）无论程序发生什么错误，总会执行发生错误前的defer修饰的语句（3）多个defer语句的执行顺序是先进后出。一、延迟调用：package main import "fmt" func main() { fmt.Println("start ...") defer fmt.Println("middle ...") fmt.Println("end ...") } // 输出： start ... end ... // 注意执行顺序 middle ...二、无论程序发生什么错误，总会执行发生错误前的defer修饰的语句：func mod(x int) (result int) { result = 100 / x return } func main() { defer fmt.Println("start ... ") result := mod(0) fmt.Println(result) defer fmt.Println("end ... ") } // 输出： start ... panic: runtime error: integer divide by zero // 没有执行 `fmt.Println("end ... ")` 语句三、多个defer语句的执行顺序是先进后出func main() { defer fmt.Println("start ... ") defer fmt.Println("end ... ") } // 输出： end ... start ... // 注意执行顺序与匿名函数的使用无传参示例：func main() { a, b := 10, 20 defer func() { fmt.Printf("a = %d, b = %d", a, b) // 输出：a = 100, b = 200 } () a, b = 100, 200 // 第二次赋值 }根据输出值可推断匿名函数是在第二次赋值之后才被调用的。传参示例：func main() { a, b := 10, 20 defer func(a int, b int) { fmt.Printf("a = %d, b = %d", a, b) // 输出：a = 10, b = 20 } (a, b) a, b = 100, 200 }可见，对匿名函数传参时，匿名函数会绑定当时当刻的变量的值（闭包），而不是需要时才获取。这样做可以保护变量不受外界污染。在Python中，想达到此目的会有些麻烦，需要两层嵌套：def main(): a, b = 10, 20 def outer(a, b): def inner(): print("a = %d, b = %d" % (a, b)) return inner f = outer(a, b) a, b = 100, 200 f() main() # 输出： a = 10, b = 20作用域花括号可以隔离变量：func main() { { num := 1024 } fmt.Println(num) } // 输出（程序报错）： undefined: num这跟C语言是如出一辙的。导包Go语言中导入包有四种操作。普通导入package main import "fmt" func main() { fmt.Println("this is a test") }通过这种方式导包后需要注意两点：第一点，如果需要用到这个包里的变量或函数，需要加上包名，如上面代码所示fmt.；第二点，必须用到fmt包中的变量或者函数至少一次，否则编译器报错。点操作package main import . "fmt" func main() { Println("this is a test") }类似Python中的from os import *。此时使用fmt包中的内容需要省略包名。重命名package main import mypen "fmt" func main() { mypen.Println("this is a test") }类似Python中的import os as xxx。_下划线操作package main import _ "fmt" func main() { }Go语言中要求，导入的包、定义的变量必须被使用，否则不能通过编译器，此时下划线替我们解决了导入包必须被使用的烦恼。如上示代码，即便导入了fmt包，但可以不使用，编译器不会报错。更多时候这样做的目的是为了自动调用导入包的init()函数。项目管理实际编程时我们需要将代码写在多个.go的源文件中，这时候需要知道Go语言是如何进行项目管理的（比Python稍稍复杂）。要求一：代码必须放在src目录下。要求二：src的父目录必须添加到GOPATH中（可用命令go env进行查看）。要求三：一个程序必须有一个main包。同目录下的多个源文件同目录下的多个源文件要求必须属于同一个包，也就说package xxx中的xxx应该是相同的。此时在目录D:\MyCode\GoCode\ImportPkg\src下创建两个文件：main.go和test.go。由于一个程序必须有一个main包，而test.go与main.go在同一目录，所以两个文件都应该属于main包。// main.go package main func main() { greet() // 直接使用test.go中的greet()函数 }// test.go package main import "fmt" func greet() { fmt.Println("hello world.") }终端使用命令：go run . 或go build .编译整个包的文件。同一个包里的函数可以直接使用。不需要导入，也不要求首字母大写。不同目录下的多个源文件不同目录的源文件可以属于不同包。同时，如果需要使用别的目录下的源文件中的函数，必须导入所属包。且使用的函数必须首字母大写。// main.go package main import "demo" func main() { demo.Greet() }// demo/test.go package demo import "fmt" func Greet() { fmt.Println("hello world.") }目录结构如下：D:\MyCode\GoCode\ImportPkg\src: |——main.go |——demo |——test.gogo install的使用在src的同级目录中创建bin目录，并添加到系统变量中，此时在src目录下执行命令go install，会在bin目录下生成可执行文件。看到网上说同时还会生成pkg目录以及该目录下的相关文件，但我的没有，暂时存疑。
前言这是我学习Go语法的笔记。由于有C和Python的基础，上手Go很快。笔记很粗糙，好在自己够用。此篇包括了Go相关的：变量声明，分支语句（if,switch），循环语句(for)。涉及关键字var const fallthrough goto range 等。简单示例// go语言以包作为管理单位 package main // 声明包 import "fmt" // 导入包 func main() { // 左括号必须与函数名同行 fmt.Println("hello world") }注意事项：每个文件必须声明包；程序必须有一个main包。声明变量声明变量的四种方式：func main() { var a int a = 512 var b int = 513 // 此时类型可以省略，就像声明c一样 var c = 514 d := 515 // 简短声明，让编译器为d自动推导类型 fmt.Println("a =", a) // 打印语句，类似Python中的print fmt.Println("b =", b) fmt.Println("c =", c) fmt.Println("d =", d) } // 输出： a = 512 b = 513 c = 514 d = 515同时多个同类型变量的声明：// 第一种 var a int var b int // 第二种 var a, b int如果多个变量类型不同，可以：var ( a int b int c float64 ) // 或者赋初值 var ( a int = 512 b int = 1024 c float64 = 3.14 )Go语言中，每个声明的变量如果未被初始化，编译器默认为其设置0值。每个类型都有其对应的0值，但不一定是0。func main() { var num int // int类型 var str string // 字符串类型 var p *int // int指针类型 fmt.Println("a =", num) // 输出0 fmt.Println("str =", str) // 输出空字符串 fmt.Println("p =", p) // 输出nil } // 输出： a = 0 str = p = <nil>nil相当于Python中的NULL。声明常量关键字const承担常量声明的职责，与var声明变量的用法相同。const num = 512 // 或者 const num int = 512注意以下几点：常量必须在声明的时候赋值；常量没有简短声明方式（:=）；常量一旦声明，就不可以赋值，否则报错：cannot assign to num 。iota枚举iota为每个常量自动赋值（从0开始），每次赋值后自动累加1。func main() { const ( a = iota b = iota c = iota ) fmt.Printf("a = %d b = %d c = %d", a, b, c) } // 输出 a = 0 b = 1 c = 2多个同一行的变量值相同：func main() { const ( a = iota b, c, d = iota, iota, iota e = iota ) fmt.Printf("a = %d b = %d c = %d d = %d e = %d", a, b, c, d, e) }也可以简化代码，只开头写一个iota。此时不允许一行多个变量：const ( a = iota b c )iota遇到const会被重置为0func main() { const ( a = iota b ) const c = iota fmt.Printf("a = %d b = %d c = %d", a, b, c) } // 输出： a = 0 b = 1 c = 0基础类型布尔类型Go语言中布尔值的false和true全小写，而Python是首字母大写：func main() { var flag bool // 声明 fmt.Println("flag =", flag) var mark = true // 声明的同时初始化 fmt.Println("mark =", mark) // 或者 // mark := true } // 输出 flag = false mark = true字符型Go语言中，用单引号表示一个字符 （byte)，同时字符类型以数值的方式存储：func main() { var c byte = 'z' fmt.Println("c =", c) } // 输出 c = 122如果希望字节正常输出，可以用fmt.Printf：fmt.Printf("c = %c", c) // %c 是字节占位符由于字节类型以数值方式存储，所以可以进行加减乘除运算：func main() { var c byte = 'z' fmt.Println("c =", c) fmt.Println("c + 1 =", c + 1) fmt.Println("c - 1 =", c - 1) fmt.Println("c / 2 = ", c / 2) fmt.Println("c * 2 =", c * 2) } // 输出 c = 122 c + 1 = 123 c - 1 = 121 c / 2 = 61 c * 2 = 244字符串类型字符串（string）双引号表示：str := "hello world"Go语言中，'a'与"a"是存在区别的（Python中没有区别）。事实上"a"由两个字符组成：'a'+'\0'。一个字符串可以通过索引的方式来访问：func main() { str := "hello world" fmt.Printf("%c, %c", str[0], str[10]) } // 输出 h, d甚至可以像Python那样使用切片：func main() { str := "hello world" fmt.Printf("%s", str[:5]) } // 输出 hello输入输出fmt.Scan()func main() { var str string fmt.Scan(&str) // 无需格式占位符 fmt.Println(str) }fmt.Scanf()func main() { var str string fmt.Scanf("%s", &str) // 需要格式占位符 fmt.Println(str) }fmt.Println()类似Python中的print()函数。fmt.Printf()类似C语言中的printf()函数。分支语句if同其他语言一样，Go语言也有if语句，中规中矩（仅仅是不需要小括号，Python对此也是允许的）：func main() { num := 512 if num == 512 { fmt.Println("num的值为512") } }还有if...else if ... else：func main() { var num int fmt.Scan(&num) if num == 512 { fmt.Println("num的值为512") } else if num > 512 { fmt.Println("num的值大于512") } else { fmt.Println("num的值小于512") } }俏皮的是，Go语言中的if语句支持初始化一个语句（if 初始化语句; 条件 {...}）比如：func main() { var num int fmt.Scan(&num) if tmp:= 512; num == tmp { fmt.Println("num等于512") } }switchGo语言中的swicth与C语言中的switch有一些区别。Go语言中，case的结尾默认带有break，无需用户自己添加：func main() { num := 1 switch num { // 不需要break case 0: fmt.Println("num等于0") case 1: fmt.Println("num等于1") // 进入`case 1`之后执行Println，然后直接跳出switch case 2: fmt.Println("num等于2") default: fmt.Println("num等于其他") } } // 输出 num等于1也可以是其他类型，如字符串变量：func main() { str := "hello world" switch str { // 不需要break case "bye~bye": fmt.Println("bye~bye") case "hello": fmt.Println("hello") case "hello world": fmt.Println("hello world") default: fmt.Println("没有匹配字符串") } } // 输出 hello world通过添加fallthrough关键字，使代码进入case之后不要直接跳出switch：func main() { num := 1 switch num { // 不需要break case 0: fmt.Println("num等于0") fallthrough case 1: fmt.Println("num等于1") fallthrough case 2: fmt.Println("num等于2") fallthrough default: fmt.Println("num等于其他") } } // 输出 num等于1 num等于2 num等于其他与C中swicth最大区别，在于条件可以写在case后面：func main() { var num int fmt.Scan(&num) switch { // 省略变量 case num > 512: // 加上条件 fmt.Println("num大于512") case num == 512: fmt.Println("num等于512") default: fmt.Println("num小于512") } }循环语句Go语言只有for循环，没有while，用法如下：func main() { for num:=0; num < 3; num++ { fmt.Println(num) } }可以省略第一个表达式，但需要保留分号;：func main() { num := 0 for ; num < 3; num++ { fmt.Println(num) } }事实上for可以当作while来使用：num := 0 for num < 3 { num++ fmt.Println(num) }如果什么都不加，就是死循环：for { fmt.Println("这是一个死循环。") }关键字range实现迭代效果，如：func main() { str := "hello" // 第一个元素是索引值，第二个是索引出来的内容 for i, alpha := range str { fmt.Printf("num = %d, alpha = %c\n", i, alpha) } }上述代码中alpha可以省略，只需要获取索引值即可：func main() { str := "hello" for i := range str { fmt.Printf("num = %d, alpha = %c\n", i, str[i]) } }这跟Python中的enumerate()函数有些相似。跳转语句如同C语言，Go语言也有goto关键字，用法相同：func main() { fmt.Println("start ...") goto End // 执行到这儿之后不会执行下面的代码，而是执行End之后的代码 fmt.Println("first end ...") End: fmt.Println("second end ...") } // 输出 start ... second end ...goto不能跨函数使用。