Go
2 接下来运行哪些代码:条件和循环
调用方法
time包有一个表示日期(年、月、日)和时间(小时、分钟、秒等)的Time类型。每一个time.Time值都有一个返回年份的Year方法。
type Time struct { } func Now() Time func (t Time) Year() int
下面的代码使用这个方法打印当前年份:
package main import ( "fmt" "time" // 我们需要导入“time”包,以便使用time.Time类型。 ) func main() { var now time.Time = time.Now() // time.Now返回一个当前日期和时间的time.Time值。 var year int = now.Year() // time.Time值有一个返回年份的Year方法。 fmt.Println(now) fmt.Println(year) }
time.Now函数返回当前日期和时间的新Time值,我们将其存储在now变量中。然后,我们对now引用的值调用Year方法。
方法是与特定类型的值关联的函数。
调用方法(续)
package main import ( "fmt" "strings" ) func main() { broken := "G# r#cks!" replacer := strings.NewReplacer("#", "o") // 这将返回一个strings.Replacer,其设置为将每个"#"替换为"o"。 fixed := replacer.Replace(broken) // 对strings.Replacer调用Replace方法,并传递一个字符串来进行替换。 fmt.Println(fixed) }
点表示右边的东西属于左边。
我们前面看到的函数属于一个包,而方法属于一个单独的值。这个值(now、replacer)出现在圆点的左边。
now.Year()
replacer.Replace(broken)
评分
我们需要编写一个程序,允许学生输入他们的百分比分数,并告诉他们是否通过。及格或不及格遵循一个简单的公式:60%或以上的分数是及格,不足60%的分数是不及格。因此,如果用户输入的百分比大于或等于60,我们的程序需要给出一个响应,否则的话将给出不同的响应。
获取用户的分数
// pass_fail reports whether a grade is passing or failing. package main import ( "bufio" "fmt" "os" ) func main() { fmt.Print("Enter a grade: ") reader := bufio.NewReader(os.Stdin) // 设置从键盘获取文本的“缓冲读取器”。 input := reader.ReadString('\n') // 返回用户输入的所有内容,直到按下<Enter>键为止。 fmt.Println(input) }
ReadString方法需要一个带有rune(字符)的参数来标记输入的结束。我们想要读取用户输入的所有内容,直到他们按下<Enter>,所以我们给ReadString一个换行符。
Error:
./test.go:13:14: assignment mismatch: 1 variable but reader.ReadString returns 2 values
函数或方法的多个返回值
Go中多个返回值最常见的用法是返回一个额外的错误值,可以通过查询该错误值来确定函数或方法运行时是否发生了错误。举几个例子:
bool, err := strconv.ParseBool("true") // 如果字符无法转换为布尔值,则返回一个错误。 file, err := os.Open("myfile.txt") // 如果文件无法打开,则返回一个错误。 response, err := http.Get("http://golang.org") // 如果无法检索页面,则返回错误。
Go要求声明的每个变量都必须在程序的某个地方使用。如果我们添加了一个err变量,而不检查它,我们的代码将无法编译。未使用的变量通常表示一个bug,所以这是一个Go帮助你检测和修复bug的例子!
Error:
reader := bufio.NewReader(os.Stdin) // 设置从键盘获取文本的“缓冲读取器”。 input := reader.ReadString('\n') // 返回用户输入的所有内容,直到按下<Enter>键为止。 fmt.Println(input)
./test.go:13:12: err declared and not used
Go不允许我们声明一个变量,除非我们使用它。
选项1:使用空白标识符忽略错误返回值
等我们有一个值时,通常会把它分配给一个变量,但我们不打算使用这个值时,我们可以使用Go的空白标识符。为空白标识符分配一个值实际上会丢弃它(同时让其他读你代码的人知道你正在这么做)。要使用空白标识符,只需在赋值语句中输入一个下划线(_)字符,通常在这里输入的是变量名。
让我们尝试使用空白标识符代替旧的err变量:
// pass_fail reports whether a grade is passing or failing. package main import ( "bufio" "fmt" "os" ) func main() { fmt.Print("Enter a grade: ") reader := bufio.NewReader(os.Stdin) // 设置从键盘获取文本的“缓冲读取器”。 input, _ := reader.ReadString('\n') // 返回用户输入的所有内容,直到按下<Enter>键为止。 fmt.Println(input) }
选项2:处理错误
如果我们从ReadString方法中得到一个错误返回,空白标识符只会导致错误被忽略,而我们的程序无论如何都会继续执行,有可能会使用无效的数据。
在这种情况下,如果出现错误,更合适的做法是警告用户并停止程序运行。
log包有一个Fatal函数,它可以同时为我们完成这两项操作:将一条消息记录到终端并停止程序运行。(在这个上下文中,“Fatal”意味着报告一个错误,并“杀死”你的程序。)
// pass_fail reports whether a grade is passing or failing. package main import ( "bufio" "fmt" "os" "log" ) func main() { fmt.Print("Enter a grade: ") reader := bufio.NewReader(os.Stdin) // 设置从键盘获取文本的“缓冲读取器”。 input, err := reader.ReadString('\n') // 返回用户输入的所有内容,直到按下<Enter>键为止。 log.Fatal(err) fmt.Println(input) }
新的问题......(输入正确的数字时依然会错误退出)
条件
如果我们的程序在从键盘读取输入时遇到问题,我们将其设置为报告错误并停止运行。但是现在,即使一切正常,它也会停止运行!
Enter a grade: 23 2025/01/04 20:32:58 <nil> exit status 1
像ReadString这样的函数和方法返回一个错误值nil,这基本上意味着“那里什么都没有”。换句话说,如果err为nil,则表示没有错误。但是我们的程序被设置为只简单地报告nil错误!我们应该做的是,只有当err变量的值不是nil时才退出程序。
要实现这一点我们可以使用条件语句:只有在满足某个条件时,才导致代码块(一个或多个由花括号{}包围的语句)被执行的语句。
计算表达式,如果结果为true,则执行条件块体中的代码。如果为false,则跳过条件块。
与大多数其他语言一样,Go也支持条件语句中的多个分支。这些语句采用if...else if...else的形式。
if grade == 100 { fmt.Println("Perfect!") } else if grade >= 60 { fmt.Println("You pass.") } else { fmt.Println("You fail!") }
条件(续)
条件语句依赖于布尔表达式(计算结果为true或false)来决定是否应执行它们所包含的代码。
if 1 == 1 { fmt.Println("I'll be printed!") }
当你仅在条件为假时才需要执行代码时,可以使用! 布尔求反运算符,它允许你获取一个真值并使其为假,或者获取一个假值并使其为真。
if !true { fmt.Println("I won't be printed!") } if !false { fmt.Println("I will!") }
如果只希望在两个条件都为真时运行一些代码,可以使用&&(“与”)运算符。如果你想让它在两个条件之一为真时运行,你可以使用||(“或”)运算符。
if true && true { fmt.Println("I'll be printed!") } if false || true { fmt.Println("I'll be printed!") }
有条件地记录致命错误
// pass_fail reports whether a grade is passing or failing. package main import ( "bufio" "fmt" "log" "os" ) func main() { fmt.Print("Enter a grade: ") reader := bufio.NewReader(os.Stdin) input, err := reader.ReadString('\n') if err != nil { log.Fatal(err) // 报告错误并停止程序运行。 } fmt.Println(input) }
冰箱上有一个打印文件大小的Go程序。它调用os.Stat函数,该函数返回一个os.FileInfo值,可能还返回错误值。然后它对FileInfo值调用Size方法来获取文件大小。
package main import ( "fmt" "log" "os" ) func main() { fileInfo, err := os.Stat("my.txt") if err != nil { log.Fatal(err) } fmt.Println(fileInfo.Size()) }
避免遮盖名字
命名一个error变量是一个坏主意,因为它会遮盖一个名为error的类型的名称。
声明变量时,应确保它与任何现有的函数、包、类型或其他变量的名称不同。如果在封闭范围内存在同名的东西(稍后我们将讨论作用域),则你的变量将对它进行覆盖,也就是说,优先于它。而这往往是一件坏事。
package main import ( "fmt" ) func main() { var int int = 12 // 命名这个变量“int”会遮盖内置的“int”类型的名称! var append string = "minutes of bonus footage" // 命名这个变量“append”会遮盖内置的“append”函数的名称! var fmt string = "DVD" // 命名这个变量“fmt”会覆盖导入的“fmt”包的名称! }
避免遮盖名字(续)
……但是如果我们试图访问变量所遮盖的类型、函数或包,我们将得到变量中的值。在这种情况下,它会导致编译错误:
package main import ( "fmt" ) func main() { var int int = 12 var append string = "minutes of bonus footage" var fmt string = "DVD" var count int // "int"现在是指上面声明的变量,而不是数字类型! var languages = append([]string{}, "Espanol") // “append”现在指的是一个变量,而不是函数! fmt.Println(int, append, "on", fmt, languages) // “fmt”现在指的是一个变量,而不是一个包! }
./test.go:4:5: "fmt" imported and not used ./test.go:11:9: count declared and not used ./test.go:11:15: int (variable of type int) is not a type ./test.go:12:21: invalid operation: cannot call non-function append (variable of type string) ./test.go:13:9: fmt.Println undefined (type string has no field or method Println)
package main import ( "fmt" ) func main() { var count int = 12 // 重新命名“int”变量 var suffix string = "minutes of bonus footage" // 重新命名“append”变量 var format string = "DVD" // 重新命名“fmt”变量 var languages = append([]string{}, "Espanol") fmt.Println(count, suffix, "on", format, languages) }
12 minutes of bonus footage on DVD [Espanol]
Go有一个名为error的内置类型。这就是为什么在声明用于保存错误的变量时,我们将它命名为err而不是error——我们希望避免用变量名来遮盖error类型的名称。
将字符串转换为数字
条件语句还允许我们评估输入的分数。让我们添加一个if/else语句来确定分数是及格还是不及格。如果输入的百分比分数为60或更高,我们会将状态设置为“及格”。否则,我们将设置为“不及格”。
// package and import statements omitted package main import ( "bufio" "log" "fmt" "os" ) func main() { fmt.Print("Enter a grade: ") reader := bufio.NewReader(os.Stdin) input, err := reader.ReadString('\n') if err != nil { log.Fatal(err) } if input >= 60 { status := "passing" } else { status := "failing" } }
编译错误:
# command-line-arguments ./test.go:18:17: invalid operation: input >= 60 (mismatched types string and untyped int)
问题是:从键盘输入的内容是作为字符串读入的。Go只能将数字与其他数字进行比较;不能将数字与字符串进行比较;而且没有直接的类型转换方法可以将字符串转换成数字。
float64("2.6") ./test.go:12:13: cannot convert "2.6" (untyped string constant) to type float64
这里我们有两个需要解决的问题:
· 输入字符串的末尾仍然有一个换行符,这源于用户在输入的过程中按了<Enter> 键。我们需要将其去除。
· 剩下的字符串需要转换为浮点数。
将字符串转换为数字(续)
从input字符串的末尾删除换行符很容易。strings包有一个TrimSpace函数,它将删除字符串开头和结尾的所有空白字符(换行符、制表符和常规空格)。
因此,我们可以通过将input传递给TrimSpace,并将返回值赋回给input变量来去除换行符。
现在input字符串中应该保留的是用户输入的数字。我们可以使用strconv包的ParseFloat函数将其转换为float64值。
短变量声明中只有一个变量必须是新的。
input, err := reader.ReadString('\n') grade, err := strconv.ParseFloat(input, 64)
新变量名(grade)被视为声明,而现有的名字(err)被视为赋值。
3 调用:函数
一些重复的代码
假设我们需要计算刷几面墙所需的油漆量。厂商说每升油漆可以刷10平方米。所以,我们需要把每面墙的宽度(以米为单位)乘以它的高度来得到它的面积,然后除以10得到所需的油漆的升数。
计算会稍微有点偏差,因为计算机上的普通浮点运算总是有点儿不太准确。但是,只要我们在显示这些数字之前将其四舍五入到一个合理的精度,就应该没问题。
使用Printf和Sprintf格式化输出
为了处理这类格式化问题,fmt包提供了Printf函数。Printf代表“带格式的打印”。它接受一个字符串并将一个或多个值插入其中,以特定的方式进行格式化。然后打印结果字符串。
fmt.Printf("About one-third: %0.2f\n", 1.0/3.0)
Sprintf函数(也是fmt包的一部分)的工作方式与Printf类似,只是它返回格式化的字符串而不是打印它。
resultString := fmt.Sprintf("About one-third: %0.2f\n", 1.0/3.0) fmt.Printf(resultString)
Printf函数的两个特性:
· 格式化动词(上面字符串中的%0.2f是动词)
· 值的宽值(也就是动词中间的0.2)
4 代码集:包
包不仅仅对组织结构有益,它还是程序之间共享代码的一种简单方法,同时也是与其他开发人员共享代码的一种简单方法。
不同的程序,相同的函数
从键盘读取分数的代码已经被移到一个新的getFloat函数中。getFloat返回用户输入的浮点数,在有错误的情况下,它返回0和错误值。如果返回错误,程序将报告错误并退出;否则,它将像以前一样报告分数是通过还是不通过。
// pass_fail reports whether a grade is passing or failing. package main import ( "bufio" "fmt" "log" "os" "strconv" "strings" ) func getFloat() (float64, error) { reader := bufio.NewReader(os.Stdin) input, err := reader.ReadString('\n') if err != nil { return 0, err } input = strings.TrimSpace(input) number, err := strconv.ParseFloat(input, 64) if err != nil { return 0, err } return number, nil } func main() { fmt.Print("Enter a grade: ") grade, err := getFloat() if err != nil { log.Fatal(err) } var status string if grade >= 60 { status = "passing" } else { status = "failing" } fmt.Println("A grade of", grade, "is", status) }
不同的程序,相同的函数(续)
在这里,我们有一个新的tocelsius.go程序,它允许用户输入一个华氏测量系统的温度,并将其转换为摄氏系统的温度。
// tocelsius converts a temperature from Fahrenheit to Celsius. package main import ( "bufio" "fmt" "log" "os" "strconv" "strings" ) func getFloat() (float64, error) { reader := bufio.NewReader(os.Stdin) input, err := reader.ReadString('\n') if err != nil { return 0, err } input = strings.TrimSpace(input) number, err := strconv.ParseFloat(input, 64) if err != nil { return 0, err } return number, nil } func main() { fmt.Print("Enter a temperature in Fahrenheit: ") fahrenheit, err := getFloat() if err != nil { log.Fatal(err) } celsius := (fahrenheit - 32) * 5 / 9 fmt.Printf("%0.2f degrees Celsius\n", celsius) }
注意,tocelsius.go中的getFloat函数与pass_fail.go中的getFloat函数相同。
使用包在程序之间共享代码
如果你的部分代码在多个程序之间共享,你应该考虑将它们移到包中。
使用GOPATH环境变量更改工作区
GOPATH是一个环境变量,Go工具会参考它来查找工作区位置。可以使用GOPATH将你的工作区转移到其他目录。
设置GOPATH
如果你的代码存储在默认目录之外的目录中,则需要配置go工具以到正确的位置进行查找。可以通过设置GOPATH环境变量来实现这一点。
在Mac或Linux系统上:
export GOPATH="/code"
在Windows系统上:
set GOPATH="C:\code"
自己Linux下的:
$ go env ... GOPATH='/root/go' ... GOROOT='/usr/local/go'
5 列表:数组
Go有两种内置的存储列表的方法。本章将介绍第一种:数组。
数组保存值的集合
数组是所有共享同一类型的值的集合。
数组中包含的值称为它的元素。你可以有一个字符串数组、一个布尔数组或任何其他Go类型的数组(甚至数组的数组)。
数组保存值的集合(续)
数组保存特定数量的元素,不能增长或收缩。
var myArray [4]string /* 4:数组中将保存的元素数 string:数组中将保存的元素类型 */
time.Time值的数组:
var dates [3]time.Time dates[0] = time.Unix(1257894000, 0) dates[1] = time.Unix(1447920000, 0) dates[2] = time.Unix(1508632200, 0) fmt.Println(dates[1])
数组的零值
创建数组时,它所包含的所有值都初始化为数组所保存类型的零值。
读取文本文件
以前,我们使用标准库的os和bufio包从键盘一次读取一行数据。我们可以使用相同的包从文本文件中一次读取一行数据。
读取文本文件(续)
编写一个只读取文件的程序
package main import ( "bufio" "fmt" "log" "os" ) func main() { file, err := os.Open("data.txt") // 打开数据文件进行读取 // 如果打开文件时出现错误,报告错误并退出。 if err != nil { log.Fatal(err) } scanner := bufio.NewScanner(file) // 为文件创建一个新的扫描器 // 循环到文件结尾,scanner.Scan返回false。 for scanner.Scan() { // 从文件中读取一行 fmt.Println(scanner.Text()) // 打印该行 } err = file.Close() // 关闭文件以释放资源 // 如果关闭文件时出现错误,报告错误并退出。 if err != nil { log.Fatal(err) } // 如果扫描文件时出现错误,报告错误并退出。 if scanner.Err() != nil { log.Fatal(scanner.Err()) } }
不太明白的还是方法的调用。
6 追加的问题:切片
我们已经知道了无法将更多的元素增加到一个数组中。切片是一个可以通过增长来保存额外数据的集合类型,正好能满足程序的需要!我们将看到切片是如何让用户以简洁的方式在程序中提供数据的,以及如何帮助你写出更加方便调用的函数。
7 标签数据:映射
如果有一种集合,其中的每个值都有个标签在上面,那么你就可以快速找到你需要的值!
8 构建存储:struct
有时你需要保存超过一种类型的数据。
有时,你需要一组不同类型的数据,例如邮件地址,混合了街道名(字符串类型)和邮政编码(整型);又如学生记录,混合保存学生名字和成绩(浮点数)。
9 我喜欢的类型:定义类型
定义类型还有更多内容需要学习。在之前的章节,我们展示了如何使用struct基础类型来定义类型。没有展示如何使用任意类型作为基础类型。
10 保密:封装和嵌入
11 你能做什么:接口
有时你并不关心一个值的特定类型。你不需要关心它是什么。你只需要知道它能做特定的事情。你能够在其上调用特定的接口。不需要关心是pen还是pencil,你仅仅需要一个Draw方法。不需要关心是Car还是Boat,你只需要一个Steer方法。
那就是Go接口的目标。它允许你定义能够保存任何类型的变量和函数参数,前提是它定义了特定的方法。
12 重新站起来:从失败中恢复
每个程序都会遇到错误。你应该为它们做好计划。在本章中,我们将向你展示如何延迟清理操作,以便在出现错误时也能执行这些操作。我们还将向你展示如何在适当的(罕见的)情况下使程序出现panic,以及如何在事后恢复。
13 分享工作:goroutine和channel
一次只做一件事并不总是完成任务最快的方法。一些大问题可以分解成小任务。goroutine可以让程序同时处理几个不同的任务。goroutine可以使用channel来协调它们的工作,channel允许goroutine互相发送数据并同步,这样一个goroutine就不会领先于另一个goroutine。goroutine让你充分利用具有多处理器的计算机,让程序运行得尽可能快!
14 代码的质量保证:自动化测试
你确定你的软件工作良好吗?真的确定吗?
15 响应请求:Web应用程序
这是21世纪。用户需要Web应用程序。
Web应用程序需要做的第一件事是当浏览器向它发送请求时能够做出响应。在本章中,我们将学习如何使用net/http包来实现这一点。
16 要遵循的模式:HTML模板
你的Web应用程序需要用HTML而不是纯文本进行响应。