Skip to content

Latest commit

 

History

History
1635 lines (1294 loc) · 49.5 KB

File metadata and controls

1635 lines (1294 loc) · 49.5 KB

八、过程和信号

在上一章中,我们讨论了许多有趣的主题,包括使用 Unix 系统文件、处理 Go 中的日期和时间、查找有关文件权限和用户以及正则表达式和模式匹配的信息。

本章的中心主题是开发能够处理可以捕获和处理的 Unix 信号的 Go 应用。Go 提供用于处理信号的os/signal包,该包使用 Go 通道。虽然在下一章中将全面探讨这些通道,但这不会阻止您学习如何在 Go 程序中使用 Unix 信号。

此外,您还将学习如何创建可以使用 Unix 管道的 Go 命令行实用程序,如何在 Go 中绘制条形图,以及如何实现cat(1)实用程序的 Go 版本。因此,在本章中,您将了解以下主题:

  • 列出 Unix 计算机的进程
  • Go中的信号处理
  • Unix 机器支持的信号以及如何使用kill(1)命令发送这些信号
  • 让信号完成你想要的工作
  • 在 Go 中实现一个简单版本的cat(1)实用程序
  • 在 Go 中绘制数据
  • 使用管道将一个程序的输出发送到另一个程序
  • 将一个大程序转换为两个较小的程序,并在 Unix 管道的帮助下进行协作
  • 为 Unix 套接字创建客户端

关于 Unix 进程和信号

严格来说,流程是一个包含指令、用户数据和系统数据部分以及运行时获得的其他种类资源的执行环境,程序是一个包含指令和数据的文件,用于初始化进程的指令和用户数据部分。

过程管理

Go 一般不擅长处理流程和流程管理。不过,本节将介绍一个小型 Go 程序,它通过执行 Unix 命令并获取其输出来列出 Unix 机器的所有进程。该程序的名称将为listProcess.go。它可以在 Linux 和 macOS 系统上工作,将分三部分介绍。

该计划的第一部分如下:

package main 

import ( 
   "fmt" 
   "os" 
   "os/exec" 
   "syscall" 
) 

listProcess.go的第二部分有以下 Go 代码:

func main() { 

   PS, err := exec.LookPath("ps") 
   if err != nil { 
         fmt.Println(err) 
   } 
fmt.Println(PS) 

   command := []string{"ps", "-a", "-x"} 
   env := os.Environ() 
   err = syscall.Exec(PS, command, env) 

如您所见,首先需要使用exec.LookPath()获取可执行文件的路径,以确保不会意外执行另一个二进制文件,然后使用切片定义要执行的命令,包括命令的参数。接下来,您必须使用os.Environ()阅读 Unix 环境。此外,您还可以使用syscall.Exec()执行所需的命令,该命令将自动打印其输出,这不是一种非常优雅的执行命令的方式,因为您无法控制任务,并且您在最低级别调用流程,而不是使用更高级别的库,如os/exec

程序的最后一部分用于打印前一个代码的错误消息,如果有:

   if err != nil { 
         fmt.Println(err) 
   } 
} 

执行listProcess.go将生成以下输出:head(1)实用程序用于获得较小的输出:

$ go run listProcess.go | head -3
/bin/ps
  PID TTY           TIME CMD
    1 ??         0:30.72 /sbin/launchd
signal: broken pipe

关于 Unix 信号

您是否曾按过Ctrl+C以停止程序运行?如果是,那么您已经熟悉了信号,因为Ctrl+C向程序发送SIGINT信号。

严格地说,Unix信号是可以通过名称或数字访问的软件中断,并提供了一种处理异步事件的方法,例如当子进程退出或进程被告知在 Unix 系统上暂停时。

一个程序不能处理所有信号;其中一些是不可捕获和不可忽略的。无法捕获、阻止或忽略SIGKILLSIGSTOP信号。原因是它们为内核和根用户提供了一种停止任何进程的方法。SIGKILL信号,也被称为数字 9,通常在极端条件下被称为,你需要快速行动;因此,它是唯一一个通常通过数字调用的信号,因为这样做更快。这里要记住的最重要的一点是,并非所有 Unix 信号都可以处理!

Go中的 Unix 信号

Go 向程序员提供os/signal包,帮助他们处理传入信号。但是,我们将通过介绍kill(1)实用程序来开始关于处理的讨论。

kill(1)命令

kill(1)命令用于终止进程或向其发送不太残酷的信号。请记住,您可以向进程发送信号并不意味着该进程可以或有代码来处理该信号。

默认情况下,kill(1)发送SIGTERM信号。如果要查找 Unix 计算机支持的所有信号,应执行kill -l命令。在 macOS Sierra 机器上,kill -l的输出如下:

$ kill -l
1) SIGHUP   2) SIGINT        3) SIGQUIT   4) SIGILL
5) SIGTRAP  6) SIGABRT       7) SIGEMT    8) SIGFPE
9) SIGKILL 10) SIGBUS        11) SIGSEGV 12) SIGSYS
13) SIGPIPE 14) SIGALRM       15) SIGTERM 16) SIGURG
17) SIGSTOP 18) SIGTSTP       19) SIGCONT 20) SIGCHLD
21) SIGTTIN 22) SIGTTOU       23) SIGIO   24) SIGXCPU
25) SIGXFSZ 26) SIGVTALRM     27) SIGPROF 28) SIGWINCH
29) SIGINFO 30) SIGUSR1       31) SIGUSR2

如果在 Debian Linux 机器上执行相同的命令,您将获得更丰富的输出:

$ kill -l
 1) SIGHUP   2) SIGINT   3) SIGQUIT  4) SIGILL   5) SIGTRAP
 6) SIGABRT  7) SIGBUS   8) SIGFPE   9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT     17) SIGCHLD 
18) SIGCONT       19) SIGSTOP 20) SIGTSTP
21) SIGTTIN       22) SIGTTOU 
23) SIGURG        24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM     27) SIGPROF 28) SIGWINCH 
29) SIGIO         30) SIGPWR
31) SIGSYS        34) SIGRTMIN 
35) SIGRTMIN+1    36) SIGRTMIN+2    37) SIGRTMIN+3
38) SIGRTMIN+4    39) SIGRTMIN+5 
40) SIGRTMIN+6    41) SIGRTMIN+7    42) SIGRTMIN+8
43) SIGRTMIN+9    44) SIGRTMIN+10 
45) SIGRTMIN+11   46) SIGRTMIN+12   47) SIGRTMIN+13
48) SIGRTMIN+14   49) SIGRTMIN+15 
50) SIGRTMAX-14   51) SIGRTMAX-13   52) SIGRTMAX-12
53) SIGRTMAX-11   54) SIGRTMAX-10 
55) SIGRTMAX-9    56) SIGRTMAX-8    57) SIGRTMAX-7
58) SIGRTMAX-6    59) SIGRTMAX-5 
60) SIGRTMAX-4    61) SIGRTMAX-3    62) SIGRTMAX-2
63) SIGRTMAX-1    64) SIGRTMAX

如果您试图在没有所需权限的情况下终止或向另一个用户的进程发送另一个信号,如果您不是root用户,则最有可能发生这种情况,kill(1)将不会执行此任务,并且您将收到类似于以下内容的错误消息:

$ kill 2908
-bash: kill: (2908) - Operation not permitted

Go 中的一个简单信号处理器

本小节将介绍一个只处理SIGTERMSIGINT信号的简单 Go 程序。h1s.go的 Go 代码将分三部分呈现;第一部分如下:

package main 

import ( 
   "fmt" 
   "os" 
   "os/signal" 
   "syscall" 
   "time" 
) 

func handleSignal(signal os.Signal) { 
   fmt.Println("Got", signal) 
} 

除了程序的前导,还有一个名为handleSignal()的函数,当程序接收到两个支持的信号中的任何一个时,将调用该函数。

h1s.go的第二部分包含以下 Go 代码:

func main() { 
   sigs := make(chan os.Signal, 1) 
   signal.Notify(sigs, os.Interrupt, syscall.SIGTERM) 
   go func() { 
         for { 
               sig := <-sigs 
               fmt.Println(sig) 
               handleSignal(sig) 
         } 
   }() 

前面的代码使用了一个goroutine和一个 Go频道,这是本书中没有讨论过的 Go 特性。不幸的是,您必须等到第 9 章**Goroutines-基本功能才能了解这两种功能的更多信息。注意,尽管os.Interruptsyscall.SIGTERM属于不同的 Go 包,但它们都是信号。

*就目前而言,理解技术是很重要的;它包括三个步骤:

  1. 信道的定义,作为传递数据的一种方式,是技术所需的(sigs
  2. 调用signal.Notify()以定义您想要捕获的信号列表。
  3. 一个匿名函数的定义,该函数在signal.Notify()之后的 goroutine(go func())中运行,用于确定当您获得任何所需信号时要执行的操作。

在这种情况下,将调用handleSignal()函数。匿名函数中的for循环用于使程序保持处理所有信号,并且在收到第一个信号后不会停止。

h1s.go的最后一部分如下:

   for { 
         fmt.Printf(".") 
         time.Sleep(10 * time.Second) 
   } 
} 

这是一个无限的for循环,它会永远延迟程序的结束:您很可能会将程序的实际代码放在它的位置。执行h1s.go并从另一个终端向其发送信号将使h1s.go产生以下输出:

$ ./h1s
......................^Cinterrupt
Got interrupt
^Cinterrupt
Got interrupt
.Hangup: 1

这里的缺点是h1s.go在收到SIGHUP信号时会停止,因为SIGHUP在没有被程序专门处理时的默认动作是终止进程!下一小节将演示如何更好地处理三个信号,之后的小节将教您如何处理所有可以处理的信号。

处理三种不同的信号!

本小节将教您如何创建一个能够处理三种不同信号的 Go 应用:程序名称为h2s.go,它将处理SIGTERMSIGINTSIGHUP信号。

h2s.go的 Go 代码将分为四个部分。

计划的第一部分包含预期的序言:

package main 

import ( 
   "fmt" 
   "os" 
   "os/signal" 
   "syscall" 
   "time" 
) 

第二部分具有以下 Go 代码:

func handleSignal(signal os.Signal) { 
   fmt.Println("* Got:", signal) 
} 

func main() { 
   sigs := make(chan os.Signal, 1) 
   signal.Notify(sigs, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP) 

这里,最后一条语句告诉您,程序将只处理os.Interruptsyscall.SIGTERMsyscall.SIGHUP信号。

h2s.go的第三部分如下:

   go func() { 
         for { 
               sig := <-sigs 
               switch sig { 
               case os.Interrupt: 
                     handleSignal(sig) 
               case syscall.SIGTERM: 
                     handleSignal(sig) 
               case syscall.SIGHUP: 
                     fmt.Println("Got:", sig) 
                     os.Exit(-1) 
               } 
         } 
   }() 

在这里,您可以看到,当捕捉到给定信号时,不必调用单独的函数;它也可以在for循环中处理它,就像在syscall.SIGHUP中一样。但是,我发现使用命名函数更好,因为它使 Go 代码更易于阅读和修改。好的是 Go 有一个处理所有信号的中心位置,这使得你很容易发现你的程序在做什么。

另外,h2s.go专门处理SIGHUP信号,虽然SIGHUP信号仍会终止程序;然而,这一次是我们的决定。

请记住,让其中一个信号处理程序停止程序被认为是一种良好的做法,因为否则您必须通过发出kill -9命令来终止程序。

h2s.go的最后一部分如下:

   for { 
         fmt.Printf(".") 
         time.Sleep(10 * time.Second) 
   } 
}

执行h2s.go并从另一个 shell 向其发送四个信号(SIGINTSIGTERMSIGHUPSIGKILL),将产生以下输出:

$ go build h2s.go
$ ./h2s
..* Got: interrupt
* Got: terminated
.Got: hangup
.Killed: 9

构建h2s.go的原因是更容易找到自治程序的进程 ID:go run命令在后台构建临时可执行程序,在这种情况下灵活性较低。如果你想改进h2s.go,你可以让它调用os.Getpid()来打印它的进程 ID,这样你就不用自己去找了。

程序在得到无法处理的SIGKILL之前处理三个信号,因此终止它!

捕捉每一个可以处理的信号

本小节将介绍一种简单的技术,它允许您捕获每一个可以处理的信号:再一次,您应该记住,您不能处理所有的信号!一旦收到SIGTERM信号,程序将停止。

节目名称为catchAll.go,分三部分呈现。

第一部分如下:

package main 

import ( 
   "fmt" 
   "os" 
   "os/signal" 
   "syscall" 
   "time" 
) 

func handleSignal(signal os.Signal) { 
   fmt.Println("* Got:", signal) 
} 

该计划的第二部分如下:

func main() { 
   sigs := make(chan os.Signal, 1) 
   signal.Notify(sigs) 
   go func() { 
         for { 
               sig := <-sigs 
               switch sig { 
               case os.Interrupt: 
                     handleSignal(sig) 
               case syscall.SIGTERM: 
                     handleSignal(sig) 
                     os.Exit(-1) 
               case syscall.SIGUSR1: 
                     handleSignal(sig) 
               default: 
                     fmt.Println("Ignoring:", sig) 
               } 
         } 
   }() 

在这种情况下,所有的差异都是由您在代码中调用signal.Notify()的方式造成的。由于您没有定义任何特定的信号,程序将能够处理任何可以处理的信号。然而,匿名函数中的for循环只处理三个信号,而忽略其余的信号!请注意,我认为这是处理Go中信号的最佳方式:捕捉所有内容,同时只处理您感兴趣的信号。然而,有些人认为,明确你所处理的是一个更好的方法。这里没有对错之分。

catchAll.go程序在得到SIGHUP时不会终止,因为switch块的default案例处理它。

最后一部分是对time.Sleep()函数的预期调用:

   for { 
         fmt.Printf(".") 
         time.Sleep(10 * time.Second) 
   } 
} 

执行catchAll.go将创建以下输出:

$ ./catchAll
.Ignoring: hangup
.......................................* Got: interrupt
* Got: user defined signal 1
.Ignoring: user defined signal 2
Ignoring: hangup
.* Got: terminated
$

重新访问旋转日志文件!

正如我在第 7 章**处理系统文件中告诉您的,本章将向您介绍一种技术,该技术允许您在信号和信号处理的帮助下以更传统的方式结束程序并旋转日志文件。

*新版rotateLog.go的名称为rotateSignals.go,分四个部分介绍。此外,当实用程序收到os.Interrupt时,它将旋转当前日志文件,而当它收到syscall.SIGTERM时,它将终止其执行。可以处理的每个其他信号都将创建一个日志条目,而无需任何其他操作。

rotateSignals.go的第一部分是预期的序言:

package main 

import ( 
   "fmt" 
   "log" 
   "os" 
   "os/signal" 
   "strconv" 
   "syscall" 
   "time" 
) 

var TOTALWRITES int = 0 
var openLogFile os.File 

rotateSignals.go的第二部分有以下 Go 代码:

func rotateLogFile(filename string) error { 
   openLogFile.Close() 
   os.Rename(filename, filename+"."+strconv.Itoa(TOTALWRITES)) 
   err := setUpLogFile(filename) 
   return err 
} 

func setUpLogFile(filename string) error { 
   openLogFile, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644) 
   if err != nil { 
         return err 
   } 
   log.SetOutput(openLogFile) 
   return nil 
} 

您刚才在这里定义了两个函数,它们执行两个任务。rotateSignals.go的第三部分包含以下 Go 代码:

func main() { 
   filename := "/tmp/myLog.log" 
   err := setUpLogFile(filename) 
   if err != nil { 
         fmt.Println(err) 
         os.Exit(-1) 
   } 

   sigs := make(chan os.Signal, 1) 
   signal.Notify(sigs) 

再一次,所有信号都将被捕获。rotateSignals.go的最后一部分如下:

   go func() { 
         for { 
               sig := <-sigs 
               switch sig { 
               case os.Interrupt: 
                     rotateLogFile(filename) 
                     TOTALWRITES++ 
               case syscall.SIGTERM: 
                     log.Println("Got:", sig) 
                     openLogFile.Close() 
                     TOTALWRITES++ 
                     fmt.Println("Wrote", TOTALWRITES, "log entries in total!") 
                     os.Exit(-1) 
               default: 
                     log.Println("Got:", sig) 
                     TOTALWRITES++ 
               } 
         } 
   }() 

   for { 
         time.Sleep(10 * time.Second) 
   } 
} 

如您所见,rotateSignals.go通过为每个信号写入一个日志条目来记录其接收到的信号的相关信息。虽然展示rotateSignals.go的整个代码很好,但是看到diff(1)实用程序的输出以显示rotateLog.gorotateSignals.go之间的代码差异将是非常有教育意义的:

$ diff rotateLog.go rotateSignals.go
6a7
>     "os/signal"
7a9
>     "syscall"
12,13d13
< var ENTRIESPERLOGFILE int = 100
< var WHENTOSTOP int = 230
33d32
<     numberOfLogEntries := 0
41,51c40,59
<     for {
<           log.Println(numberOfLogEntries, "This is a test log entry")
<           numberOfLogEntries++
<           TOTALWRITES++
<           if numberOfLogEntries > ENTRIESPERLOGFILE {
<                 _ = rotateLogFile(filename)
<                 numberOfLogEntries = 0
<           }
<           if TOTALWRITES > WHENTOSTOP {
<                 _ = rotateLogFile(filename)
<                 break
---
>     sigs := make(chan os.Signal, 1)
>     signal.Notify(sigs)
>
>     go func() {
>           for {
>                 sig := <-sigs
>                 switch sig {
>                 case os.Interrupt:
>                       rotateLogFile(filename)
>                       TOTALWRITES++
>                 case syscall.SIGTERM:
>                       log.Println("Got:", sig)
>                       openLogFile.Close()
>                       TOTALWRITES++
>                       fmt.Println("Wrote", TOTALWRITES, "log entries in total!")
>                       os.Exit(-1)
>                 default:
>                       log.Println("Got:", sig)
>                       TOTALWRITES++
>                 }
53c61,64
<           time.Sleep(time.Second)
---
>     }()
>
>     for {
>           time.Sleep(10 * time.Second)
55d65
<     fmt.Println("Wrote", TOTALWRITES, "log entries!")

这里的好处是rotateSignals.go中使用的信号使得rotateLog.go中使用的大多数全局变量变得不必要,因为您现在可以通过发送信号来控制实用程序。此外,rotateSignals.go的设计和结构比rotateLog.go简单,因为您只需要了解匿名函数的作用。

执行rotateSignals.go并向其发送一些信号后,/tmp/myLog.log的内容如下:

$ cat /tmp/myLog.log
2017/06/03 14:53:33 Got: user defined signal 1
2017/06/03 14:54:08 Got: user defined signal 1
2017/06/03 14:54:12 Got: user defined signal 2
2017/06/03 14:54:19 Got: terminated

此外,您将在/tmp中拥有以下文件:

$ ls -l /tmp/myLog.log*
-rw-r--r--  1 mtsouk  wheel  177 Jun  3 14:54 /tmp/myLog.log
-rw-r--r--  1 mtsouk  wheel  106 Jun  3 13:42 /tmp/myLog.log.0

改进文件复制

原始cp(1)实用程序在收到SIGINFO信号时打印有用信息,如下输出所示:

$ cp FileToCopy /tmp/copy
FileToCopy -> /tmp/copy  26%
FileToCopy -> /tmp/copy  29%
FileToCopy -> /tmp/copy  31%

因此,本节的其余部分将实现与cp(1)命令的 Go 实现相同的功能。本节中的 Go 代码将基于cp.go程序,因为当使用较小的缓冲区时,它会非常慢,给我们测试时间。新复制实用程序的名称将为cpSignal.go,分为四个部分。

cpSignal.gocp.go之间的根本区别在于cpSignal.go应该找到输入文件的大小,并保留在给定点写入的字节数。除了这些修改之外,您不必担心其他问题,因为这两个版本的核心功能(复制文件)完全相同。

该计划的第一部分如下:

package main 

import ( 
   "fmt" 
   "io" 
   "os" 
   "os/signal" 
   "path/filepath" 
   "strconv" 
   "syscall" 
) 

var BUFFERSIZE int64 
var FILESIZE int64 
var BYTESWRITTEN int64 

为了简化开发人员的工作,程序引入了两个全局变量FILESIZEBYTESWRITTEN,它们分别保持输入文件的大小和已写入的字节数。处理SIGINFO信号的函数使用这两个变量。

第二部分内容如下:

func Copy(src, dst string, BUFFERSIZE int64) error { 
   sourceFileStat, err := os.Stat(src) 
   if err != nil { 
         return err 
   } 

   FILESIZE = sourceFileStat.Size() 

   if !sourceFileStat.Mode().IsRegular() { 
         return fmt.Errorf("%s is not a regular file.", src) 
   } 

   source, err := os.Open(src) 
   if err != nil { 
         return err 
   } 
   defer source.Close() 

   _, err = os.Stat(dst) 
   if err == nil { 
         return fmt.Errorf("File %s already exists.", dst) 
   } 

   destination, err := os.Create(dst) 
   if err != nil { 
         return err 
   } 
   defer destination.Close() 

   if err != nil { 
         panic(err) 
   } 

   buf := make([]byte, BUFFERSIZE) 
   for { 
         n, err := source.Read(buf) 
         if err != nil && err != io.EOF { 
               return err 
         } 
         if n == 0 { 
               break 
         } 
         if _, err := destination.Write(buf[:n]); err != nil { 
               return err 
         } 
         BYTESWRITTEN = BYTESWRITTEN + int64(n) 
   } 
   return err 
} 

在这里,您使用sourceFileStat.Size()函数获取输入文件的大小,并设置FILESIZE全局变量的值。

第三部分是定义信号处理的部分:

func progressInfo() { 
   progress := float64(BYTESWRITTEN) / float64(FILESIZE) * 100 
   fmt.Printf("Progress: %.2f%%\n", progress) 
} 

func main() { 
   if len(os.Args) != 4 { 
         fmt.Printf("usage: %s source destination BUFFERSIZE\n", filepath.Base(os.Args[0])) 
         os.Exit(1) 
   } 

   source := os.Args[1] 
   destination := os.Args[2] 
   BUFFERSIZE, _ = strconv.ParseInt(os.Args[3], 10, 64) 
   BYTESWRITTEN = 0 

   sigs := make(chan os.Signal, 1) 
   signal.Notify(sigs) 

在这里,您选择捕捉所有信号。但匿名函数的 Go 码只有在收到syscall.SIGINFO信号后才会调用progressInfo()

如果你想有一种优雅地终止程序的方法,你可能想使用SIGINT信号,因为当捕获所有信号时,优雅地终止程序不再是可能的:你需要发送SIGKILL来终止你的程序,这有点残忍。

cpSignal.go的最后一部分如下:

   go func() { 
         for {
               sig := <-sigs 
               switch sig { 
               case syscall.SIGINFO:
                     progressInfo() 
               default: 
                     fmt.Println("Ignored:", sig) 
               } 
         } 
   }() 

   fmt.Printf("Copying %s to %s\n", source, destination) 
   err := Copy(source, destination, BUFFERSIZE) 
   if err != nil { 
         fmt.Printf("File copying failed: %q\n", err) 
   } 
} 

执行cpSignal.go并向其发送两个SIGINFO信号将产生以下输出:

$ ./cpSignal FileToCopy /tmp/copy 2
Copying FileToCopy to /tmp/copy
Ignored: user defined signal 1
Progress: 21.83%
^CIgnored: interrupt
Progress: 29.78%

绘图数据

本节中开发的实用程序将读取多个日志文件,并将创建一个图形图像,其中包含与读取的日志文件数量相同的条形图。每个条表示在日志文件中找到给定 IP 地址的次数。

但是,Unix 的原理告诉我们,与其开发单个实用程序,不如开发两个不同的实用程序:一个用于处理日志文件和创建报告,另一个用于打印第一个实用程序生成的数据:这两个实用程序将使用 Unix 管道进行通信。虽然本节将实现第一种方法,但您将在本章节中的plotIP.go实用程序中看到第二种方法的实现。

这个实用程序的想法来自于我为一本杂志编写的一个教程,在那里我开发了一个小型Go程序,它可以进行一些策划:即使是小而幼稚的程序也可以激发你开发更大的东西,所以不要低估它们的威力。

实用程序的名称将是plotIP.go,它将分为七个部分:好的是plotIP.go将重用countIP.gofindIP.go的部分代码。plotIP.go唯一不做的事情是将文本写入图像,因此您只能在不知道特定条的实际值或相应日志文件的情况下绘制条:您可以尝试向程序添加文本功能作为练习。

此外,plotIP.go将至少需要三个参数,即图像的宽度和高度以及将使用的日志文件的名称:为了使plotIP.go更小,plotIP.go将不使用flag包,并假设您将以正确的顺序给出其参数。如果你给它更多的参数,它会把它们当作日志文件。

plotIP.go的第一部分如下:

package main 

import ( 
   "bufio" 
   "fmt" 
   "image" 
   "image/color" 
   "image/png" 
   "io" 
   "os" 
   "path/filepath" 
   "regexp" 
   "strconv" 
) 

var m *image.NRGBA
var x int 
var y int 
var barWidth int 

这些全局变量与图像的尺寸(xy、作为 Go 变量的图像(m)以及其中一个条形图(barWidth)的宽度有关,这取决于图像的大小和将要绘制的条形图的数量。请注意,在这里使用xy作为变量名,而不是像IMAGEWIDTHIMAGEHEIGHT这样的名称可能有点错误和危险。

第二部分如下:

func findIP(input string) string { 
   partIP := "(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])" 
   grammar := partIP + "\\." + partIP + "\\." + partIP + "\\." + partIP 
   matchMe := regexp.MustCompile(grammar) 
   return matchMe.FindString(input) 
} 

func plotBar(width int, height int, color color.RGBA) { 
   xx := 0
   for xx < barWidth { 
         yy := 0 
         for yy < height { 
               m.Set(xx+width, y-yy, color) 
               yy = yy + 1 
         } 
         xx = xx + 1 
   } 
} 

在这里,您实现了一个名为plotBar()的 Go 函数,该函数根据每个条的高度、宽度和颜色绘制每个条。此功能是plotIP.go中最具挑战性的部分。

第三部分具有以下 Go 代码:

func getColor(x int) color.RGBA { 
   switch {

   case x == 0: 
         return color.RGBA{0, 0, 255, 255} 
   case x == 1: 
         return color.RGBA{255, 0, 0, 255} 
   case x == 2: 
         return color.RGBA{0, 255, 0, 255} 
   case x == 3: 
         return color.RGBA{255, 255, 0, 255} 
   case x == 4: 
         return color.RGBA{255, 0, 255, 255} 
   case x == 5: 
         return color.RGBA{0, 255, 255, 255} 
   case x == 6: 
         return color.RGBA{255, 100, 100, 255} 
   case x == 7: 
         return color.RGBA{100, 100, 255, 255} 
   case x == 8: 
         return color.RGBA{100, 255, 255, 255} 
   case x == 9: 
         return color.RGBA{255, 255, 255, 255} 
   } 
   return color.RGBA{0, 0, 0, 255} 
} 

此函数允许您定义输出中显示的颜色:如果需要,您可以更改这些颜色。

第四部分包含以下 Go 代码:

func main() { 
   var data []int 
   arguments := os.Args 
   if len(arguments) < 4 { 
         fmt.Printf("%s X Y IP input\n", filepath.Base(arguments[0])) 
         os.Exit(0) 
   } 

   x, _ = strconv.Atoi(arguments[1]) 
   y, _ = strconv.Atoi(arguments[2]) 
   WANTED := arguments[3] 
   fmt.Println("Image size:", x, y) 

这里,您读取保存在WANTED变量中的所需 IP 地址,并读取生成的 PNG 图像的尺寸。

第五部分包含以下 Go 代码:

   for _, filename := range arguments[4:] { 
         count := 0 
         fmt.Println(filename) 
         f, err := os.Open(filename) 
         if err != nil { 
               fmt.Fprintf(os.Stderr, "Error: %s\n", err) 
               continue 
         } 
         defer f.Close() 

         r := bufio.NewReader(f) 
         for { 
               line, err := r.ReadString('\n') 
               if err == io.EOF { 
                     break 
               } 

if err != nil { 
                fmt.Fprintf(os.Stderr, "Error in file: %s\n", err) 
                     continue 
               } 
               ip := findIP(line) 
               if ip == WANTED { 
                     count++

               } 
         } 
         data = append(data, count) 
   } 

在这里,您逐个处理输入日志文件,并将计算的值存储在data切片中。将错误消息打印到os.Stderr:将错误消息打印到os.Stderr的主要优点是,您可以轻松地将错误消息重定向到文件,同时以不同的方式使用写入os.Stdout的数据。

plotIP.go的第六部分包含以下 Go 代码:

   fmt.Println("Slice length:", len(data)) 
   if len(data)*2 > x { 
         fmt.Println("Image size (x) too small!") 
         os.Exit(-1) 
   } 

   maxValue := data[0] 
   for _, temp := range data { 
         if maxValue < temp { 
               maxValue = temp 
         } 
   } 

   if maxValue > y { 
         fmt.Println("Image size (y) too small!") 
         os.Exit(-1) 
   } 
   fmt.Println("maxValue:", maxValue) 
   barHeighPerUnit := int(y / maxValue) 
   fmt.Println("barHeighPerUnit:", barHeighPerUnit) 
   PNGfile := WANTED + ".png" 
   OUTPUT, err := os.OpenFile(PNGfile, os.O_CREATE|os.O_WRONLY, 0644) 
   if err != nil { 
         fmt.Println(err) 
         os.Exit(-1) 
   } 
   m = image.NewNRGBA(image.Rectangle{Min: image.Point{0, 0}, Max: image.Point{x, y}}) 

在这里,您计算有关绘图的内容,并使用os.OpenFile()创建输出图像文件。plotIP.go实用程序生成的 PNG 文件以给定的 IP 地址命名,以简化操作。

plotIP.go的 Go 代码的最后一部分如下:

   i := 0 
   barWidth = int(x / len(data)) 
   fmt.Println("barWidth:", barWidth) 
   for _, v := range data { 
         c := getColor(v % 10) 
         yy := v * barHeighPerUnit 
         plotBar(barWidth*i, yy, c) 
         fmt.Println("plotBar", barWidth*i, yy) 
         i = i + 1 
   } 
   png.Encode(OUTPUT, m) 
} 

在这里,您读取data切片的值,并通过调用plotBar()函数为每个切片创建一个条。

执行plotIP.go将生成以下输出:

$ go run plotIP.go 1300 1500 127.0.0.1 /tmp/log.*
Image size: 1300 1500
/tmp/log.1
/tmp/log.2
/tmp/log.3
Slice length: 3
maxValue: 1500
barHeighPerUnit: 1
barWidth: 433
plotBar 0 1500
plotBar 433 1228
plotBar 866 532
$  ls -l 127.0.0.1.png
-rw-r--r-- 1 mtsouk mtsouk 11023 Jun  5 18:36 127.0.0.1.png

但是,除了生成的文本输出外,重要的是生成的 PNG 文件,如下图所示:

plotIP.go 实用程序生成的输出

如果要将错误消息保存到其他文件,可以使用以下命令的变体:

$ go run plotIP.go 130 150 127.0.0.1 doNOTExist 2> err
Image size: 130 150
doNOTExist
Slice length: 0
$ cat err
Error: open doNOTExist: no such file or directory
panic: runtime error: index out of range

goroutine 1 [running]:
main.main()
     /Users/mtsouk/Desktop/goBook/ch/ch8/code/plotIP.go:112 +0x12de
exit status 2

以下命令通过将所有错误消息发送到/dev/null来丢弃它们:

$ go run plotIP.go 1300 1500 127.0.0.1 doNOTExist 2>/dev/null
Image size: 1300 1500
doNOTExist
Slice length: 0  

Go 中的 Unix 管道

我们首先在第 6 章、**文件输入和输出中谈到管道。管道有两个严重的限制:第一,它们通常在一个方向上通信;第二,它们只能在具有共同祖先的进程之间使用。

管道背后的一般思想是,如果您没有要处理的文件,您应该等待从标准输入获取输入。类似地,如果没有人告诉您将输出保存到文件中,您应该将输出写入标准输出,以便用户查看或其他程序处理。因此,管道可以用于在两个进程之间传输数据,而无需创建任何临时文件。

本节将介绍一些用 Go 编写的简单实用程序,为了清晰起见,它们使用 Unix 管道。

从标准输入读取

要开发支持 Unix 管道的 Go 应用,首先需要知道的是如何读取标准输入。

开发的程序名为readSTDIN.go,将分三部分介绍。

计划的第一部分是预期的序言:

package main 

import ( 
   "bufio" 
   "fmt" 
   "os" 
) 

readSTDIN.go的第二部分有以下 Go 代码:

func main() { 
   filename := "" 
   var f *os.File 
   arguments := os.Args 
   if len(arguments) == 1 { 
         f = os.Stdin 
   } else { 
         filename = arguments[1] 
         fileHandler, err := os.Open(filename) 
         if err != nil { 
               fmt.Printf("error opening %s: %s", filename, err) 
               os.Exit(1) 
         } 
         f = fileHandler 
   } 
   defer f.Close() 

在这里,您可以确定是否有实际的文件要处理,这可以由程序的命令行参数的数量来确定。如果您没有要处理的文件,您将尝试从os.Stdin读取数据。确保您理解所介绍的技术,因为本章将多次使用该技术。

readSTDIN.go的最后一部分如下:

   scanner := bufio.NewScanner(f) 
   for scanner.Scan() { 
         fmt.Println(">", scanner.Text()) 
   } 
} 

无论您处理的是实际文件还是os.Stdin,此代码都是相同的,因为 Unix 中的所有内容都是文件。请注意,程序输出以>字符开头。

执行readSTDIN.go将生成以下输出:

$ cat /tmp/testfile
1
2
$ go run readSTDIN.go /tmp/testFile
> 1
> 2
$ cat /tmp/testFile | go run readSTDIN.go
> 1
> 2
$ go run readSTDIN.go
3
> 3
2
> 2
1
> 1

在最后一种情况下,readSTDIN.go回显它读取的每一行,因为输入是逐行读取的:cat(1)实用程序的工作方式相同。

将数据发送到标准输出

本小节将向您展示如何以比仅使用fmt.Println()fmt标准 Go 包中的任何其他功能更好的方式将数据发送到标准输出。Go程序将命名为writeSTDOUT.go,分三部分呈现给您。

第一部分如下:

package main 

import ( 
   "io" 
   "os" 
) 

writeSTDOUT.go的第二部分有以下 Go 代码:

func main() { 
   myString := "" 
   arguments := os.Args 
   if len(arguments) == 1 { 
         myString = "You did not give an argument!" 
   } else { 
         myString = arguments[1] 
   } 

writeSTDOUT.go的最后一部分如下:

   io.WriteString(os.Stdout, myString) 
   io.WriteString(os.Stdout, "\n") 
} 

唯一微妙的是,在使用io.WriteString()将数据写入os.Stdout之前,您需要将文本放入一个片段中。

执行writeSTDOUT.go将生成以下输出:

$ go run writeSTDOUT.go 123456
123456
$ go run writeSTDOUT.go
You do not give an argument!

在 Go 中实现 cat(1)

本小节将介绍cat(1)命令行实用程序的 Go 版本。如果您为cat(1)提供一个或多个命令行参数,则cat(1)将在屏幕上打印其内容。但是,如果您只是在 Unix shell 上键入cat(1),则cat(1)将等待您的输入,当您键入Ctrl+D时,输入将终止。

Go 实现的名称将为cat.go,并将分为三个部分。

cat.go的第一部分如下:

package main 

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

第二部分如下:

func catFile(filename string) error { 
   f, err := os.Open(filename) 
   if err != nil { 
         return err 
   } 
   defer f.Close() 
   scanner := bufio.NewScanner(f) 
   for scanner.Scan() { 
         fmt.Println(scanner.Text()) 
   } 
   return nil 
} 

cat.go实用程序必须处理实际文件时,调用catFile()函数。有一个功能来完成你的工作,会使程序的设计变得更好。

最后一部分具有以下 Go 代码:

func main() { 
   filename := "" 
   arguments := os.Args 
   if len(arguments) == 1 { 
         io.Copy(os.Stdout, os.Stdin) 
         os.Exit(0) 
   } 

   filename = arguments[1] 
   err := catFile(filename) 
   if err != nil { 
         fmt.Println(err) 
   } 
} 

因此,如果程序没有参数,则假定它必须读取os.Stdin。在这种情况下,它只是重复你给它的每一行。如果程序有参数,则使用catFile()函数将第一个参数作为文件处理。

执行cat.go将生成以下输出:

$ go run cat.go /tmp/testFile  |  go run cat.go
1
2
$ go run cat.go
Mihalis
Mihalis
Tsoukalos
Tsoukalos $ echo "Mihalis Tsoukalos" | go run cat.go
Mihalis Tsoukalos

重新访问 plotIP.go 实用程序

正如本章前一节所承诺的,本节将创建两个单独的实用程序,当组合起来时,将实现plotIP.go的功能。就我个人而言,我更喜欢有两个独立的实用程序,并在需要时将它们组合起来,而不是只有一个实用程序可以完成两个或多个任务。

这两个公用设施的名称为extractData.goplotData.go。正如您很容易理解的,只有第二个实用程序才能从标准输入中获取输入,只要第一个实用程序使用os.Stdout(这是正确的方式)或使用fmt.Println()(通常执行此任务)在标准输出上打印其输出。

我想我现在应该告诉你们我的小秘密:我先创建了extractData.goplotData.go,然后开发了plotIP.go,因为开发两个独立的实用程序比开发一个更大的实用程序更容易!此外,使用两种不同的实用程序,您可以使用标准 Unix 实用程序(如tail(1)sort(1)head(1))过滤extractData.go的输出,这意味着您可以以不同的方式修改数据,而无需编写任何额外的 Go 代码。

使用两个命令行实用程序并创建一个实现这两个实用程序功能的实用程序要比使用一个大型实用程序并将其功能划分为两个或多个不同的实用程序容易,因为后者通常需要更多的变量和更多的错误检查。

extractData.go实用程序将分四部分介绍;第一部分如下:

package main 

import ( 
   "bufio" 
   "fmt" 
   "io" 
   "os" 
   "path/filepath" 
   "regexp" 
) 

extractData.go的第二部分有以下 Go 代码:

func findIP(input string) string { 
   partIP := "(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])" 
   grammar := partIP + "\\." + partIP + "\\." + partIP + "\\." + partIP 
   matchMe := regexp.MustCompile(grammar) 
   return matchMe.FindString(input) 
} 

您应该熟悉findIP()功能,您在第 7 章*、*中看到的findIP.go处理系统文件

extractData.go的第三部分如下:

func main() { 
   arguments := os.Args 
   if len(arguments) < 3 { 
         fmt.Printf("%s IP <files>\n", filepath.Base(os.Args[0])) 
         os.Exit(-1) 
   } 

   WANTED := arguments[1] 
   for _, filename := range arguments[2:] { 
         count := 0 
         buf := []byte(filename)
         io.WriteString(os.Stdout, string(buf)) 
         f, err := os.Open(filename) 
         if err != nil { 
               fmt.Fprintf(os.Stderr, "Error: %s\n", err) 
               continue 
         } 
         defer f.Close() 

这里使用buf变量是多余的,因为filename是一个字符串,io.WriteString()需要一个字符串:我习惯将filename的值放入字节片中。如果需要,可以将其删除。

同样,大部分 Go 代码都来自plotIP.go实用程序。extractData.go的最后一部分如下:

         r := bufio.NewReader(f) 
         for { 
               line, err := r.ReadString('\n') 
               if err == io.EOF { 
                     break 
               } else if err != nil { 
                     fmt.Fprintf(os.Stderr, "Error in file: %s\n", err) 
                     continue 
               } 

               ip := findIP(line) 
               if ip == WANTED { 
                     count = count + 1 
               } 
         } 
         buf = []byte(strconv.Itoa(count))
         io.WriteString(os.Stdout, " ") 
         io.WriteString(os.Stdout, string(buf)) 
         io.WriteString(os.Stdout, "\n") 
   } 
} 

这里,extractData.go将其输出写入标准输出(os.Stdout,而不是使用fmt包的功能,以便与管道更兼容。extractData.go实用程序至少需要两个参数:IP 地址和日志文件,但它可以处理任意数量的日志文件。

您可能希望将filename值的打印从第三部分移到此处,以便所有打印命令都位于同一位置。

执行extractData.go将生成以下输出:

$ ./extractData 127.0.0.1 access.log{,.1}
access.log 3099
access.log.1 6333

虽然extractData.go每行打印两个值,plotData.go只使用第二个字段。最好的方法是使用awk(1)过滤extractData.go的输出:

$ ./extractData 127.0.0.1 access.log{,.1} | awk '{print $2}'
3099
6333

正如您所理解的,awk(1)允许您使用生成的值做更多的事情。

plotData.go实用程序也将分六部分介绍;其第一部分如下:

package main 

import ( 
   "bufio" 
   "fmt" 
   "image" 
   "image/color" 
   "image/png" 
   "os" 
   "path/filepath" 
   "strconv" 
) 

var m *image.NRGBA 
var x int 
var y int 
var barWidth int 

同样,使用全局变量是为了避免将过多的参数传递给实用程序的某些函数。

plotData.go的第二部分包含以下 Go 代码:

func plotBar(width int, height int, color color.RGBA) { 
   xx := 0
   for xx < barWidth { 
         yy := 0 
         for yy < height { 
               m.Set(xx+width, y-yy, color) 
               yy = yy + 1 
         } 
         xx = xx + 1 
   } 
} 

plotData.go的第三部分有以下 Go 代码:

func getColor(x int) color.RGBA { 
   switch {
   case x == 0: 
         return color.RGBA{0, 0, 255, 255} 
   case x == 1: 
         return color.RGBA{255, 0, 0, 255} 
   case x == 2: 
         return color.RGBA{0, 255, 0, 255} 
   case x == 3: 
         return color.RGBA{255, 255, 0, 255} 
   case x == 4: 
         return color.RGBA{255, 0, 255, 255} 
   case x == 5: 
         return color.RGBA{0, 255, 255, 255} 
   case x == 6: 
         return color.RGBA{255, 100, 100, 255} 
   case x == 7: 
         return color.RGBA{100, 100, 255, 255} 
   case x == 8: 
         return color.RGBA{100, 255, 255, 255} 
   case x == 9: 
         return color.RGBA{255, 255, 255, 255} 
   } 
   return color.RGBA{0, 0, 0, 255} 
} 

plotData.go的第四部分包含以下 Go 代码:

func main() { 
   var data []int 
   var f *os.File 
   arguments := os.Args 
   if len(arguments) < 3 { 
         fmt.Printf("%s X Y input\n", filepath.Base(arguments[0])) 
         os.Exit(0) 
   } 

   if len(arguments) == 3 { 
         f = os.Stdin 
   } else { 
         filename := arguments[3] 
         fTemp, err := os.Open(filename) 
         if err != nil { 
               fmt.Println(err) 
               os.Exit(0) 
         } 
         f = fTemp 
   } 
   defer f.Close() 

   x, _ = strconv.Atoi(arguments[1]) 
   y, _ = strconv.Atoi(arguments[2]) 
   fmt.Println("Image size:", x, y) 

plotData.go的第五部分如下:

   scanner := bufio.NewScanner(f) 
   for scanner.Scan() { 
         value, err := strconv.Atoi(scanner.Text()) 
         if err == nil { 
               data = append(data, value) 
         } else { 
               fmt.Println("Error:", value) 
         } 
   } 

   fmt.Println("Slice length:", len(data)) 
   if len(data)*2 > x { 
         fmt.Println("Image size (x) too small!") 
         os.Exit(-1) 
   } 

   maxValue := data[0] 
   for _, temp := range data { 
         if maxValue < temp { 
               maxValue = temp 
         } 
   } 

   if maxValue > y { 
         fmt.Println("Image size (y) too small!") 
         os.Exit(-1) 
   } 
   fmt.Println("maxValue:", maxValue) 
   barHeighPerUnit := int(y / maxValue) 
   fmt.Println("barHeighPerUnit:", barHeighPerUnit) 

plotData.go的最后一部分如下:

   PNGfile := arguments[1] + "x" + arguments[2] + ".png" 
   OUTPUT, err := os.OpenFile(PNGfile, os.O_CREATE|os.O_WRONLY, 0644) 
   if err != nil { 
         fmt.Println(err) 
         os.Exit(-1) 
   } 
   m = image.NewNRGBA(image.Rectangle{Min: image.Point{0, 0}, Max: image.Point{x, y}}) 

   i := 0 
   barWidth = int(x / len(data)) 
   fmt.Println("barWidth:", barWidth) 
   for _, v := range data { 
         c := getColor(v % 10) 
         yy := v * barHeighPerUnit 
         plotBar(barWidth*i, yy, c) 
         fmt.Println("plotBar", barWidth*i, yy) 
         i = i + 1 
   } 

   png.Encode(OUTPUT, m) 
} 

虽然您可以单独使用plotData.go,但使用extractData.go的输出作为plotData.go的输入,与执行以下命令一样简单:

$ ./extractData.go 127.0.0.1 access.log{,.1} | awk '{print $2}' | ./plotData 6000 6500
Image size: 6000 6500
Slice length: 2
maxValue: 6333
barHeighPerUnit: 1
barWidth: 3000
plotBar 0 3129
plotBar 3000 6333
$ ls -l 6000x6500.png
-rw-r--r-- 1 mtsouk mtsouk 164915 Jun  5 18:25 6000x6500.png

上一个命令的图形输出可以是如下图所示的图像:

plotData.go 实用程序生成的输出

Go 中的 Unix 套接字

存在两种套接字:Unix 套接字和网络套接字。网络套接字将在第 12 章、**网络编程中进行说明,而 Unix 套接字将在本节中进行简要说明。但是,由于所介绍的 Go 函数也适用于 TCP/IP 套接字,因此您仍然需要等到第 12 章网络编程,才能完全理解它们,因为这里将不进行解释。因此,本节将介绍 Unix 套接字客户端的 Go 代码,这是一个使用 Unix 套接字(一个特殊的 Unix 文件)来读写数据的程序。节目名称为readUNIX.go,分三部分呈现。

第一部分如下:

package main 

import ( 
   "fmt" 
   "io" 
   "net" 
   "strconv" 
   "time" 
) 

readUNIX.go的第二部分如下:

func readSocket(r io.Reader) { 
   buf := make([]byte, 1024) 
   for { 
         n, _ := r.Read(buf[:]) 
         fmt.Print("Read: ", string(buf[0:n])) 
   } 
} 

最后一部分包含以下 Go 代码:

func main() { 
   c, _ := net.Dial("unix", "/tmp/aSocket.sock") 
   defer c.Close() 

   go readSocket(c) 
   n := 0 
   for { 
         message := []byte("Hi there: " + strconv.Itoa(n) + "\n") 
         _, _ = c.Write(message) 
         time.Sleep(5 * time.Second) 
         n = n + 1 
   } 
} 

readUNIX.go的使用需要存在另一个进程,该进程也可以读取和写入同一套接字文件(/tmp/aSocket.sock

生成的输出取决于其他部分的实现:在本例中,该输出如下所示:

$ go run readUNIX.go
Read: Hi there: 0
Read: Hi there: 1

如果找不到套接字文件或没有任何程序在监视它,您将收到以下错误消息:

panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x10cfe77]

goroutine 1 [running]:
main.main()
      /Users/mtsouk/Desktop/goBook/ch/ch8/code/readUNIX.go:21 +0x67
exit status 2

Go中的 RPC

RPC 代表远程过程调用,它是一种执行对远程服务器的函数调用并从客户端获取答案的方法。再一次,您必须等到第 12 章*、*网络编程,才能学习如何在 Go 中开发 RPC 服务器和 RPC 客户端。

在 Go 中编程 unixshell

本节将简要地、天真地呈现 GO 代码,这些代码可以作为开发 UNIX 外壳的基础。除了exit命令外,程序可以识别的唯一其他命令是只打印程序版本的version命令。所有其他用户输入将在屏幕上回响。

UNIXshell.go的 Go 代码将分三部分介绍。然而,在此之前,我将向您展示 shell 的第一个版本,其中主要包含注释,以便更好地理解我通常如何开始实施一个相对具有挑战性的程序:

package main 

import ( 
   "fmt" 
) 

func main() { 

   // Present prompt 

   // Read a line 

   // Get the first word of the line 

   // If it is a built-in shell command, execute the command 

   // otherwise, echo the command 

} 

这或多或少是我将用作起点的算法:好的是,注释简要说明了程序将如何运行。请记住,算法不依赖于编程语言。在那之后,开始实施事情就更容易了,因为你知道你想做什么。

因此,shell 最终版本的第一部分如下所示:

package main 

import ( 
   "bufio" 
   "fmt" 
   "os" 
   "strings" 
) 

var VERSION string = "0.2" 

第二部分如下:

func main() { 
   scanner := bufio.NewScanner(os.Stdin) 
   fmt.Print("> ") 
   for scanner.Scan() { 

         line := scanner.Text() 
         words := strings.Split(line, " ") 
         command := words[0] 

在这里,您只需逐行读取来自用户的输入,并找出输入的第一个字。

UNIXshell.go的最后一部分如下:

         switch command { 
         case "exit": 
               fmt.Println("Exiting...") 
               os.Exit(0) 
         case "version": 
               fmt.Println(VERSION) 
         default: 
               fmt.Println(line) 
         } 

         fmt.Print("> ") 
   } 
} 

前面提到的 Go 代码检查用户发出的命令并相应地执行。

执行UNIXshell.go并与之交互将生成以下输出:

$ go run UNIXshell.go
> version
0.2
> ls -l
ls -l
> exit
Exiting...

如果您想了解更多关于在 Go 中创建自己的 Unix shell 的信息,可以访问https://github.com/elves/elvish

还有一个小的 Go 更新

在我写这一章的时候,Go 被更新了:这是一个小的更新,主要修复了 bug:

$ date
Thu May 25 06:30:53 EEST 2017
$ go version
go version go1.8.3 darwin/amd64

练习

  1. plotIP.go的绘图功能放入 Go 包中,并使用该包重写plotIP.goplotData.go
  2. 查看第 6 章、**文件输入输出ddGo.go的 Go 代码,以便在接收到SIGINFO信号时打印其进度信息。
  3. 更改cat.go的 Go 代码,增加对多个输入文件的支持。
  4. 更改代码plotData.go以便将网格线打印到生成的图像上。
  5. 更改plotData.go的代码,以便在绘图条之间留出一点空间。
  6. 尝试通过添加新功能使UNIXshell.go程序变得更好。

总结

在本章中,我们讨论了许多有趣且方便的主题,包括信号处理和在 Go 中创建图形图像。此外,我们还教您如何在 Go 程序中添加对 Unix 管道的支持。

在下一章中,我们将讨论 Go 最独特的功能,即 goroutines。您将了解什么是 goroutine,如何创建和同步它们,以及如何创建通道和管道。请记住,许多人来这里是为了学习一种现代的、安全的编程语言,但留下来是为了它的成功!**