Go语言圣经的练习题


第一章

1.2 命令行参数

练习 1.1:

修改 echo 程序,使其能够打印 os.Args[0],即被执行命令本身的名字。

func OneDotOne() {
	fmt.Println(os.Args[0])
}

练习 1.2:

修改 echo 程序,使其打印每个参数的索引和值,每个一行。

func OneDotTwo() {
	for i, val := range os.Args {
		fmt.Printf("第%d个:%s \n", i, val)
	}
}

练习 1.3:

做实验测量潜在低效的版本和使用了 strings.Join 的版本的运行时间差异。(1.6 节讲解了部分 time 包,11.4 节展示了如何写标准测试程序,以得到系统性的性能评测。)

1.3 查找重复的行

案例ch1/dup1:

func Example_ch1_dup1() {
	Counts := make(map[string]int)
	input := bufio.NewScanner(os.Stdin)
	for input.Scan() {
		Counts[input.Text()]++
	}
	for key, val := range Counts {
		fmt.Printf("字符串%s 出现 %d 次\n", key, val)
	}
}

问题:
1、不知道怎么结束输入,一直让输入内容,连续按下回车也不会结束输入。明明文章中说Scan 函数在读到一行时返回 true,不再有输入时返回 false。怎么算不再输入?

连续按下回车,输入的一直时””,不算结束,他返回false的唯一条件是接收到文件结束符(EOF),而在不同操作系统,结束输入的方式不同:
linux 和 mac 下是Ctrl + D
windows下是 ctrl + Z

2、疑问:这个EOF到底是怎么获取的?是文件中最后一个字符是EOF还是系统在读完文件之后会自动返回一个EOF?

上面两个说法都不太准确,EOF 不是数据,而是一种状态信号,更准确的说法是Scanner底层会使用read()系统调用函数,当每次能读到数据的时候返回字节数就不会是0,但是当下一次读没有内容时,返回的字节数为0,Go语言将这个0自动转换为程序能理解的EOF信号,从而使得Scanner的Scan返回false。

案例ch1/dup2:

func Example_ch1_dup2() {
	// 从文件中读取
	counts := make(map[string]int)
	files := os.Args[1:]
	if len(files) == 0 {
		// 继续从终端中读取
		countline(os.Stdin, counts)
	} else {
		for _, filename := range files {
			pFile, err := os.Open(filename)
			if err != nil {
				fmt.Printf("open %s failed= %v\n", filename, err)
				continue
			}
			countline(pFile, counts)
			pFile.Close() // 注意:我写的过程中忘记关闭了
		}
	}
	fmt.Println("结果如下:")
	for key, val := range counts {
		fmt.Printf("字符串%s 出现 %d 次\n", key, val)
	}
}

func countline(reader io.Reader, counts map[string]int) {
	input := bufio.NewScanner(reader)
	for input.Scan() {
		counts[input.Text()]++
	}
}

案例ch1/dup3:

func Example_ch1_dup3() {
	counts := make(map[string]int)
	for _, filename := range os.Args[1:] {
		data, err := os.ReadFile(filename)
		if err != nil {
			fmt.Println("readFile err = ", err.Error())
			continue
		}
		// 一次性将file中的内容全部读入内存,然后再split
		for _, line := range strings.Split(string(data), "\n") {
			counts[line]++
		}
	}
	for line, n := range counts {
		if n > 1 {
			fmt.Printf("%d\t%s\n", n, line)
		}
	}
}

练习 1.4:

修改 dup2,出现重复的行时打印文件名称。

func Practice_ch1_4() {
	counts := make(map[string]int)
	for _, filename := range os.Args[1:] {
		data, err := os.ReadFile(filename)
		if err != nil {
			fmt.Println("readFile err = ", err.Error())
			continue
		}
		// 一次性将file中的内容全部读入内存,然后再split
		for _, line := range strings.Split(string(data), "\n") {
			if _, ok := counts[line]; ok {
				fmt.Println("文件名为:", filename)
			}
			counts[line]++
		}
	}
	for line, n := range counts {
		if n > 1 {
			fmt.Printf("%d\t%s\n", n, line)
		}
	}
}

两种方式,一种是在累加的时候直接判断存不存在这个key,另一种方式则是创建一个新的map来存储。

1.4 GIF动画

这一小节暂时用不到,暂时不看

1.5 获取URL

案例gopl.io/ch1/fetch

// Fetch prints the content found at a URL.
package main

import (
    "fmt"
    "io/ioutil"
    "net/http"
    "os"
)

func main() {
    for _, url := range os.Args[1:] {
        resp, err := http.Get(url)
        if err != nil {
            fmt.Fprintf(os.Stderr, "fetch: %v\n", err)
            os.Exit(1)
        }
        b, err := io.ReadAll(resp.Body)
        resp.Body.Close()
        if err != nil {
            fmt.Fprintf(os.Stderr, "fetch: reading %s: %v\n", url, err)
            os.Exit(1)
        }
        fmt.Printf("%s", b)
    }
}

练习1.7:

函数调用io.Copy(dst, src)会从src中读取内容,并将读到的结果写入到dst中,使用这个函数替代掉例子中的ioutil.ReadAll来拷贝响应结构体到os.Stdout,避免申请一个缓冲区(例子中的b)来存储。记得处理io.Copy返回结果中的错误。

func Practice_ch1_fetch() {
	if len(os.Args) < 2 {
		fmt.Println("xxx")
		os.Exit(-1)
	}
	proxyURLStr := "http://127.0.0.1:7897"
	proxyURL, err := url.Parse(proxyURLStr)
	if err != nil {
		fmt.Printf("解析代理 URL 失败: %s\n", err.Error())
		os.Exit(1)
	}
	transport := &http.Transport{
		Proxy: http.ProxyURL(proxyURL),
	}
	client := &http.Client{
		Transport: transport,
		Timeout:   10 * time.Second, // 强烈建议在实际项目中设置超时时间
	}
	resp, err := client.Get(os.Args[1])
	if err != nil {
		fmt.Printf("%s", err.Error())
		os.Exit(-1)
	}
	defer resp.Body.Close()
	_, err = io.Copy(os.Stdout, resp.Body)
	if err != nil {
		fmt.Printf("读取响应体失败: %s\n", err.Error())
	}
	// fmt.Printf("%s", data)
}

1、由于网络问题,使用http库重新创建了一个http.Client,并添加本地代理,简单封装了一个函数,后续直接使用

func getProxyClient() *http.Client {
	proxyUrlStr := "http://127.0.0.1:7897"
	proxyUrl, err := url.Parse(proxyUrlStr)
	if err != nil {
		fmt.Printf("%s", err.Error())
		os.Exit(1)
	}
	transport := &http.Transport{
		Proxy: http.ProxyURL(proxyUrl),
	}
	return &http.Client{
		Transport: transport,
	}
}

io.Copy核心作用就是像水管一样,将src中的数据,一块一块的直接输送到dst中,而不是先把整桶水接满再倒入进去。
优点:

  • 1、极致的内存效率(防止OOM)
    加入传输的是一个2GB的大文件,如果使用io.ReadAll函数,则整个文件都存在内存中,极容易导致Out of Memory。如果使用io.Copy则它内部会分配一个32KB的缓冲区,每读满32KB或读到EOF就会清空缓冲区,无论传输内容大小,程序占用内存始终稳定在几十KB大小。
  • 2、代码封装
    io.Copy的功能也可以手动创建缓冲区,并手动编写循环代码,完成内容读取。
buf := make([]byte, 32*1024)
for {
	n, err := resp.Body.Read(buf)
	if n>0 {
		os.Stdout.Write(buf[:n])// 手动写入
	}
	if err != nil {
		if err == io.EOF {
			break
		}
		fmt.Printf("读取错误:%v\n", err)
		break
	}
}
  • 3、底层性能优化
    io.Copy在Go标准库中经过高度优化,在文件复制到文件、网络套接字到网络套接字等情况下,Go甚至会尝试调用操作系统级别的系统调用,来实现零拷贝,进一步减少CPU和内存的开销。

练习 1.8:

修改fetch这个范例,如果输入的url参数没有 http:// 前缀的话,为这个url加上该前缀。你可能会用到strings.HasPrefix这个函数。

if !strings.HasPrefix(v, "http://") {
			v = "http://" + v
		}

练习 1.9:

修改fetch打印出HTTP协议的状态码,可以从resp.Status变量得到该状态码。

fmt.Printf("状态码: %d\n", resp.StatusCode)
fmt.Printf("状态码: %s\n", resp.Status)

1.6 并发获取多个URL

案例gopl.io/ch1/fetchall

func Example_ch1_fetchall() {
	start := time.Now()
	ch := make(chan string)
	for _, url := range os.Args[1:] {
		go fetch(url, ch)
	}
	for range os.Args[1:] {
		fmt.Println(<-ch)
	}
	fmt.Printf("%2.fs elapsed\n", time.Since(start).Seconds())
}

func fetch(url string, ch chan<- string) {
	start := time.Now()
	client := getProxyClient()

	resp, err := client.Get(url)
	if err != nil {
		ch <- fmt.Sprint(err)
		return
	}
	defer resp.Body.Close()
	nbytes, err := io.Copy(io.Discard, resp.Body)
	if err != nil {
		ch <- fmt.Sprintf("while reading %s: %v", url, err)
		return
	}
	secs := time.Since(start).Seconds()
	ch <- fmt.Sprintf("%.2fs  %7d  %s\n", secs, nbytes, url)
}

练习 1.10:

找一个数据量比较大的网站,用本小节中的程序调研网站的缓存策略,对每个URL执行两遍请求,查看两次时间是否有较大的差别,并且每次获取到的响应内容是否一致,修改本节中的程序,将响应结果输出到文件,以便于进行对比。

func Practice_ch1_fetchall() {
	start := time.Now()
	ch := make(chan string)
	urls := []string{}
	for _, url := range os.Args[1:] {
		urls = append(urls, url)
		urls = append(urls, url) // 添加两次
	}
	for i, url := range urls {
		filename := strings.ReplaceAll(url, "https://", "")
		filename = strings.ReplaceAll(filename, "http://", "")
		filename = strings.ReplaceAll(filename, "/", "_")
		filename = strings.ReplaceAll(filename, ":", "_")
		filename = filename + fmt.Sprintf("_%d", i) + ".txt" // 统一加上 .txt 后缀
		go fetchToFile(url, filename, ch)
	}
	for range urls {
		fmt.Println(<-ch)
	}
	fmt.Printf("%2.fs elapsed\n", time.Since(start).Seconds())
}
func fetchToFile(url string, filename string, ch chan<- string) {
	start := time.Now()
	client := getProxyClient()

	resp, err := client.Get(url)
	if err != nil {
		ch <- fmt.Sprint(err)
		return
	}
	defer resp.Body.Close()
	f, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY, 0664)
	if err != nil {
		ch <- fmt.Sprintf("Open %s file failed: %s", url, err)
		return
	}
	defer f.Close()
	nbytes, err := io.Copy(f, resp.Body)
	if err != nil {
		ch <- fmt.Sprintf("while reading %s: %v", url, err)
		return
	}
	secs := time.Since(start).Seconds()
	ch <- fmt.Sprintf("%.2fs  %7d  %s\n", secs, nbytes, url)
}
PS D:\blog\myblog\source\_posts\golang> .\main.exe https://raw.githubusercontent.com/dwyl/english-words/master/words.txt https://jsonplaceholder.typicode.com/photos
0.53s  1071472  https://jsonplaceholder.typicode.com/photos

0.54s  1071472  https://jsonplaceholder.typicode.com/photos

0.78s  4862985  https://raw.githubusercontent.com/dwyl/english-words/master/words.txt

0.82s  4862985  https://raw.githubusercontent.com/dwyl/english-words/master/words.txt

 1s elapsed
PS D:\blog\myblog\source\_posts\golang> 

这个结果能说明对应网站开启了缓存,还能说明什么?

练习 1.11:

在fetchall中尝试使用长一些的参数列表,比如使用在alexa.com的上百万网站里排名靠前的。如果一个网站没有回应,程序将采取怎样的行为?(Section8.9 描述了在这种情况下的应对机制)。
回答:
从程序上看,如果一个请求没有响应则会直接返回, 不影响其他的继续执行。这个前提是http.Client设置了超时时间,如果超过10s没有返回,则会Get得到超时,但是如果没有设置,则请求永远阻塞,然后主协程一直等待通道数据,fetch协程不会返回,导致整个程序挂起。

如果某个网站完全没有响应,对应的 http.Get 会一直阻塞(因为默认 Client 没有超时),导致主 goroutine 在 <-ch 处永久等待,整个程序挂起无法退出。其他已经完成的请求结果虽然会先打印出来,但程序不会正常结束。

1.7 web服务

gopl.io/ch1/server1

func Server1() {
	http.HandleFunc("/", handler)
	log.Fatal(http.ListenAndServe("localhost:8000", nil))
}

func handler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "URL.path= %s", r.URL.Path)
}

撰写代码过程中将handleFunc写成了handlerFunc
这两者一个是将一个func(w, r)类型的内容注册到默认的Mux上,另一个则是handlerFunc则是一个类型,可以将普通函数包装成http.handler接口值返回

gopl.io/ch1/server2

var mu sync.Mutex
var count int

func Server2() {
	http.HandleFunc("/", handler2)

	http.HandleFunc("/count", countHandler)
	log.Fatal(http.ListenAndServe(":8000", nil))
}

// handler echoes the Path component of the requested URL.
func handler2(w http.ResponseWriter, r *http.Request) {
	mu.Lock()
	count++
	mu.Unlock()
	fmt.Printf("handler2 被调用,路径 = %q\n", r.URL.Path) // 关键
	fmt.Fprintf(w, "URL.Path = %q\n", r.URL.Path)
}

func countHandler(w http.ResponseWriter, r *http.Request) {
	mu.Lock()
	defer mu.Unlock()
	fmt.Fprintf(w, "count is %d", count)
}

基本没什么问题,但是发现在浏览器中每次发起请求的时候,无论请求路径是什么,总会导致count变量累加,于是在/处理函数中添加了下面的内容:

func handler2(w http.ResponseWriter, r *http.Request) {
	mu.Lock()
	count++
	mu.Unlock()
	fmt.Printf("handler2 被调用,路径 = %q\n", r.URL.Path) // 关键
	fmt.Fprintf(w, "URL.Path = %q\n", r.URL.Path)
}

当再次访问/count的时候,发现浏览器会携带一个”/favicon.ico”的路由,用来请求网站图标,给这个路径单独写一个处理函数即可。

练习 1.12:

修改Lissajour服务,从URL读取变量,比如你可以访问 http://localhost:8000/?cycles=20 这个URL,这样访问可以将程序里的cycles默认的5修改为20。字符串转换为数字可以调用strconv.Atoi函数。你可以在godoc里查看strconv.Atoi的详细说明。

暂时不写

gopl.io/ch1/server3

第二章

第八章

8.1 Groutines

ch8/spinner

func ch8_spinner() {
	go spinner(100 * time.Millisecond)
	const n = 45
	fibN := fib(n)
	fmt.Printf("\rFibonacci(%d) = %d\n", n, fibN)
}

func spinner(delay time.Duration) {
	for {
		for _, r := range `-\|/` {
			fmt.Printf("\r%c", r)
			time.Sleep(delay)
		}
	}
}

8.2 并发的Clock服务

gopl.io/ch8/clock1

func ch8_clock1() {
	listener, err := net.Listen("tcp", "localhost:8000")
	if err != nil {
		log.Fatal(err)
	}
	log.Print("Tcp receiving...")
	for {
		conn, err := listener.Accept()
		if err != nil {
			log.Print(err)
			continue
		}
		handleConn(conn)
	}
}

func handleConn(c net.Conn) {
	defer c.Close()
	for {
		_, err := io.WriteString(c, time.Now().Format("15:04:05\n"))
		if err != nil {
			log.Print(err)
			return
		}
		time.Sleep(1 * time.Second)
	}
}

gopl.io/ch8/netcat1

// Netcat1 is a read-only TCP client.
package main

import (
    "io"
    "log"
    "net"
    "os"
)

func main() {
    conn, err := net.Dial("tcp", "localhost:8000")
    if err != nil {
        log.Fatal(err)
    }
    defer conn.Close()
    mustCopy(os.Stdout, conn)
}

func mustCopy(dst io.Writer, src io.Reader) {
    if _, err := io.Copy(dst, src); err != nil {
        log.Fatal(err)
    }
}

gopl.io/ch8/clock2

func ch8_clock2() {
	listener, err := net.Listen("tcp", "localhost:8000")
	if err != nil {
		log.Fatal(err)
	}
	log.Print("Tcp receiving...")
	for {
		conn, err := listener.Accept()
		if err != nil {
			log.Print(err)
			continue
		}
		go handleConn(conn)
	}
}

练习 8.1

修改clock2来支持传入参数作为端口号,然后写一个clockwall的程序,这个程序可以同时与多个clock服务器通信,从多个服务器中读取时间,并且在一个表格中一次显示所有服务器传回的结果,类似于你在某些办公室里看到的时钟墙。如果你有地理学上分布式的服务器可以用的话,让这些服务器跑在不同的机器上面;或者在同一台机器上跑多个不同的实例,这些实例监听不同的端口,假装自己在不同的时区。像下面这样:

$ TZ=US/Eastern    ./clock2 -port 8010 &
$ TZ=Asia/Tokyo    ./clock2 -port 8020 &
$ TZ=Europe/London ./clock2 -port 8030 &
$ clockwall NewYork=localhost:8010 Tokyo=localhost:8020 London=localhost:8030
// server 
func practice_ch8_clock1() {
	port := "8000"
	if len(os.Args) > 1 {
		port = os.Args[1]
	}

	listener, err := net.Listen("tcp", fmt.Sprintf("localhost:%s", port))
	if err != nil {
		log.Fatal(err)
	}
	log.Print("Tcp receiving...")
	for {
		conn, err := listener.Accept()
		if err != nil {
			log.Print(err)
			continue
		}
		go handleConn(conn)
	}
}

func handleConn(c net.Conn) {
	defer c.Close()
	// 执行server之前现在powershell中设置时区的环境变量
	// $env:TZ = "America/New_York"
	// 1. 在代码中读取 TZ 环境变量
	tzName := os.Getenv("TZ")
	if tzName == "" {
		tzName = "Local" // 如果没设置环境变量,默认使用本地时间
	}

	// 2. 根据读取到的名字加载时区
	loc, err := time.LoadLocation(tzName)
	if err != nil {
		// 如果加载失败(比如名字写错了),回退到 Local
		log.Printf("无法识别时区 '%s': %v, 回退到本地时间", tzName, err)
		loc = time.Local
	}

	for {
		t := time.Now().In(loc)
		_, err := io.WriteString(c, t.Format("15:04:05\n"))
		if err != nil {
			log.Print(err)
			return
		}
		time.Sleep(1 * time.Second)
	}
}
// clockall
type ClockInfo struct {
	Location string
	timeStr  string
	err      error
}

func clockall() {
	if len(os.Args) < 2 {
		log.Fatal("至少填写一个服务器地址")
	}
	var wg sync.WaitGroup
	update := make(chan *ClockInfo)
	timeRes := make(map[string]string)

	for _, address := range os.Args[1:] {
		parts := strings.Split(address, "=")
		if len(parts) != 2 {
			log.Printf("%s parse error\n", address)
			continue
		}
		location := parts[0]
		port := parts[1]
		wg.Add(1)
		go func(port, location string) {
			defer wg.Done()
			conn, err := net.Dial("tcp", "localhost:"+port)
			clockInfo := &ClockInfo{
				Location: location,
				timeStr:  "",
				err:      err,
			}
			// 不知道怎么将通道传递过来,也不知道怎么把消息写入通道中去
			if err != nil {
				update <- clockInfo
				return
			}
			defer conn.Close()

			for {
				// 循环读取消息
				scanner := bufio.NewScanner(conn)

				// 忘记这个scanner怎么读取数据了
				for scanner.Scan() {
					clockInfo.timeStr = scanner.Text()
					update <- clockInfo
				}
				if err := scanner.Err(); err != nil {
					clockInfo.err = err
					update <- clockInfo
					return
				}
			}
		}(port, location)
	}
	go func() {
		wg.Wait()
		close(update)
	}()

	for info := range update {
		if info.err != nil {
			timeRes[info.Location] = info.err.Error()
		} else {
			timeRes[info.Location] = info.timeStr
		}
		// 每接受到一个都更新一次
		renderClockWall(timeRes)
	}

}

// renderClockWall 负责清屏并打印格式化的表格
func renderClockWall(times map[string]string) {
	// 使用 ANSI 转义序列清屏并将光标移动到左上角 (兼容 Windows Terminal / PowerShell / Linux / macOS)
	fmt.Print("\033[2J\033[H")

	fmt.Println("=====================================")
	fmt.Println("           🌍 GLOBAL CLOCK WALL 🌍           ")
	fmt.Println("=====================================")

	// 使用map时每次遍历的顺序会不同,可以直观的看到,名称一直在跳
	for name, timeStr := range times {
		if timeStr == "" {
			timeStr = "等待连接..."
		}
		// %-15s 表示左对齐,占用 15 个字符宽度,保证冒号对齐
		fmt.Printf("  %-15s : %s\n", name, timeStr)
	}
	fmt.Println("=====================================")
	fmt.Println("按 Ctrl+C 退出")
}

遇到的问题:

1、怎么把协程的结果放在一起

没有放在一起,只创建了一个channel,每次对应协程有数据发送回来
就往channel中写入即可
2、怎么等待协程都退出
使用wg.Wait(),每创建一个就add一下,每结束一个就done一下
3、为啥一开始写不出来?
感觉自己想的太复杂了,就是一下就像把整个代码的功能和结构写成最完美的样子
就是什么都想封装一下,然后代码直接写出整理后的结果。
解决办法:

解决“想一次写好”的心理问题
这是最重要的:

接受第一版很丑。丑但能跑,比脑子里完美但写不出来强一万倍。
把问题拆成最小可验证的步骤:
改完 clock2 → 手动测试。
能连一个服务器 → 再加第二个。
能打印原始输出 → 再想怎么整理成表格。

写代码时只允许自己想当前这一步。其他“封装、复用、优雅关闭”全部先丢到 TODO 里。
写完一个能跑的版本后,强制自己休息 10 分钟,再回来看有没有可以改进的地方。

很多人卡死,不是因为技术难,而是因为大脑同时开了太多“线程”,上下文切换成本太高。你现在就是这种情况。

练习 8.2

实现一个并发FTP服务器。服务器应该解析客户端发来的一些命令,比如cd命令来切换目录,ls来列出目录内文件,get和send来传输文件,close来关闭连接。你可以用标准的ftp命令来作为客户端,或者也可以自己实现一个。
思路:

1、基于tcpServer 不一直往client写数据,而是等待接收数据

func Practice_ch1_clock2() {
	port := "8000"
	// if len(os.Args) == 2 {
	// 	port = os.Args[1]
	// }
	listener, err := net.Listen("tcp", fmt.Sprintf("localhost:%s", port))
	if err != nil {
		log.Fatal(err)
	}

	log.Print("Server starting...")
	for {
		conn, err := listener.Accept()
		if err != nil {
			log.Print(err)
			continue
		}

		go handleFtpConn(conn)
	}
}

func handleFtpConn(conn net.Conn) {
	defer conn.Close()
	remoteAddr := conn.RemoteAddr().String()
	log.Printf("新客户端连接: %s", remoteAddr)
	// 1、判断文件夹是否存在

	if err := os.MkdirAll("./data", 0644); err != nil {
		log.Println(err)
		io.WriteString(conn, "初始化文件夹失败\n")
	}
	scanner := bufio.NewScanner(conn)
	for scanner.Scan() {
		cmd := scanner.Text()
		log.Printf("收到命令: %s", cmd)
		switch cmd {
		case "ls":
			content, err := ListDir("./data")
			if err != nil {
				fmt.Fprintf(conn, "错误: %v\n", err)
				continue
			}
			// 直接把整个列表写回去
			io.WriteString(conn, content)
			// 可选:加一个结束标记
			io.WriteString(conn, ".\n")

		case "cwd":
			io.WriteString(conn, "cwd\n")
		case "cd":
			io.WriteString(conn, "cd\n")
		case "quit":
			return
		case "restart":
			if err := os.MkdirAll(remoteAddr, 0644); err != nil {
				io.WriteString(conn, "初始化文件夹失败\n")
			}
		default:
			io.WriteString(conn, "没有识别到命令\n")
		}
	}
}

func ListDir(path string) (string, error) {
	entries, err := os.ReadDir(path)
	if err != nil {
		return "", err
	}

	var b strings.Builder
	for _, entry := range entries {
		if entry.IsDir() {
			b.WriteString(entry.Name() + "/\n")
		} else {
			b.WriteString(entry.Name() + "\n")
		}
	}
	return b.String(), nil
}
package main

import (
	"bufio"
	"fmt"
	"io"
	"log"
	"net"
	"os"
)

func main() {
	port := "8000"
	// if len(os.Args) == 2 {
	// 	port = os.Args[1]
	// }
	conn, err := net.Dial("tcp", "localhost:"+port)
	if err != nil {
		log.Fatal(err)
	}
	defer conn.Close()

	go func(conn net.Conn) {
		connScanner := bufio.NewScanner(conn)
		for connScanner.Scan() {
			respStr := connScanner.Text()
			log.Println("收到服务返回的内容:", respStr)
			if respStr == "start_tans_file" {
				f, err := os.OpenFile("./data/receive.txt", os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)
				if err != nil {
					log.Println(err)
					continue
				}
				defer f.Close()

				_, err = io.Copy(f, conn)
				if err != nil {
					log.Println(err)
					continue
				}

			}

		}
	}(conn)

	stdinScanner := bufio.NewScanner(os.Stdin)
	fmt.Print("请输入内容: ")
	for stdinScanner.Scan() {
		fmt.Print("请输入内容: ")
		line := stdinScanner.Text()
		// 直接将全部内容发送过去即可
		_, err := io.WriteString(conn, line+"\n")
		if err != nil {
			log.Println("发送失败:", err)
			break
		}

		if line == "quit" {
			log.Println("退出")
			break
		}
	}
}

遇到的问题:

1、输入之后没有反应,服务端和客户端都没有响应:

io.WriteString的时候末尾必须加上换行符,因为使用bufio的时候读取时是以换行符分割的

2、不知道如何判断文件夹是否存在,以及如何遍历整个目录

3、还没有实现文件传输

io.Copy不会停止,一直读,直到读到 EOF(连接关闭)或发生错误才停止。
又由于这个TCP是长连接,然后直到TCP断开才会停止,因此应该使用io.CopyN

参考书籍

go program language中文版

英文版


文章作者: ZhangZhengYu
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 ZhangZhengYu !
评论
  目录