diff --git a/src/study/day7Function/DeferredFunc.go b/src/study/day7Function/DeferredFunc.go new file mode 100644 index 0000000..791863f --- /dev/null +++ b/src/study/day7Function/DeferredFunc.go @@ -0,0 +1,146 @@ +package main + +import ( + "Study/src/study/day7Function/HtmlTools" + "bufio" + "fmt" + "golang.org/x/net/html" + "log" + "net/http" + "os" + "strings" + "time" +) + +// defer语句经常被用于处理成对的操作, +// 如打开、关闭、连接、断开连接、加锁、释放锁。 +// 通过defer机制,不论函数逻辑多复杂,都能保证在任何执行路径下,资源被释放。 +// 释放资源的defer应该直接跟在请求资源的语句后。 + +func title(url string) error { + resp, err := http.Get(url) + if err != nil { + return err + } + // 直到包含defer的函数执行完毕之后,defer的这条语句才会执行 + defer resp.Body.Close() + // Check Content-Type is HTML (e.g., "text/html;charset=utf-8"). + ct := resp.Header.Get("Content-Type") + if ct != "text/html" && !strings.HasPrefix(ct, "text/html;") { + return fmt.Errorf("%s has type %s, not text/html", url, ct) + } + doc, err := html.Parse(resp.Body) + + if err != nil { + return fmt.Errorf("parsing %s as HTML: %v", url, err) + } + visitNode := func(n *html.Node) { + if n.Type == html.ElementNode && n.Data == "title" && n.FirstChild != nil { + fmt.Println(n.FirstChild.Data) + } + } + HtmlTools.ForEachNode(doc, visitNode, nil) + return nil +} + +// 每一次bigSlowOperation被调用,程序都会记录函数的进入,退出,持续时间。 +// (我们用time.Sleep模拟一个耗时的操作) +func bigSlowOperation() { + defer trace("aaa")() + time.Sleep(3 * time.Second) +} +func trace(msg string) func() { + start := time.Now() + log.Printf("enter %s", msg) + return func() { + log.Printf("leave %s, (%s)", msg, time.Since(start)) + } +} + +// defer 可在函数调用执行完毕时,打印一些调试信息 +func double(x int) (result int) { + defer func() { fmt.Println(x) }() + return x + x +} +func triple(x int) (result int) { + defer func() { result += x }() + return double(x) +} +func main() { + //if err := title(os.Args[1]); err != nil { + // log.Fatalln(err) + //} + bigSlowOperation() + + fmt.Println(triple(3)) // 9 + + //⚠️注意,在循环体里调用defer时,由于defer所在的语句要在整个函数执行完毕时在执行, + // 这样的代码会导致系统的文件描述符耗尽,因为在所有文件都被处理之前,没有文件会被关闭。 + +} + +// 我们可以稍微对以前练习1.13的代码做改进 +func extractRestructure(URL string, isSave bool, saveOriginOnly bool) ([]string, error) { + // 检查参数 + if URL == "" { + URL = "https://go.dev/" + } + + node, resp := HtmlTools.GetHtmlResponse(URL) + originURL := resp.Request.URL.Host + var links []string + + // 这样就不会有重复打开文件的浪费资源的bug了, + // 也没有在循环体里调用defer + // 定义变量 + var filePath string + var file *os.File + // 定义保存路径 + if isSave { + var err error + filePath = HtmlTools.GetCurrentAbPath() + "/www/urls.txt" + // 打开文件 + file, err = os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666) + if err != nil { + fmt.Println("文件打开失败", err) + } + } + // 关闭文件 + defer file.Close().Error() + visitNode := func(n *html.Node) { + if n.Type == html.ElementNode && n.Data == "a" { + for _, a := range n.Attr { + if a.Key != "href" { + continue + } + // 将相对路径和url的内容组合在一起形成完整的绝对路径 + link, err := resp.Request.URL.Parse(a.Val) + if err != nil { + // 忽略掉报error的URL + fmt.Println(err) + continue + } + // 判断是否只保存同一域名下的地址 + if saveOriginOnly && (link.Host != originURL) { + continue + } + // 判断是否保存页面 + if isSave && link.String() != "" { + write := bufio.NewWriter(file) + if _, err := write.WriteString(link.String() + "\r\n"); err != nil { + panic(err) + } + // 将缓存写入文件 + if err := write.Flush(); err != nil { + panic(err) + } + } + // 将link添加到切片 + links = append(links, link.String()) + + } + } + } + HtmlTools.ForEachNode(node, visitNode, nil) + return links, nil +} diff --git a/src/study/day7Function/Practice/Practice5.13.go b/src/study/day7Function/Practice/Practice5.13.go index 44c57aa..6139873 100644 --- a/src/study/day7Function/Practice/Practice5.13.go +++ b/src/study/day7Function/Practice/Practice5.13.go @@ -7,10 +7,6 @@ import ( "golang.org/x/net/html" "log" "os" - "path" - "path/filepath" - "runtime" - "strings" ) // BreadthFirst 队列中的每节点都调用f()。 @@ -43,7 +39,7 @@ func BreadthFirst(f func(item string) []string, worklist []string) { // saveOriginOnly bool, 是否只保存与当前页面域名相同的页面 // // return []string, error, 提取到的链接列表和错误信息 -func ExtractRestructure(URL string, isSave bool, saveOriginOnly bool) ([]string, error) { +func extractRestructure(URL string, isSave bool, saveOriginOnly bool) ([]string, error) { // 检查参数 if URL == "" { URL = "https://go.dev/" @@ -64,7 +60,6 @@ func ExtractRestructure(URL string, isSave bool, saveOriginOnly bool) ([]string, fmt.Println(err) continue } - // 将link添加到切片 links = append(links, link.String()) // 判断是否只保存同一域名下的地址 @@ -73,10 +68,7 @@ func ExtractRestructure(URL string, isSave bool, saveOriginOnly bool) ([]string, } // 判断是否保存页面 if isSave && link.String() != "" { - if err != nil { - panic(err) - } - filePath := getCurrentAbPath() + "/www/urls.txt" + filePath := HtmlTools.GetCurrentAbPath() + "/www/urls.txt" file, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666) if err != nil { fmt.Println("文件打开失败", err) @@ -104,7 +96,7 @@ func main() { // crawl 此函数返回一个[]string, 内容为url crawl := func(url string) []string { fmt.Println("Crawl:", url) - list, err := ExtractRestructure(url, true, true) + list, err := extractRestructure(url, true, true) if err != nil { log.Print(err) } @@ -115,43 +107,3 @@ func main() { // starting from the command-line arguments. BreadthFirst(crawl, []string{"https://go.dev/"}) } - -// see https://zhuanlan.zhihu.com/p/363714760 -// 最终方案-全兼容 -func getCurrentAbPath() string { - dir := getCurrentAbPathByExecutable() - if strings.Contains(dir, getTmpDir()) { - return getCurrentAbPathByCaller() - } - return dir -} - -// 获取系统临时目录,兼容go run -func getTmpDir() string { - dir := os.Getenv("TEMP") - if dir == "" { - dir = os.Getenv("TMP") - } - res, _ := filepath.EvalSymlinks(dir) - return res -} - -// 获取当前执行文件绝对路径 -func getCurrentAbPathByExecutable() string { - exePath, err := os.Executable() - if err != nil { - log.Fatal(err) - } - res, _ := filepath.EvalSymlinks(filepath.Dir(exePath)) - return res -} - -// 获取当前执行文件绝对路径(go run) -func getCurrentAbPathByCaller() string { - var abPath string - _, filename, _, ok := runtime.Caller(0) - if ok { - abPath = path.Dir(filename) - } - return abPath -} diff --git a/src/study/day7Function/Practice/Practice5.18.go b/src/study/day7Function/Practice/Practice5.18.go new file mode 100644 index 0000000..a153dc1 --- /dev/null +++ b/src/study/day7Function/Practice/Practice5.18.go @@ -0,0 +1,55 @@ +package main + +import ( + "fmt" + "io" + "log" + "net/http" + "os" + "path" +) + +// Fetch downloads the URL and returns the +// name and length of the local file. +func fetch(url string) (filename string, n int64, err error) { + resp, err := http.Get(url) + if err != nil { + return "", 0, err + } + defer resp.Body.Close() + local := path.Base(resp.Request.URL.Path) + if local == "/" { + local = "index.html" + } + fmt.Println(local) + f, err := os.Create(local) + if err != nil { + return "", 0, err + } + n, err = io.Copy(f, resp.Body) + + // 这里要搞明白 return defer 和 返回值被赋值给调用者 的先后执行顺序 + // 这里先说结论 defer的执行顺序在return之后,但是在返回值返回给调用方之前 + + // 匿名返回值是在return执行时被声明,有名返回值则是在函数声明的同时被声明, + // 因此在defer语句中只能访问有名返回值,而不能直接访问匿名返回值 + + // return其实应该包含前后两个步骤: + // 第一步是给返回值赋值(若为有名返回值则直接赋值,若为匿名返回值则先声明再赋值); + // 第二步是调用RET返回指令并传入返回值,而RET则会检查defer是否存在,若存在就先逆序插播defer语句, + // 最后RET携带返回值退出函数; + + // 回到这里这个例子, err是被显示定义的有名返回值变量, 所以defer可以访问err + // 这个例子中, 执行到defer时如果Close函数发生错误, 那么返回值中的err将会被改变 + defer func() { + if closeErr := f.Close(); closeErr != nil && err == nil { + err = closeErr + } + }() + return local, n, err +} +func main() { + if l, n, err := fetch("https://go.dev/"); err != nil { + log.Fatalln(l, n, err) + } +} diff --git a/src/study/day7Function/VariableParameter.go b/src/study/day7Function/VariableParameter.go new file mode 100644 index 0000000..23b6048 --- /dev/null +++ b/src/study/day7Function/VariableParameter.go @@ -0,0 +1,50 @@ +package main + +import ( + "fmt" + "os" +) + +// 可变参数 +// 在声明可变参数函数时,需要在参数列表的最后一个参数类型之前加上省略符号“...”, +// 这表示该函数会接收任意数量的该类型参数。 +func sum(vals ...int) int { + total := 0 + for _, val := range vals { + total += val + } + return total +} +func main() { + fmt.Println(sum()) // "0" + fmt.Println(sum(3)) // "3" + fmt.Println(sum(1, 2, 3, 4)) // "10" + + // 虽然在可变参数函数内部,...int 型参数的行为看起来很像切片类型, + // 但实际上,可变参数函数和以切片作为参数的函数是不同的。 + var f = func(a ...int) { + fmt.Printf("%T\n", a) + } + var g = func(a []int) { + fmt.Printf("%T\n", a) + } + fmt.Printf("%T\n", f) // "func(...int)" + fmt.Printf("%T\n", g) // "func([]int)" + + // 但是可变参数函数内部,实际传入的参数还是一个切片 + f(1, 2, 3) + g([]int{1, 2, 3}) + + // 可变参数函数经常被用于格式化字符串。 + // 下面的errorf函数构造了一个以行号开头的, + // 经过格式化的错误信息。函数名的后缀f是一种通用的命名规范, + // 代表该可变参数函数可以接收Printf风格的格式化字符串。 + errorf := func(linenum int, format string, args ...interface{}) { + fmt.Fprintf(os.Stderr, "Line %d: ", linenum) + fmt.Fprintf(os.Stderr, format, args...) + fmt.Fprintln(os.Stderr) + } + linenum, name := 12, "count" + errorf(linenum, "undefined: %s", name) // "Line 12: undefined: count" + +}