Go
本笔记用于记录在阅读Go程序设计语言的一些重要的知识点!并不完全!
第一章(入门)
命令行参数
在Go语言中可以直接go run 文件,也可以go build后会生成一个可执行文件,直接使用该可执行文件可以运行go文件。
#第一种
go run file.go
#第二种
go build file.go
./file
下面用一个程序来理解如何获得命令行参数
package chapter1
import (
"fmt"
"os"
)
/**
该程序用来模拟linux的echo,输入后面带的参数
*/
func main() {
args := os.Args
var s string
for i := 1; i < len(args); i++ {
s += args[i] + " "
}
fmt.Println(sy
}
运行效果
PS D:\goTrip\chapter1> go build .\echo1.go
PS D:\goTrip\chapter1> .\echo1 aa bb cc
aa bb cc
补充(for range)
上面的代码没有任何问题,但是go语言的for循环其实不止这个用法,他和java不同,不用死板的i++,而是可以像java的for(:)这样。
首先我们得直到os.Args是一种string的切片,由于要实现函数的功能,我们得从index=1开始遍历,所以我们只用取切片的[1:],现在来实现函数。
package main
import (
"fmt"
"os"
)
/**
该程序用来模拟linux的echo,输入后面带的参数
*/
func main() {
args := os.Args
var s string
for _, val := range args[1:] {
s += val + " "
}
fmt.Println(s)
}
前面的下划线是什么?
这个在go中代表忽略该值,因为go语言创建了一个值但是不使用,会报错,用了_,就可以不用也不会报错。
被省略的是index!
补充(i++)
go中的i++对i+1,他等价于i += 1,也等价于i = i + 1.但是这些是语句,并不是表达式,所以j = i++不是合法语句。
优化(echo1)
在上面我们是通过循环通过追加旧的字符串来拼接命令行参数的,会将新的内容赋值给s,然后旧的内容垃圾回收。如果有大量的数据处理,这种代价会比较大,一种简单并且高效的方式。
/**
该程序用来模拟linux的echo,输入后面带的参数
*/
func main() {
args := os.Args
join := strings.Join(args[1:], " ")
fmt.Println(join)
}
找出重复行
有点像unix的dup,输出输入中次数大于1的行。
方式1(map+scanner)
第一个实现方式使用map+scanner实现。
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
record := make(map[string]int)
s := bufio.NewScanner(os.Stdin)
for s.Scan() {
record[s.Text()] += 1
}
for k, v := range record {
if v > 0 {
fmt.Printf("dup line is %s\n", k)
}
}
}
scanner是bufio包下的扫描器,它可以读取输入,以行或者单词为单位断开。有点像java的scanner,简直一毛一样。
方式2(流)
第二个实现方式用流的方式读入,并且可以读入文件或者键盘键入的文字。(通过命令行参数控制)
大概使用一个count函数,参数1是*File,参数2是记录的map
package main
import (
"bufio"
"fmt"
"io"
"os"
)
func main() {
args := os.Args
record := make(map[string]int)
//若长度为1,说明后面没有追加文件名字
if len(args) == 1 {
count2(os.Stdin, record)
} else {
for _, v := range args[1:] {
open, err := os.Open(v)
if err != nil {
continue
}
count1(open, record)
//记得关闭!!!
open.Close()
}
}
for k, v := range record {
if v > 1 {
fmt.Printf("%s:%d\n", k, v)
}
}
}
func count1(stream *os.File, record map[string]int) {
reader := bufio.NewReader(stream)
for {
line, _, err := reader.ReadLine()
if err == io.EOF {
break
}
record[string(line)] += 1
}
return
}
func count2(stream *os.File, record map[string]int) {
reader := bufio.NewScanner(stream)
for reader.Scan() {
if reader.Text() == "exit" {
break
}
record[reader.Text()] += 1
}
return
}
获得多个URL
用http来get请求即可。
package main
import (
"fmt"
"io/ioutil"
"net/http"
"os"
)
func main() {
for _, url := range os.Args[1:] {
get, err := http.Get(url)
if err != nil {
fmt.Printf("err: %v\n", err)
return
}
body := get.Body
all, err := ioutil.ReadAll(body)
if err != nil {
fmt.Printf("err: %v\n", err)
return
}
fmt.Println(string(all))
}
}
小作业1,用io.copy将b文件读入到控制台
package main
import (
"fmt"
"io"
"net/http"
"os"
)
func main() {
for _, url := range os.Args[1:] {
get, err := http.Get(url)
if err != nil {
fmt.Printf("err: %v\n", err)
return
}
body := get.Body
io.Copy(os.Stdout, body)
if err != nil {
fmt.Printf("err: %v\n", err)
return
}
}
}
并发获得多个URL
管道是用来go协程间通信的,这个管道很有意思,他默认需要两个协程操作,比如一个协程对他进行数据操作了,他会阻塞直到另一个协程对他进行数据的写入或者读取。
package main
import (
"fmt"
"io"
"net/http"
"os"
"time"
)
func main() {
c := make(chan string)
start := time.Now()
for _, url := range os.Args[1:] {
go fetch(url, c)
}
//如果main先到则会阻塞等待,如果go协程先到则会将数据写入channel,如果main没取,也会阻塞
for range os.Args[1:] {
fmt.Println(<-c)
}
since := time.Since(start)
fmt.Printf("%.2f has passed", since.Seconds())
}
func fetch(url string, c chan string) {
now := time.Now()
get, err := http.Get(url)
if err != nil {
c <- fmt.Sprint(err)
fmt.Printf("error,err:%v\n", err)
}
byte, err := io.Copy(io.Discard, get.Body)
get.Body.Close()
if err != nil {
c <- fmt.Sprint(err)
fmt.Printf("error,err:%v\n", err)
}
since := time.Since(now).Seconds()
c <- fmt.Sprintf("%.2f %7d %s", since, byte, url)
}
一个Web服务器
我们这边通过http的listen方法,启动一个服务器
package main
import (
"fmt"
"net/http"
)
func main() {
//路由
http.HandleFunc("/", handler)
//监听一个端口
http.ListenAndServe("localhost:8000", nil)
}
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "url=%v\n", r.URL.Path)
}
PS D:\goTrip\chapter1> go build .\serve.go
PS D:\goTrip\chapter1> ./serve
PS D:\goTrip\chapter1> ./fetch http://localhost:8000/
url=/
可以看到我们上面发送的get请求,我们的服务器成功的收到了。
计数器
我们为这个服务器加上了一个计数器
package main
import (
"fmt"
"net/http"
"sync"
)
var count int
var lock sync.Mutex
func main() {
//路由
http.HandleFunc("/", handler)
http.HandleFunc("/count", counts)
//监听一个端口
http.ListenAndServe("localhost:8000", nil)
}
func handler(w http.ResponseWriter, r *http.Request) {
lock.Lock()
count++
lock.Unlock()
fmt.Fprintf(w, "url=%v\n", r.URL.Path)
}
func counts(w http.ResponseWriter, r *http.Request) {
lock.Lock()
fmt.Fprintf(w, "count=%v\n", count)
lock.Unlock()
}
第二章(程序结构)
flag包
有以下两种常用的定义命令行flag
参数的方法。
flag.Type()
基本格式如下:
flag.Type(flag名, 默认值, 帮助信息)*Type
例如我们要定义姓名、年龄、婚否三个命令行参数,我们可以按如下方式定义:
name := flag.String("name", "张三", "姓名")
age := flag.Int("age", 18, "年龄")
married := flag.Bool("married", false, "婚否")
delay := flag.Duration("d", 0, "时间间隔")
需要注意的是,此时name
、age
、married
、delay
均为对应类型的指针。
flag.TypeVar()
基本格式如下: flag.TypeVar(Type指针, flag名, 默认值, 帮助信息)
例如我们要定义姓名、年龄、婚否三个命令行参数,我们可以按如下方式定义:
var name string
var age int
var married bool
var delay time.Duration
flag.StringVar(&name, "name", "张三", "姓名")
flag.IntVar(&age, "age", 18, "年龄")
flag.BoolVar(&married, "married", false, "婚否")
flag.DurationVar(&delay, "d", 0, "时间间隔")
通过以上两种方法定义好命令行flag参数后,需要通过调用flag.Parse()
来对命令行参数进行解析。
支持的命令行参数格式有以下几种:
-flag xxx
(使用空格,一个-
符号)--flag xxx
(使用空格,两个-
符号)-flag=xxx
(使用等号,一个-
符号)--flag=xxx
(使用等号,两个-
符号)
代码
package main
import (
"flag"
"fmt"
"strings"
)
var (
n = flag.Bool("n", false, "忽略结尾的换行符")
sep = flag.String("sep", " ", "替换输出参数时候用的分隔符")
)
func main() {
flag.Parse()
fmt.Print(strings.Join(flag.Args(),*sep))
if !*n{
fmt.Println()
}
}
可以发现我们使用flag所解析的参数*,因为flag.Bool和flag.String是返回的指针,我们需要取指针的值要用到 *
new
new用于产生某一个类型的指针,他是预定义的函数,不是关键字,所以如果在某一函数指定了名为new的变量,我们就用不了new函数了。
func demo(new int){
//用不了new
}
变量的生命周期
如果是包范围的变量,他的生命周期和我们程序的生命周期一样。
如果是局部变量,它从被声明创建开始,直到无法被访问。垃圾回收器怎么知道它是否无法被访问呢?有点像Java的GC Root,将每一个包级别的变量和每一个函数的局部变量,都当成一个源头,以该通过指针和其他引用方式找到变量,如果变量没有路径能够到达,说明无法被访问。
即分析变量的可到达性。
变量逃逸分析
在go语言中,一般没有发生逃逸的话,局部变量会申请栈空间,包级别的变量会申请堆空间,但是函数内的局部变量可能发生逃逸。
例如下面的情况,最终go会通过逃逸分析将d分配到堆空间上。
type Data{
//...
}
func escape() *Data{
d := &Data{}
return d
}
包的别名
可以在函数上方对包进行一个别名,如下方例子,虽然Celsius和Fahrenheit底层都是Float64,但是他们是无法相互转换的。
也不能一起计算,得做类型转型。
package main
//摄氏度
type Celsius float64
//华氏度
type Fahrenheit float64
const(
AbsoluteZero Celsius = -273.15
Freezing Celsius = 0
Boiling Celsius = 100
)
func CToF(c Celsius) Fahrenheit{
//不可以
//var f Fahrenheit = Freezing
return Fahrenheit(c*9/5 + 32)
}
func FToC(f Fahrenheit) Celsius{
return Celsius((f-32)*5/9)
}
func main() {
}
作用域
一般会分为系统预留的值比如int,true,包级别:别入在函数外定义的var,局部:函数内或者语句块内定义的。
go在查找变量的引用,会从内层(局部->系统)去查找,所以如果函数内定义了一个val,但是又有一个全局的val会优先使用局部变量。
对于for循环很有意思,他会创建两个语法块,一个是循环体的显示语法块,一个是隐式块,包含了初始化的变量i,i++等。
所以他会出现一个很奇怪的现象,就是一个for里面可能有两个i
作用域也会导致许多奇怪的问题,比如有一个包级别的属性叫val,我们调用某个函数也会得到val,详细见下图。
这里我们其实想让全局的val初始化,但是err和val都没在函数内声明,只能用:=,不然err会爆红,表示找不到这个err,此时我们可以这样
第三章(基本数据)
整数
Go具有有符号整数和无符号整数。并且每个整数都有8,16,32,64位,分别对应int8,int16,int32,int64(无符号整数 uint8,uint16,uint32,uint64).
对于n位有符号整数,他的取值范围位-(2^(n-1) - 1) ~ 2^(n-1) - 1.
即对于int8他的取值范围是-128~127
对于n位无符号整数,他的取值范围是0~2^n - 1
即对于uint8他的取值范围是0~255
由于整数有他的数值范围,所以完全是可能溢出的,我们以范围最小的uint8来演示一下溢出的情况。
位运算
直接看程序吧。
package main
import "fmt"
func main() {
var x uint8 = 1<<1 | 1<<5 //"10001"
var y uint8 = 1<<1 | 1<<2 //"00011"
fmt.Printf("%08b\n", x)
fmt.Printf("%08b\n", y)
fmt.Printf("%08b\n", x&y)
fmt.Printf("%08b\n", x|y)
fmt.Printf("%08b\n", x^y)
//就等同于x&(^y)
fmt.Printf("%08b\n", x&^y)
for i := uint8(0); i < 8; i++ {
//统计x的二进制形式有几个1
if (x>>i)&1 != 0 {
fmt.Println(i)
}
}
}
关于go的一些内置函数为什么返回有符号整数
比如go的len函数就是返回有符号整数,为什么要这么做,因为我们知道uint的数值范围是0~?,所以按道理来说无符号整数应该恒大于等于0,那在开发中我们经常会写出这种代码。
for i := len(list);i >= 0;i--{
//logic
}
假设返回的是uint,那么i也随之变成uint,那i–一辈子不可能小于0,会陷入死循环。因此无符号整数常常只用于位运算或者算术运算符。
浮点数
go中支持两种大小的浮点数float32,float64.
字符串
字符串是不可变的字节序列。它可以包含任何数据,包括零值字节。
例如下面的代码,可以看到虽然str改变了,但是t持有的str的旧值仍没有改变
由于str不可变,所以str内部的值也不能变
不可能出现str[0] = '0’这种情况。
字符串字面量
字符串的值可以直接写成字符串字面量,形式上就是代双引号的字节序列。
string的几个标准包
4个标准包对字符串操作特别重要:bytes,strings,strconv,unicode
byetes包内有用于操作字节slice的方法。由于字符串不可变,因此按增量方式构建字符串会导致多次内存分配,用bytes.Buffer会更高效。
strconv包含将字符串转换的函数,课转换为布尔值,整数,浮点数等函数。
unicode具有判别文字符号值特性的函数,比如isDigit(是否是数字),isLetter(是否是字母)
strings包中具有一些操作字符串的函数,类似于bytes。
basename demo
首先不借助任何库。
func basename(str string) string {
//从后往前找到最后一个/
for i := len(str) - 1; i >= 0; i-- {
if str[i] == '/' {
str = str[i+1:]
break
}
}
var index int
for i := len(str) - 1; i >= 0; i-- {
if str[i] == '.' {
index = i
break
}
}
return str[:index]
}
借助lastIndex
func basename1(str string) string {
//从后往前找到最后一个/
index := strings.LastIndex(str, "/")
end := strings.LastIndex(str, ".")
return str[index+1 : end]
}
对每三个数字就插入一个,
不用库函数,递归
package main
import "fmt"
func main() {
fmt.Println(foo("123456789"))
}
func foo(str string) string {
if len(str) <= 3 {
return str
}
return str[:3] + "," + foo(str[3:])
}
模拟打印数组
有点像java的数组的tostirng,在这里是用bytes的buf接受字符串,[]和数字,这里值得注意的是fpringf的writer得传一个指针。+
package main
import (
"bytes"
"fmt"
)
func intsToString(arr []int) string {
var buf bytes.Buffer
buf.WriteString("[")
for i, v := range arr {
if i > 0 {
buf.WriteString(", ")
}
fmt.Fprintf(&buf, "%d", v)
}
buf.WriteString("]")
return buf.String()
}
func main() {
ints := make([]int, 0, 3)
ints = append(ints, 1)
ints = append(ints, 2)
ints = append(ints, 3)
fmt.Println(intsToString(ints))
}
字符串转数字
数字—》字符串
i := 1
s1 := fmt.Sprintf("%d", i)
s2 := strconv.Itoa(i)
字符串–》数字
atoi, err := strconv.Atoi("123")
parseInt, err := strconv.ParseInt("123", 10, 64)
parseInt第二个参数代表进制,第三个参数代表位数。
第四章(复合数据类型)
数组
数组的长度是固定的,所以在go中很少使用数组。更多的会使用切片。
var a [3]int
var a [3]int = {1,2,3}
a := [...]int{1,2,3}
这里可以注意,如果此时用print(“%T\n”,a) 得到的类型会是[3]int,但是[3]int和[4]int是不一样的。
如果数组的元素是可以比较的,那么数组也是可以比较的。
因为数组如果容量过大,作为方法的参数,会复制一个副本,这是十分消耗资源的。此时可以用指针,传的就是引用。
切片
切片是一种可变长度的序列。切片中的类型是唯一的,例如[]T,切片中都是T类型。
切片有三个属性,指针,长度,容量。长度是指slice中的元素的个数,它不能大于容量。容量是一般是第一个到最后一个元素的长度。
但是值得注意的是切片不能用==来比较,对于字节切片,可以直接调用bytes.equal,但是其他的就需要我们自己写函数来比较了。
对于切片,如何判断是一个空的切片,要用len(切片),如果用切片==nil,会发生一种情况,比如刚刚make出来的切片,此时是没有元素的,但是不等于nil,他本来是空的。
通过make创建切片,可以有两种方法。
make([]int,len)
make([]int,len,cap)
对于第一种返回的是整个数组的引用,而第二种是数组中len元素的引用,但是cap会为以后的slice留出空间。
append
growslice
首先将当前容量进行一个double,判断是否大于旧容量。
若小于,则说明没有初始化,所以直接将newCap赋值给数组即可。
如果当前的容量小于1024,会进行一个double,否则会进行一个1+1/4的扩容。
由于我们调用append并不知道是否会发生内存重分配,即并不知道是返回的原数组还是新数组,所以我们一般使用append函数都会将返回的切片赋值给原来的切片。
这句话可以由下面代码很清楚的看出来,下面代码大概是取出一个string切片的所有空字符串
所以建议用下面这种方式
或者直接操作切片
remove掉切片中index的值
可以用copy直接覆盖掉index后的值,然后返回len-1的切片即可,详细可以看代码
func remove(s []string, i int) []string {
copy(s[i:], s[i+1:])
return s[:len(s)-1]
}
reverse
package main
import "fmt"
func reverse(arr *[]int) {
A := *arr
l1 := len(*arr) - 1
l := len(*arr)/2 - 1
for i := 0; i <= l; i++ {
A[i], A[l1-i] = A[l1-i], A[i]
}
}
func main() {
arr := []int{1, 2, 3, 4, 5}
fmt.Println(arr)
reverse(&arr)
fmt.Println(arr)
}
map
map是一种key,val键值对的存储结构,其中的key和val都是可以用比较的类型,所以在加入map的时候可以通过判断map中是否有key。
值得注意的是,我们无法取map的地址,当我们用&map[‘bob’]的时候会编译报错,为什么呢?
我认为是因为map中的元素并不是永久的,可能随着map的扩容,导致该位置上的元素rehash到了其他位置,可能会让存储地址无效。
如何有序的遍历map
比如我们map是map[string]int,此时有一个需求,需要按照字典的顺序读取map,如何做呢?
看下面代码,可以先对key排序,然后根据排序后的key去字典中取
package main
import (
"fmt"
"sort"
)
func main() {
dict := make(map[string]int)
dict["b"] = 1
dict["a"] = 1
dict["c"] = 1
l := make([]string, 0, 3)
for k := range dict {
l = append(l, k)
}
sort.Strings(l)
for _, v := range l {
fmt.Printf("%s:%d\n", v, dict[v])
}
}
如何判断map中是否有元素?
比如我们map是map[string]int,如果去到一个不存在的key,会自动返回默认值,但是万一有一个key刚好val=0呢?那他明明存在,我们的逻辑判断成了不存在。
所以得用下面的方式判断:
if _, ok := dict["d"]; !ok {
fmt.Println("不存在")
}
我想要切片作为key怎么办
上面说过,对于map来说key是需要能通过==来判断相等的,那万一我就想要让切片作为key呢?
我们可以间接的完成这个需求
dict := make(map[string]int)//先创建一个string为key的map
func trans(s []string)string{
return fmt.Sprintf("%q",s) //将切片转换成string类型
}
func Add(list []string){
dict[trans(list)] += 1
}
基本上所有的需求,都可以用上面的方法完成。当然也不一定非要是字符串类型,任何可以得到想要结果的可以使用==的结构都可以。
map是支持这种的map[string]map[string]string.
结构体
结构体将零个或者多个任意类型的命名变量组合在一起成为一个聚合的数据类型。
type Book struct{
Name string
//......
}
var b book
对于b,我们可以通过 . 来取出属性,例如b.Name = ‘Go入门’,在go中也可以对指针类型的结构体使用 .
var b *book = &Book{Name: 'aaa'}
b.Name = 'bbb' //等价于(*b).Name = 'bbb'
需要注意的地方
加入某个函数能够返回结构体,必须得返回结构体指针才可以对结构体的属性进行操作,否则会找不到变量
正解:
结构体无法包含他自己,但是可以是指针
当我们学习算法的时候,经常需要自己手写一个简单的二叉树数据结构。下方会报错
换成指针即可。
写一个小demo试试,二叉树排序
package main
import "fmt"
type Tree struct {
val int
left, right *Tree
}
func sort(vals []int) {
var root *Tree
for _, v := range vals {
root = add(root, v)
}
s := make([]int, 0, len(vals))
s = TreeToSlice(s[:0], root)
fmt.Println(s)
}
func add(node *Tree, val int) *Tree {
if node == nil {
t := new(Tree)
t.val = val
return t
} else {
if node.val < val {
node.right = add(node.right, val)
} else {
node.left = add(node.left, val)
}
return node
}
}
func TreeToSlice(s []int, root *Tree) []int {
if root != nil {
s = TreeToSlice(s, root.left)
s = append(s, root.val)
s = TreeToSlice(s, root.right)
}
return s
}
func main() {
sort([]int{3, 1, 2, 4, 5})
}
如果结构体属性首字母是小写,别的包无法引用
比如下面这种情况
a,b都是不可导出的,虽然上面代码没有显示的引用a,b,但是他们被隐式的引用了,这也是不允许的。
结构体之间的比较
如果结构体的所有属性都是可以比较的,那么结构体也是可以比较的。
既然结构体是可以比较的,说明结构体是可以当成map的key的。
匿名成员
比如我们模拟一下人类这个类。
如果我们把所有属性写在一个结构体,例如
type Human struct {
Name string
Age int
SchoolName string
WorkPlace string
}
这样结构会比较不清晰,我们可以把一些部分抽出来。
type Human struct {
Name string
Age int
School
Work
}
type School struct {
SchoolName string
}
type Work struct {
WorkPlace string
}
Go允许我们定义不带名字的结构体成员,只需要指定他的类型。这些结构体成员叫做匿名成员。
第五章(函数)
在GO中是值传递,即如果实参传入的是非指针的类型,函
数会创建一个副本,对副本进行修改不会影响到本体,但是如果是引用类型,那么函数使用形参可能会间接修改本体。
异常
函数所发生的异常我们必须考虑,有以下几个方法
1.遇到异常,返回异常打印异常
if err != nil{
return nil,fmt.Errorf("%v",err)
}
2.设置超时时间并且重试
const timeount = 1 * time.Minute
deadline := time.Now().Add(timeout)
//重试逻辑
for tris:= 0;time.Now().Before();treis++{
//逻辑
if err == nil{
break;
}
log.printf("err")
//等待一定时间,指数退避策略
time.Sleep(time.Second << uint(tries))
}
3.输出错误,优雅的结束
if err != nil{
fmt.Fprintf(os.Stderr,"err : %v",err)
os.exit(1)
}
4.日志记录,继续运行
if err != nil{
log.printf(os.Stderr,"err : %v",err)
}
匿名函数
首先得知道什么是匿名函数,一般的函数是这样的
func 函数名(...)(返回)
匿名函数就是没有函数名,例如
func (...)(返回)
闭包
Go 语言支持匿名函数,可作为闭包。匿名函数是一个"内联"语句或表达式。匿名函数的优越性在于可以直接使用函数内的变量,不必申明。
以下实例中,我们创建了函数 getSequence() ,返回另外一个函数。该函数的目的是在闭包中递增 i 变量,代码如下:
课程拓扑排序(dfs)
package main
import (
"fmt"
"sort"
)
var prereqs = map[string][]string{
"algorithms": {"data structures"},
"calculus": {"linear algebra"},
"compilers": {
"data structures",
"formal languages",
"computer organization",
},
"data structure": {"discrete math"},
"discrete math": {"intro to programming"},
"databases": {"data structures"},
"formal languages": {"discrete math"},
"networks": {"operating systems"},
"operating systems": {"data structures", "computer organization"},
"programming languages": {"data structures", "computer organization"},
}
func topoSort(m map[string][]string) []string {
var order []string
seen := make(map[string]bool)
var findAll func(s []string)
findAll = func(s []string) {
for _, v := range s {
if !seen[v] {
seen[v] = true
findAll(m[v])
order = append(order, v)
}
}
}
var keys []string
for k, _ := range m {
keys = append(keys, k)
}
sort.Strings(keys)
findAll(keys)
return order
}
func main() {
for i, v := range topoSort(prereqs) {
fmt.Printf("%d\t%s\n", i, v)
}
}
注意得先声明var findAll func(s []string),不然在匿名函数中无法调用自己。
函数的作用域陷阱
看看下面这段代码,大概意思是先打印所有的tempdir,然后会有一个匿名函数,匿名函数的作用也是打印位于循环内结构的d。看似没什么问题把,看看打印结果
func main() {
var rmdirs []func()
for _, d := range os.TempDir() {
fmt.Println(d)
rmdirs = append(rmdirs, func() {
fmt.Print("func:")
fmt.Println(d)
})
}
for _, dir := range rmdirs {
dir()
}
}
67
58
92
85
115
101
114
115
92
77
121
72
111
112
101
92
65
112
112
68
97
116
97
92
76
111
99
97
108
92
84
101
109
112
func:112
func:112
func:112
func:112
func:112
func:112
func:112
func:112
func:112
func:112
func:112
func:112
func:112
func:112
func:112
func:112
func:112
func:112
func:112
func:112
func:112
func:112
func:112
func:112
func:112
func:112
func:112
func:112
func:112
func:112
func:112
func:112
func:112
func:112
为啥匿名函数的输出全是112?
func的输出似乎都被for的最后一个元素给覆盖了,为什么?
因为我们知道闭包可以用上面的局部变量,可以知道当我们在下面的for循环中使用的局部变量是for循环中最后一次的。
正确的代码?
package main
import (
"fmt"
"os"
)
func main() {
var rmdirs []func()
for _, d := range os.TempDir() {
dir := d
fmt.Println(dir)
rmdirs = append(rmdirs, func() {
fmt.Print("func:")
fmt.Println(dir)
})
}
for _, dir := range rmdirs {
dir()
}
}
变长函数
变长函数可以接受长度变化的参数。
package main
import "fmt"
func test(vals ...int) {
fmt.Println(vals)
}
func main() {
test(1)
test(1, 2)
test(1, 2, 3)
test([]int{4, 5, 6}...)
}
延迟调用
defer语句可以用来调用一个复杂的函数,即在函数的入口和出口设置调试行为。
当下面代码:
package main
import (
"log"
"time"
)
func slow(){
defer trace("hello")()
time.Sleep(10 * time.Second)
}
func trace(msg string) func(){
start := time.Now()
log.Printf("enter %s",msg)
return func() {
log.Printf("exit %s(%s)",msg,time.Since(start))
}
}
func main() {
slow()
}
可以完成对trace的return函数的延时调用,一定不要忘记defer的函数后面要带个小括号。
如果没带就不会调用返回的函数,而是会在slow函数结束后直接调用trace。
再看另外一个例子。
func double(x int)(result int){
defer func() {fmt.Printf("double(%d) = %d\n",x,result)}()
time.Sleep(10 * time.Second)
return x + x
}
这一次虽然是defer了一个函数,但仍然等到了double结束了才进入函数,所以可以推断出,如果defer的函数会返回一个函数,先回进入函数取到那个返回的函数,然后等待主程序return,去延时调用返回函数,才会出现上面日志记录的情况。
第六章(方法)
方法
func f(...)(返回) //普通方法
func (f *function)f(...)(返回) //方法属于function