第一章
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