day7函数 可变参数 defer机制 练习5.18

master
独孤伶俜 2022-12-11 23:44:39 +08:00
parent 70b913091c
commit 010c5bc593
4 changed files with 254 additions and 51 deletions

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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"
}