go_study

Go

————

————

Go 语言被设计成一门应用于搭载 Web 服务器,存储集群或类似用途的巨型中央服务器的系统编程语言。

第一个Go程序

hello.go

package main

import "fmt"

func main() {
    fmt.Println("Hello, World!")
}

执行

  • terminal 执行:go run hello.go
image-20211101152722492
  • go build 命令来生成二进制文件:
image-20211101152859147

语言结构

Go 语言的基础组成有以下几个部分:

  • 包声明
  • 引入包
  • 函数
  • 变量
  • 语句 & 表达式
  • 注释
package main

import "fmt"

func main() {
     /* 这是我的第一个简单的程序 */
    fmt.Println("Hello, World!")
}
  • package main 定义了包名。你必须在源文件中非注释的第一行指明这个文件属于哪个包,如:package main。package main表示一个可独立执行的程序,每个 Go 应用程序都包含一个名为 main 的包。
  • import “fmt” 告诉 Go 编译器这个程序需要使用 fmt 包(的函数,或其他元素),fmt 包实现了格式化 IO(输入/输出)的函数。
  • func main() 是程序开始执行的函数。main 函数是每一个可执行程序所必须包含的,一般来说都是在启动后第一个执行的函数(如果有 init() 函数则会先执行该函数)。
  • /*...*/ 是注释,在程序执行时将被忽略。单行注释是最常见的注释形式,你可以在任何地方使用以 // 开头的单行注释。多行注释也叫块注释,均已以 /* 开头,并以 */ 结尾,且不可以嵌套使用,多行注释一般用于包的文档描述或注释成块的代码片段。
  • fmt.Println(…) 可以将字符串输出到控制台,并在最后自动增加换行字符 \n。
  • 当标识符(包括常量、变量、类型、函数名、结构字段等等)以一个大写字母开头,如:Group1,那么使用这种形式的标识符的对象就可以被外部包的代码所使用(客户端程序需要先导入这个包),这被称为导出(像面向对象语言中的 public);标识符如果以小写字母开头,则对包外是不可见的,但是他们在整个包的内部是可见并且可用的(像面向对象语言中的 protected )。

注意: { 不能单独放在一行

Go 程序的一般结构

// 当前程序的包名
package main

// 导入其他包
import . "fmt"

// 常量定义
const PI = 3.14

// 全局变量的声明和赋值
var name = "gopher"

// 一般类型声明
type newType int

// 结构的声明
type gopher struct{}

// 接口的声明
type golang interface{}

// 由main函数作为程序入口点启动
func main() {
    Println("Hello World!")
}

Go 程序是通过 package 来组织的。

只有 package 名称为 main 的源码文件可以包含 main 函数。

一个可执行程序有且仅有一个 main 包。

通过 import 关键字来导入其他非 main 包。

导入多个:

import (
    "fmt"
    "math"
)

使用 <PackageName>.<FunctionName> 调用:

package 别名:
// 为fmt起别名为fmt2
import fmt2 "fmt"

通过 const 关键字来进行常量的定义。

通过在函数体外部使用 var 关键字来进行全局变量的声明和赋值。

通过 type 关键字来进行结构(struct)和接口(interface)的声明。

通过 func 关键字来进行函数的声明。

Go语言中,使用大小写来决定该常量、变量、类型、接口、结构或函数是否可以被外部包所调用。

函数名首字母小写即为 private :

func getId() {}

函数名首字母大写即为 public :

func Printf() {}

基础语法

Go 标记

Go 程序可以由多个标记组成,可以是关键字,标识符,常量,字符串,符号。如以下 GO 语句由 6 个标记组成:

fmt.Println("Hello, World!")

6 个标记是(每行一个):

1. fmt
2. .
3. Println
4. (
5. "Hello, World!"
6. )
行分隔符

在 Go 程序中,一行代表一个语句结束。每个语句不需要像 C 家族中的其它语言一样以分号 ; 结尾

如果你打算将多个语句写在同一行,它们则必须使用 ; 人为区分

注释

单行注释是最常见的注释形式,你可以在任何地方使用以 // 开头的单行注释。多行注释也叫块注释,均已以 /* 开头,并以 */ 结尾。如:

// 单行注释
/*
 Author by 菜鸟教程
 我是多行注释
 */
标识符

标识符用来命名变量、类型等程序实体。一个标识符实际上就是一个或是多个字母(AZ和az)数字(0~9)、下划线_组成的序列,但是第一个字符必须是字母或下划线而不能是数字。

字符串连接

Go 语言的字符串可以通过 + 实现:

package main

import "fmt"

func main() {
  fmt.Println("Google" + "Runoob")
}

以上实例输出结果为:

GoogleRunoob
空格

Go 语言中变量的声明必须使用空格隔开,如:

var age int;
格式化字符串

Go 语言中使用 fmt.Sprintf 格式化字符串并赋值给新串:

package main

import (
    "fmt"
)

func main() {
   // %d 表示整型数字,%s 表示字符串
    var stockcode=123
    var enddate="2020-12-31"
    var url="Code=%d&endDate=%s"
    var target_url=fmt.Sprintf(url,stockcode,enddate)
    fmt.Println(target_url)
}

输出结果为:

Code=123&endDate=2020-12-31

数据类型

​ 数据类型让编程语言、编译器、数据库和代码执行环境知道如何操作和处理数据。例如,如果数据类型为数字,通常可对其执行数学运算。编程语言和数据库常常根据数据类型赋予程序不同的功能和性能。大多数编程语言还提供了用于处理常见数据的标准库,而数据库提供了查询语言,让程序员能够根据底层数据类型来查询数据以及与之交互。无论数据类型是否被显式地声明,它们都是重要的编程和计算结构。

强类型以及弱类型

  • 强类型

    声明变量时指定变量的类型,给变量赋值时必须是指定类型,否则会报错,Java、C#、Object-C、Ruby

  • 弱类型

    数据类型可以被忽略,一个变量可以赋不同数据类型的值。一旦给一个整型变量 a 赋一个字符串值,那么 a 就变成字符类型。

  • 动态类型

    不对变量类型进行识别,在运行的时候可以改变其结构的语言、JavaScript、PHP、Python

  • 静态类型

​ 静态语言的数据类型是在编译其间确定的或者说运行之前确定的,编写代码的时候要明确确定变量的数据类型。

​ 主要语言:C、C++、C#、Java、Object-C。

  • go 是静态类型 强类型

布尔类型

布尔型的值只可以是常量 true 或者 false。一个简单的例子:var b bool = true。如果没有给布尔变量赋值,它将默认为false。

布尔变量可在声明后重新赋值.

package main

import (
   "fmt"
)

func main() {
   var b bool
   fmt.Println("old b=",b)
   b = true
   fmt.Println("new b=",b)
}

old b= false
new b= true

数值类型

浮点数、整数、无符号整数、8位、64位、bigint、smallint、tinyint,这些都是整型(数值)类型。要明白这些术语的含义,必须知道数字在计算机内部是以二进制位的方式存储的。二进制位就是一系列布尔值,取值要么为1,要么为0。1位可表示1或0,对于4位整数,可表示16个不同的数字。

无符号整数

二进制十进制
00000
00011
111014
111115
带符号整数与无符号整数

对于带符号整数,需要使用一位来表示符号,这通常是符号−。无符号的4位整数,其取值范围为0~15。带符号整数可正可负,因此4位带符号整数的取值范围为−8到7。

二进制十进制
00000
00011
00102
00113
01004
01015
01106
01117
1000−8
1001−7
1010−6
1011−5
1100−4
1101−3
1110−2
1111-1

在Go语言中,声明整型变量的方式如下。

var i int = 3

类型int表示带符号整数,因此可正可负。根据计算机的底层体系结构,int可能是32位的带符号整数,也可能是64位的带符号整数。

浮点数

浮点数是带小数点的数字,如11.2、0.1111、43.22。整数不能包含小数部分,因此要处理分数,必须使用浮点数。

根据实际数字的大小,Go语言中的浮点数可以是32位的,也可以是64位的。在大多数现代计算机中,推荐使用float64。

字符串

字符串可以是任何字符序列,其中的字符可能是数字、字母和符号。下面是一些简单的字符串。

  • cow。
  • $^%$​。
  • a1234。

在Go语言中,声明并初始化字符串变量很简单。

var s string = "foo"

字符串变量可以为空,这种变量非常适合用来累积其他变量中的数据以及存储临时数据。

var s string = ""

创建字符串变量后,可将其与其他数据相加。下面的代码创建一个空字符串,再将字符串foo附加到末尾,这在Go语言中是合法的。

var s string = ""
s += "foo"

s= afoo

数组

声明数组时,必须指定其长度和类型。

var beatles [4]string
beatles[0] = "John"
beatles[1] = "Paul"
beatles[2] = "Ringo"
beatles[3] = "George"

方括号内的数字表示数组的长度,而紧跟在方括号后的是数组的类型——这里为字符串。注意:数组索引从0开始

检查变量类型

有些情况下,需要检查变量的类型,为此可使用标准库中的reflect包,它让您能够访问变量的底层类型。

package main

 import (
   "fmt"
   "reflect"
 )

func main() {
   var s string = "string"
   var i int = 1024
   var f float64 = 2.46

   fmt.Println(reflect.TypeOf(s))
   fmt.Println(reflect.TypeOf(i))
   fmt.Println(reflect.TypeOf(f))
}

string int float64

类型转换

将数据从一种类型转换为另一种类型是常见的编程任务,这通常是在从网络或数据库读取数据时进行的。Go标准库提供了良好的类型转换支持。strconv包提供了一整套类型转换方法,可用于转换为字符串或将字符串转换为其他类型。

只有相同底层类型的变量之间可以进行相互转换(如将 int16 类型转换成 int32 类型),不同底层类型的变量相互转换时会引发编译错误(如将 bool 类型转换为 int 类型)

Parse 系列函数用于将字符串转换为指定类型的值,其中包括 ParseBool()、ParseFloat()、ParseInt()、ParseUint()。ParseBool()只能接受 1、0、t、f、T、F、true、false、True、False、TRUE、FALSE,其它的值均返回错误

假设有一个字符串变量s,其值为“true”,要将其用于布尔比较,必须先转换为布尔类型。

var v string = "1"
	b, err := strconv.ParseBool(v)
	if err == nil {
		fmt.Printf("str1: %v\n", err)
	}
	//字符串转化为bool型
	fmt.Printf("type:%T value:%#v\n",b,b)
//输出
str1: <nil>
type:bool value:true
//布尔值也可转换为字符串。
func main() {
   //var b bool =  true
   var s string = strconv.FormatBool(true)
   fmt.Println(reflect.TypeOf(s))

string

理解变量

Go是一种静态类型语言,因此声明变量时必须显式或隐式地指定其类型。

var s string = "string"

 var s string
  s = "Hello World"

变量的类型很重要,因为这决定了可将什么值赋给变量。例如,对于类型为string的变量,不能将整数值赋给它;同理,不能将字符串赋给布尔变量。将类型不正确的值赋给变量时,将导致编译错误。

快捷声明变量

Go支持多种快捷变量声明方式。可在一行内声明多个类型相同的变量并给它们赋值

var s, t string = "foo", "bar" //可在一行内声明多个类型相同的变量并给它们赋值
var (
    j string = "foo"//不同类型的变量
   i int = 4
 )
var j int = 1

声明变量后,就不能再次声明它。虽然可以给变量重新赋值,但不能重新声明变量,否则将导致编译阶段错误。

# command-line-arguments

.\string.go:25:6: j redeclared in this block
        previous declaration at .\string.go:20:3

理解变量与零值

声明变量时如果没有给它指定值,则变量将为默认值,这种默认值被称为零值

unc main() {
	var(
		i int
		f float64
		b bool
		s string
	)
	fmt.Printf("%v %v %v %q\n", i, f, b, s)
}

输出:

0 0 false ""

为确定变量是否已经赋值,不能检查它是否为nil,而必须检查它是否为默认值。Go禁止将变量初始化为nil值,因为这样做将导致编译阶段错误。

编写简短的变量声明

使用简短变量声明时,编译器会推断变量的类型,因此您无须显式地指定变量的类型。请注意,只能在函数中使用简短变量声明。

func main() {
    s := "hello World"
}

简短变量赋值语句:=表明使用的是简短变量声明,这意味着不需要使用关键字var,也不用指定变量的类型。同时这还意味着应将:=右边的值赋给变量。

变量声明方式

var s string = "Hello World"

var s = "Hello World"

var t string
t = "Hello World"

u := "Hello World"

Go语言设计者在标准库中遵循的约定如下:在函数内使用简短变量声明,在函数外省略类型。

理解变量作用域

术语作用域指的是变量在什么地方可以使用,而不是变量是在什么地方声明的。Go语言使用基于块的词法作用域。

  • 在Go语言中,一对大括号({})表示一个块。
  • 对于在大括号({})内声明的变量,可在相应块的任何地方访问。
  • 大括号内的大括号定义了一个新块——内部块。
  • 在内部块中,可访问外部块中声明的变量。
  • 在外部块中,不能访问在内部块中声明的变量。
  • 代码的缩进程度反映了块作用域的层级。在每个块中,代码都被缩进。

Go语言将文件也视为块,所以在第一级大括号外声明的变量可在所有块中访问。

使用指针

在Go语言中声明变量时,将在计算机内存中给它分配一个位置,以便能够存储、修改和获取变量的值。要获取变量在计算机内存中的地址,可在变量名前加上&字符。内存地址由16进制表示。

将变量传递给函数时,会分配新内存并将变量的值复制到其中。这样将有两个变量实例,它们位于不同的内存单元中。一般而言,这不可取,因为这将占用更多的内存,同时由于存在变量的多个副本,很容易引入Bug。考虑到这一点,Go提供了指针。

指针是Go语言中的一种类型,指向变量所在的内存单元。要声明指针,可在变量名前加上星号字符。

如果要使用指针指向的变量的值,而不是其内存地址,可在指针变量前加上星号。

func showAddress_1(x int) {
	fmt.Println(&x)
}

func showAddress_2(x *int) {
	fmt.Println("address=",&x)
	fmt.Println("x=",*x)
}

 func main() {
 	s := 1
 	fmt.Println(&s)
 	showAddress_1(s)
 	showAddress_2(&s)
 }

输出:

0xc0000160b0
0xc0000160b8
address= 0xc000006030
x= 1

声明常量

常量指的是在整个程序生命周期内都不变的值。常量初始化后,可以引用它,但不能修改它。

const greeting string = "Hello, world"

使用函数

函数是什么

简单地说,函数接受输入并返回输出。数据流经函数时,将被变换。

函数的结构

在Go语言中,函数向编译器和程序员提供了有关的信息,这些信息指出了函数将接受什么样的输入并提供什么样的输出。这种信息是在函数的第一行中提供的,而这一行被称为函数签名。

func addUp(x int, y int) int {
  return x + y
}

关键字func指出这是一个函数的开头位置。接下来是函数名,括号,指出了函数接受什么样的值。在右括号后面是返回值,这里也是一个类型为int的值。左大括号表示接下来为函数体,函数体以右大括号结束。如果函数签名声明了返回值,则函数体必须以终止语句结束。

返回单个值

函数的输入和输出类型是在签名中声明的

func isEven(i int) bool {
	return i%2==0
}

func main() {
	fmt.Printf("%v\n", isEven(1))
	fmt.Printf("%v\n", isEven(2))
}
  • 让每个函数只做一件事情并把这件事情做好。软件不可避免地要修改,通过结合使用大量简短的函数,可让软件更容易修改。这还有助于测试各个函数以及整个软件。
  • 维护。在团队合作开发中,您编写的函数易于阅读和理解吗?如果不是这样的,就说明它过于复杂或必须添加注释。
  • 性能。在有些情况下,函数的性能至关重要。定义明确的函数能够让程序员修改其实现,并测试其性能是否达到了目标基准
返回多个值

可在函数签名中声明多个返回值,让函数返回多个结果。在这种情况下,终止语句可返回多个值

func getPrize() (int, string) {
  i := 2
  s := "goldfish"
  return i, s
}

func main() {
  quantity, prize := getPrize()
  fmt.Printf("You won %v %v\n", quantity, prize)
}

定义不定参数函数

不定参数函数是参数数量不确定的函数。在Go语言中,能够传递可变数量的参数,但它们的类型必须与函数签名指定的类型相同。要指定不定参数,可使用3个点(…)。

func sumNumbers(numbers...int) int {
   total :=0
   for _, number :=range numbers {
      total +=number
   }
   return total
}

使用具名返回值

具名返回值让函数能够在返回前将值赋给具名变量,这有助于提升函数的可读性,使其功能更加明确。要使用具名返回值,可在函数签名的返回值部分指定变量名。

func sayHi() (x, y string) {
  x = "hello"
  y = "world"
  return
}

func main() {
  fmt.Println(sayHi())
}

hello world

这个函数体中,在终止语句return前给具名变量进行了赋值。使用具名返回值时,无须显式地返回相应的变量。这被称为裸(naked)return语句

使用递归函数

递归函数是不断调用自己直到满足特定条件的函数。要在函数中实现递归,可将调用自己的代码作为终止语句中的返回值。

func feedMe(portion int, eaten int) int {
  eaten = portion + eaten
  if eaten >= 5 {
    fmt.Printf("I'm full! I've eaten %d\n", eaten)
    return eaten
  }
  fmt.Printf("I'm still hungry! I've eaten %d\n", eaten)
  return feedMe(portion, eaten)
}

最重要的是函数体的最后一行,它没有返回值,而是调用自己,这样将再次执行这个函数。

将函数作为值传递

Go将函数视为一种类型,因此可将函数赋给变量,以后再通过变量来调用它们

  package main
 
  import "fmt"
 
  func anotherFunction(f func() string) string {
    return f()
  }
 
  func main() {
   fn := func() string {
     return "function called"
   }
   fmt.Println(anotherFunction(fn))
 }

控制流程

介绍控制流程以及代码执行流程是如何确定的

使用if语句

if语句检查指定的条件,并在条件满足时执行指定的操作。

b :=false
	if b {
		fmt.Println("b is true!")
    } else{
        fmt.Println("b is false!")
    }

if语句总是计算一个布尔表达式,在它为true时执行大括号内的代码,在它为false时不执行

使用else语句

else语句指定了到达该分支时将执行的代码。它不做任何判断,只要到达它所在的分支就执行。只要前面有语句的结果为true,else语句就不会执行。在Go语言中,else语句紧跟在其他语句的右大括号后面,通常是当前块中的最后一条语句。大致而言,else相当于说:如果其他条件都不为true,就执行这条语句。

使用else if 语句

在很多情况下,都需要依次判断多个布尔表达式,此时可使用else if语句。else if语句能够让您在前面的布尔表达式为false时接着判断后面的布尔表达式,这种逻辑的意思是,如果前面的if或else if语句为false,就试试这条else if语句。

b :=4
	if b ==2 {
		fmt.Println("b is 2!")
    } else if b ==3 {
        fmt.Println("b is 3!")
    } else if b ==4 {
        fmt.Println("b is 4!")
    }

使用比较运算符

比较运算符可用于对任何两项类型相同的数据进行比较,如果它们满足比较运算符指定的条件,结果将为true;否则为false

字符运算符
==等于
!=不等
<小于
<=小于等于
>大于
>=大于等于

两个操作数的类型必须相同

使用算术运算符

字符运算符
+和(也叫加)
-差(也叫减)
*积(也叫乘)
/商(也叫除)
%余(也叫模)

使用逻辑运算符

字符运算符
&&与:两个条件是否都为true
||或:两个条件是否至少有一个为true
!非:条件是否为false

使用switch语句

switch语句可用来替代冗长的if else布尔比较。相比于else if条件,switch语句更简洁,它还支持在其他case条件都不满足时将执行的default case。在switch语句中,可使用关键字default来指定其他case条件都不满足时要执行的代码。default case通常放在switch语句末尾,但也可将其放在任何其他地方。

func main(){
	s := "c"

	switch s {
	case "a":
		fmt.Println("The letter a!")
	case "b":
		fmt.Println("The letter b!")
	default:
		fmt.Println("I don't recognize that letter!")
	}
}

使用for循环

i:=0
for i < 10 {
    i++
    fmt.Println("i is",i)
}

如果这个布尔条件为true,就执行for语句中的代码。

包含初始化语句和后续语句的for语句

除要检查的条件外,for语句还可指定在循环开始时执行的初始化语句以及后续(post)语句。这些语句可让迭代代码简短得多,但必须使用分号将它们分隔开。

  • 初始化语句:仅在首次迭代前执行。
  • 条件语句:每次迭代前都将检查的布尔表达式。
  • 后续语句:每次迭代后都将执行
for i := 0; i < 10; i++ {
    fmt.Println("i is",i)
}
包含range子句的for语句

for语句也可用来遍历数据结构。

func main() {
	numbers := []int{1,2,3,4,5}
	for i,n := range numbers {
		fmt.Println("The index of the loop is", i)
		fmt.Println("The value from the slice is", n)
	}
}
  • for语句指定了迭代变量i,用于存储索引值。这个变量将在每次迭代结束后更新。
  • for语句指定了迭代变量n,用于存储来自数组中的值。它也将在每次迭代结束后更新。
  • 迭代变量从0开始,且每次都加1。要在特定的迭代中执行操作,务必从0而不是1开始计算迭代数!
使用defer语句

defer是一个很有用的Go语言功能,它能够让您在函数返回前执行另一个函数。函数在遇到return语句或到达函数末尾时返回。defer语句通常用于执行清理操作或确保操作(如网络调用)完成后再执行另一个函数。

package main

import (
    "fmt"
)

func main() {
    defer fmt.Println("I am the first defer statement")
    defer fmt.Println("I am the second defer statement")
    defer fmt.Println("I am the third defer statement")
    fmt.Println("Hello World!")
}

Hello World!
I am the third defer statement
I am the second defer statement
I am the first defer statement
  • 3条defer语句都指定了它们所在的函数执行完毕后要执行的函数。
  • 向终端打印Hello World!,外部函数就此结束。
  • 外部函数执行完毕后,按与defer语句出现顺序相反的顺序执行它们指定的函数。

数组、切片与映射

使用数组

数组是一个数据集合,在编程中它通常按逻辑对数据进行分组。数组也是基本的编程构件,常用于存储一系列用数字做索引的数据。

var cheeses [2]string
  • 使用关键字var声明一个名为cheeses的变量。
  • 将一个长度为2的数组赋给这个变量。
  • 这个数组的类型为字符串。

声明变量后,便可将字符串赋给数组的元素了。

cheeses[0] = "Mariolles"
cheeses[1] = "Époisses de Bourgogne"

索引从0而不是1开始

要打印数组的所有元素,可使用变量名本身。

fmt.Println(cheeses)

声明数组的长度后,就不能给它添加元素了。

切片

切片是底层数组中的一个连续片段,通过它您可以访问该数组中一系列带编号的元素。因此,切片能够让您顺序访问数组的特定部分。

要声明一个长度为2的空切片,可使用如下语法。

var cheeses = make([]string, 2)

使用Go内置函数make创建一个切片,其中第一个参数为数据类型,而第二个参数为长度。在这里,创建的切片包含两个字符串元素。

对slice切片的语法为:SLICE[A:B:C]

其中A表示从SLICE的第几个元素开始切,B控制切片的长度(B-A),C控制切片的容量(C-A),如果没有给定C,则表示切到底层数组的最尾部

在切片中添加元素

Go语言提供了内置函数append,让您能够增大切片的长度。

cheeses = append(cheeses, "Camembert")
fmt.Println(cheeses[2])

函数append也是一个不定参数函数,这意味着使用函数append可在切片末尾添加很多值。

cheeses := append(cheeses, "Camembert", "Reblochon", "Picodon")
  • 在切片开头添加元素一般都会导致内存的重新分配,而且会导致已有元素全部被复制 1 次。 因此,从切片的开头添加元素的性能要比从尾部追加元素的性能差很多。
  • make()产生的切片分配内存,给定位置的切片指向对应内存区域,不分配新的内存。
  • 扩容机制:内存不够时,扩展为原来的两倍
从切片中删除元素?

要从切片中删除元素,也可使用内置函数append。在下面的示例中,删除了索引2处的元素。

cheeses = append(cheeses[:2])

通过在删除元素前后检查切片cheeses的长度,可知已经正确地调整了该切片的长度。另外,元素的排列顺序没有发生变化。

sec := make([]int, 1)
	fmt.Println(&sec[0])

	sec[0]=1
	fmt.Println(sec)
	fmt.Println(&sec[0])

	sec = append(sec,2)
	fmt.Println(sec)
	fmt.Println(&sec[0])

	sec = append(sec,3,4)
	fmt.Println(sec)
	fmt.Println(&sec[0])

	sec = append(sec[:3])
	fmt.Println(sec)
	fmt.Println(&sec[0])

	sec = append(sec[2:4],sec[1:4]...)
	fmt.Println(sec)
	fmt.Println(&sec[0])

0xc0000160b0
[1]
0xc0000160b0 //改变切片sec[0]元素,此元素的内存地址不变
[1 2]
0xc0000160f0 //追加元素,改变了原有元素sec[0]的内存地址
[1 2 3 4]
0xc00000c4e0
[1 2 3]
0xc00000c4e0// 删除后面的元素,原先的元素sec[0]内存地址不变
[3 4 2 3 4]//?
0xc000014450
复制切片的元素

要复制切片的全部或部分元素,可使用内置函数copy。在复制切片中的元素前,必须再声明一个类型与该切片相同的切片,例如,不能将字符串切片中的元素复制到整数切片中

func main() {
    var cheeses = make([]string, 2)
    cheeses[0] = "Mariolles"
    cheeses[1] = "Époisses de Bourgogne"
    fmt.Println(cheeses)
    var smellyCheeses = make([]string, 3)
    copy(smellyCheeses, cheeses)  
    fmt.Println(smellyCheeses)
}

[Mariolles Époisses de Bourgogne]
[Mariolles Époisses de Bourgogne ]

函数copy在新切片中创建元素的副本,因此修改一个切片中的元素不会影响另一个切片。还可将单个元素或特定范围内的元素复制到新切片中,下面的示例复制索引1处的元素。

copy(smellyCheeses, cheeses[1:])

使用映射

数组和切片是可通过索引值访问的元素集合,而映射是通过键来访问的无序元素编组。大多数编程语言都支持数组;在其他编程语言中,映射也被称为关联数组、字典或散列。映射在信息查找方面的效率非常高,因为可直接通过键来检索数据。简单地说,映射可视为键-值对集合。

只需一行代码就可声明并创建一个空映射。

var players = make(map[string]int)
  • 关键字var声明一个名为players的变量。

  • 在等号右边,使用Go语言内置函数make创建了一个映射,其键的类型为字符串,而值的类型为整数。

  • 将这个空映射赋给了变量players。

  • 现在可在这个空映射中添加键-值对了。

players["cook"] = 32
players["bairstow"] = 27
players["stokes"] = 26

​ 变量名后面的方括号内为键,而等号右边是要赋给键的整数值。

​ 要打印映射中特定键对应的值,可使用这个键来获取相应的值。

fmt.Println(players["cook"])
fmt.Println(players["stokes"])

​ 与数组和切片一样,要打印映射中所有的键-值对,可使用变量名本身。

fmt.Println(players)
map[cook:32 bairstow:27 stokes:26]

​ 可在映射中动态地添加元素,而无须调整映射的长度。

delete(players, "cook")

使用结构体和指针

结构体是什么

结构体是一系列具有指定数据类型的数据字段,它能够让您通过单个变量引用一系列相关的值。通过使用结构体,可在单个变量中存储众多类型不同的数据字段。存储在结构体中的值可轻松地访问和修改,这提供了一种灵活的数据结构创建方式。通过使用结构体,可提高模块化程度,还能够让您创建并传递复杂的数据结构。

type Movie struct {
    Name string
    Rating float32
}

func main() {
    m := Movie{
        Name: "Citizen Kane",
        Rating: 10,
    }
    fmt.Println(m.Name, m.Rating)
}
  • 关键字type指定一种新类型。
  • 将新类型的名称指定为Movie。
  • 类型名右边是数据类型,这里为结构体。
  • 在大括号内,使用名称和类型指定了一系列数据字段。请注意,此时没有给数据字段赋值。可将结构体视为模板。
  • 在main函数中,使用简短变量赋值声明并初始化了变量m,给数据字段指定的值为相应的数据类型。
  • 使用点表示法访问数据字段并将其打印到控制台。

创建结构体

声明结构体后,就可通过多种方式创建它。假设您已经声明了一个结构体,那么就可直接声明这种类型的变量。

var m Movie
m.Name = "Metropolis"
m.Rating = 0.99

没有给字段赋值,默认为零值。对于字符串,零值为空字符串" ";对于float32,零值为0。

结构体数据字段的值是可变的,这意味着可动态地修改它们

也可使用关键字new来创建结构体实例,如下所示。关键字new创建结构体Movie的一个实例(为其分配内存);将这个结构体实例赋给变量m后,就可像前面那样使用点表示法给数据字段赋值了。

m := new(Movie)
m.Name = "Metropolis"
m.Rating = 0.99

​ 还可使用简短变量赋值来创建结构体实例,此时可省略关键字new。创建结构体实例时,可同时给字段赋值,方法是使用字段名、冒号和字段值。

c := Movie{Name: "Citizen Kane", Rating: 10}

​ 也可省略字段名,按字段声明顺序给它们赋值,但出于可维护性考虑,不推荐这样做。

c := Movie{"Citizen Kane", 10}

​ 字段很多时,让每个字段独占一行能够提高代码的可维护性和可读性,最后一个数据字段所在的行也必须以逗号结尾。

c := Movie {
    Name: "Citizen Kane",
    Rating: 10,
}

​ 使用简短变量赋值是最常用的结构体创建方式,也是推荐的方式

嵌套结构体

有时候,数据结构需要包含多个层级。此时,虽然可选择使用诸如切片等数据类型,但有时候需要的数据结构更复杂。为建立较复杂的数据结构,在一个结构体中嵌套另一个结构体的方式很有用。

type Superhero struct {
  Name  string
  Age   int
  Address Address
}

type Address struct {
  Number int
  Street string
  City  string
}

可在创建Superhero结构体前创建Address结构体并给它赋值,但也可在创建Superhero结构体时这样做。程序清单7.5使用简短变量赋值创建了一个嵌套结构体实例。

自定义结构体数据字段的默认值

创建数据结构时,自定义数据字段的默认值是很有必要的。默认情况下,Go给数据字段指定相应数据类型的零值。

类型零值
布尔型(Boolean)false
整型(Integer)0
浮点型(Float)0.0
字符串(String)" "
指针(Pointer)nil
函数(Function)nil
接口(Interface)nil
切片(Slice)nil
通道(Channel)nil
映射(Map)nil

创建结构体时,如果没有给其数据字段指定值,它们将为表所示的零值。Go语言没有提供自定义默认值的内置方法,但可使用构造函数来实现这个目标。构造函数创建结构体,并将没有指定值的数据字段设置为默认值。

type Alarm struct {
    Time string
    Sound string
}

func NewAlarm(time string) Alarm {
    a := Alarm{
        Time: time,
        Sound: "Klaxon",
    }
    return a
}

​ 这里不直接创建结构体Alarm,而是使用函数NewAlarm来创建,从而让字段Sound包含自定义的默认值。

比较结构体

对结构体进行比较,要先看它们的类型和值是否相同。对于类型相同的结构体,可使用相等性运算符来比较。要判断两个结构体是否相等,可使用==;要判断它们是否不等,可使用!=。不能对两个类型不同的结构体进行比较,否则将导致编译错误。因此,试图比较两个结构体之前,必须确定它们的类型相同。要检查结构体的类型,可使用Go语言包reflect。

要调试或查看结构体的值,可使用fmt包将结构体的字段名和值打印出来。为此,可使用占位符%+v并将其传入结构体。

package main

import (
    "fmt"
)

type Drink struct {
    Name  string
    Ice   bool
}

func main() {
    a := Drink{
        Name: "Lemonade",
        Ice: true,
    }
    b := Drink{
        Name: "Lemonade",
        Ice: true,
    }
    if a == b {
        fmt.Println("a and b are the same")
    }
    fmt.Printf("%+v\n", a)
    fmt.Printf("%+v\n", b)
    
}

fmt.Println(reflect.TypeOf(a))

fmt.Println(reflect.TypeOf(b))

理解公有和私有值

如果一个值被导出,可在函数、方法或包外面使用,那么它就是公有的;如果一个值只能在其所属上下文中使用,那么它就是私有的。

根据Go语言约定,结构体及其数据字段都可能被导出,也可能不导出。如果一个标识符的首字母是大写的,那么它将被导出;否则不会导出。

要导出结构体及其字段,结构体及其字段的名称都必须以大写字母打头。

区分指针引用和值引用

使用结构体时,明确指针引用和值引用的区别很重要

数据值存储在计算机内存中。指针包含值的内存地址,这意味着使用指针可读写存储的值。创建结构体实例时,给数据字段分配内存并给它们指定默认值;然后返回指向内存的指针,并将其赋给一个变量。使用简短变量赋值时,将分配内存并指定默认值。

a := Drink{}

复制结构体时,明确内存方面的差别很重要。将指向结构体的变量赋给另一个变量时,被称为赋值。

a := b

赋值后,a与b相同,但它是b的副本,而不是指向b的引用。修改b不会影响a,反之亦然。

要修改原始结构体实例包含的值,必须使用指针。指针是指向内存地址的引用,因此使用它操作的不是结构体的副本而是其本身。要获得指针,可在变量名前加上和号。

指针和值的差别很微妙,但选择使用指针还是值很容易区分:如果需要修改原始结构体实例,就使用指针;如果要操作一个结构体,但不想修改原始结构体实例,就使用值。

要创建结构体的副本,但不希望修改影响原始结构体时,应使用值;要操作结构体的副本,并希望所做的修改在原始结构体中反映出来时,应使用指针

func main() {
	a := Drink{
		Name: "Lemonade",
		Ice: true,
	}
	b := &a
	b.Ice=false
	fmt.Printf("%+v\n", a)
	fmt.Printf("%+v\n", *b)
	fmt.Printf("%p\n", b)
	fmt.Printf("%p\n", &a)

}

{Name:Lemonade Ice:false}
{Name:Lemonade Ice:false}
0xc000004480
0xc000004480

创建方法和接口

可使用点表示法来访问结构体中的数据。然而,涉及更复杂的操作时,理解和处理起来就不那么容易了。Go提供了另一种操作数据的方式——通过方法来操作。

使用方法

方法类似于函数,但有一点不同:在关键字func后面添加了另一个参数部分,用于接受单个参数。

type Movie struct {
  Name string
  Rating float64
}

func (m *Movie) summary() string {
  r := strconv.FormatFloat(m.Rating, 'f', 1, 64)
  return m.Name + ", " + r
}

func main(){
    m := Movie{
        Name: "Spiderman",
        Rating: 3.2,
    }
    fmt.Println(m.summry())
}

Spiderman, 3.2

在方法声明中,关键字func后面多了一个参数——接收者。严格地说,方法接收者是一种类型,这里是指向结构体Movie的指针。

通过声明方法summary,让结构体Movie的任何实例都可使用它。方法summary的实现将float64等级制转换为字符串并设置其格式。使用方法的优点在于,只需编写方法实现一次,就可对结构体的任何实例进行调用。

为何要使用方法,而不直接使用函数呢?函数summary和结构体Movie相互依赖,但它们之间没有直接关系。例如,如果不能访问结构体Movie的定义,就无法声明函数summary。如果使用函数,则在每个使用函数或结构体的地方,都需包含函数和结构体的定义,这会导致代码重复。另外,函数发生任何改变,都必须随之修改多个地方。这样看来在函数与结构体关系密切时,使用方法更合理。

创建方法集

方法集是可对特定数据类型进行调用的一组方法。在Go语言中,任何数据类型都可有相关联的方法集,这让您能够在数据类型和方法之间建立关系。方法集可包含的方法数量不受限制,这是一种封装功能和创建库代码的有效方式。

计算球体的表面积与体积

type Sphere struct {
    Radius float64
}
func (s *Sphere) SurfaceArea() float64 {
    return float64(4) * math.Pi * (s.Radius * s.Radius)
}
func (s *Sphere) Volume() float64 {
    radiusCubed := s.Radius * s.Radius * s.Radius
    return (float64(4) / float64(3)) * math.Pi * radiusCubed
}

需要指出的是,在方法中可以访问结构体的Radius值,这是使用点表示法访问的。

相比于使用函数,使用方法集的优点在于,只需编写一次方法SurfaceArea和Volume。例如,如果发现这两个方法中有一个存在Bug,则只需修改一个地方即可。

使用方法和指针

方法是一个接受被称为接收者的特殊参数的函数,接收者可以是指针,也可以是值,但两者的差别非常微妙。假设有一个存储三角形数据的结构体。

向方法传递指针引用

计算三角形面积

type Triangle struct {
  width float64
  height float64
}

func (t *Triangle) area() float64 {
  return 0.5 * (t.width * t.height)
}

可以直接使用结构体的数据字段来计算,但更简结的方式是定义一个方法

方法area返回前述公式的结果。请注意,接收者是指向结构体Triangle的指针,这是由星号指定的。

向方法传递值引用?

为理解将接收者参数声明为指针引用和值引用的差别,我们来看一个简单的示例,它修改结构体中定义的三角形的底值。假设要修改三角形的底值,可使用方法changeBase来实现。

func (t Triangle) changeBase(f float64) {
  t.base = f
  return
}

注意到指定接收者参数类型时没有在Triangle前面加上星号,这意味着接收者参数是值而不是指针。

t :=Triangle{width: 3,height: 2 }
	t.changeWidth(7)
	fmt.Println(t.width)

3

之所以打印的是3,是因为方法changeBase接受的是一个值引用。这意味着这个方法操作的是结构体Triangle的副本,而原始结构体不受影响。

将指针作为接收者的方法能够修改原始结构体的数据字段,这是因为它使用的是指向原始结构体内存单元的指针,因此操作的不是原始结构体的副本。

如果需要修改原始结构体,就使用指针;如果需要操作结构体,但不想修改原始结构体,就使用值,那副本在哪?怎么显示

使用接口

在Go语言中,接口指定了一个方法集,这是实现模块化的强大方式。您可将接口视为方法集的蓝本,它描述了方法集中的所有方法,但没有实现它们。接口功能强大,因为它充当了方法集规范,这意味着可在符合接口要求的前提下随便更换实现

接口描述了方法集中的所有方法,并指定了每个方法的函数签名。

下面的示例假设需要编写一些控制机器人(Robot)的代码。粗略地说,可假定有多种类型的机器人,控制这些机器人行为的方式存在细微的差别。给定这个编程任务,您可能认为需要为每种机器人编写不同的代码。通过使用接口,可将代码重用于有相同行为的实体。就这个机器人示例而言,下面的接口描述了开关机器人的方式。

type Robot interface {
    PowerOn() err
}

​ 接口Robot只包含一个方法——PowerOn。这个接口描述了方法PowerOn的函数签名:不接受任何参数且返回一种错误类型。从高级层面说,接口还有助于理解代码设计。在无须关心实现的情况下,很容易理解设计是什么样的。

​ 那么如何使用接口呢?接口是方法集的蓝本,要使用接口,必须先实现它。如果代码满足了接口的要求,就实现了接口。要实现接口Robot,可声明一个满足其要求的方法集。

type T850 struct {
    Name string
}
func (a *T850) PowerOn() err {
    return nil
}

​ 这个实现很简单,但满足了接口Robot的要求,因为它包含方法PowerOn,且这个方法的函数签名与接口Robot要求的一致。接口的强大之处在于,它们支持多种实现。例如,您也可以像下面这样来实现接口Robot。

type R2D2 struct {
    Broken bool
}
func (r *R2D2) PowerOn() err {
    if r.Broken {
        return errors.New("R2D2 is broken")
    } else {
        return nil
    }
}

​ 这也满足了接口Robot的要求,因为它符合这个方法集的定义——包含方法PowerOn,同时函数签名也相同。请注意,这里与方法集相关联的结构体为R2D2,它包含的数据字段与T850不同,方法PowerOn的代码也完全不同,但函数签名一样。

​ 要满足接口的要求,只要实现了它指定的方法集,且函数签名正确无误就可以了。

​ 当前,接口Robot有两种实现,虽然有相同的Robot定义很有用,但没有可同时用于T850和R2D2实例的代码。接口也是一种类型,可作为参数传递给函数,因此可编写可重用于多个接口实现的函数。

例如,编写一个可用于启动任何机器人的函数。

func Boot(r Robot) error {
    return r.PowerOn()
}

​ 这个函数将接口Robot的实现作为参数,并返回调用方法PowerOn的结果。这个函数可用于启动任何机器人,而不管其方法PowerOn是如何实现的。T850和R2D2都可利用这个方法。

func main() {
    t := T850{
        Name: "The Terminator",
    }
    r := R2D2{
        Broken: true,
    }
    err := Boot(&r)
    if err != nil {
        fmt.Println(err)
    } else {
        fmt.Println("Robot is powered on!")
    }
    err = Boot(&t)
    if err != nil {
        fmt.Println(err)
    } else {
        fmt.Println("Robot is powered on!")
    }
}

​ 假设要编写一个使用MySQL数据库的计算机程序,如果不使用接口,则代码将完全是针对MySQL的。在这种情况下,如果后来要将MySQL数据库换成其他数据库,如PostgreSQL,就可能需要重写大量的代码。

通过定义一个数据库接口,该接口的实现将比使用的数据库更重要。从理论上说,只要实现满足接口的要求,就可使用任何数据库,因此可轻松地更换数据库。数据库接口可包含多个实现,这就引入了多态的概念。

多态意味着多种形式,它让接口能够有多种实现。在Go语言中,接口以声明的方式提供了多态,因为接口描述了必须提供的方法集以及这些方法的函数签名。如果一个方法集实现了一个接口,就可以说它与另一个实现了该接口的方法集互为多态。编译器也会验证接口:检查方法集并确保接口确实是多态的。通过将接口正式化,可确保接口的两种实现是多态的。这无疑会让代码可验证、可测试且是灵活的。

使用字符串

创建字符串字面量

Go语言支持两种创建字符串字面量的方式。解释型字符串字面量是用双引号括起的字符,如"hello"。

除换行符和未转义的双引号外,解释型字符串字面量可包含其他任何字符。对于前面有反斜杠(\)的字符,将像它们出现在rune字面量中那样进行解读。

rune字面量/转义符Unicode字符
\aU+0007 alert or bell
\bU+0008(退格)
\fU+000C(换页符)
\nU+000A(换行符)
\rU+000D(回车)
\tU+0009(水平制表符)
\vU+000b(垂直制表符)
\U+005c(反斜杠)
U+0027(单引号,这个转义序列只能包含在rune字面量中)
\"U+0022(双引号,这个转义序列只能包含在字符串字面量中)

解释型字符串字面量使用双引号("),可包含rune字面量,这意味着可使用特殊字符来设置格式;原始字符串字面量使用反引号(`),位于反引号中的格式保持不变,这包括空格、制表符和换行符。

理解rune字面量

通过使用rune字面量,可将解释型字符串字面量分成多行,还可在其中包含制表符和其他格式选项。

在下方程序中,使用rune字面量添加换行符和制表符,虽然字符串声明位于一行中。

func main() {
    s := "After a backslash, certain single character escapes represent special values\nn is a line feed or new line \n\t t is a tab"
    fmt.Println(s)
}

​ 运行这个示例将生成如下经过格式设置的字符串。

After a backslash, certain single character escapes represent special values
n is a line feed or new line
         t is a tab

原始字符串字面量用反引号括起,如hello。不同于解释型字符串,原始字符串中的反斜杠没有特殊含义,Go按原样解释这种字符串。

	t := `After a backslash, certain single character escapes represent special values
n is a line feed or new line
   t is a tab`

拼接字符串

在Go语言中,要拼接(合并)字符串,可将运算符+用于字符串变量。

v := "Oh sweet ignition" + " be my fuse"
fmt.Println(v)
v +="\nsorry"
fmt.Println(v)

Oh sweet ignition be my fuse
Oh sweet ignition be my fuse
sorry

只能拼接类型为字符串的变量,如果将整数和字符串进行拼接将导致编译错误。Go标准库提供了strconv包,您可使用其中的方法Itoa来完成这种任务:它将整数转换为字符串

i := 1
s:=" egg"
intToString:=strconv.Itoa(i)
b:=intToString +s
使用缓冲区拼接字符串

对于简单而少量的拼接,使用运算符+和+=的效果虽然很好,但随着拼接操作次数的增加,这种做法的效率并不高。如果需要在循环中拼接字符串,则使用空的字节缓冲区来拼接的效率更高。

package main
import (
    "fmt"
    "bytes"
)

func main() {
    var buffer bytes.Buffer
    for i := 0; i < 500; i++ {
        buffer.WriteString("z")
    }
    fmt.Println(buffer.String())
}

1.创建一个空的字节缓冲区,并将其赋给变量buffer。

2.一个运行500次的循环,每次循环都将字符串"z"写入缓冲区。

3.循环结束后,对缓冲区调用函数String( )以字符串的方式输出结果。

理解字符串是什么

要理解字符串是什么,必须明白计算机是如何显示和存储字符的。计算机将数据解读为数字,计算机实际上是将字符存储为数字的。

​ 历史上有很多编码标准,最后大家就如何将字符映射到数字达成了一致。ASCII(美国信息交换标准码)曾经是最重要的编码标准,它就如何使用数字来表示英语字母表中的字符进行了标准化。

​ ASCII编码标准定义了如何使用7位的整数(通俗地说是数字)来表示128个字符。下表列出了ASCII字符集中的一些字符,数字是一一映射到字符的。

二进制八进制十进制十六进制字符
10000011016541A
10000101026642B
111010016411674t

​ 虽然ASCII在英语字符标准化的道路上迈出了重要的一步,但它不包含其他任何语言的字符集。简而言之,它支持使用英语说“hello”,但不支持使用中文说“您好”。

​ 鉴于此,Unicode编码方案于1987年应运而生,它支持全球各地的大多数字符集。最新的版本支持128000个字符,涵盖135种或现代或古老的语言。更重要的是,Unicode涵盖了ASCII标准,其开头的128个字符就是ASCII字符。

​ 很多字符编码方案都实现了Unicode标准,其中最著名的是UTF-8。更巧的是,Go语言的两位设计者Rob Pike和Ken Thompson也是UTF-8的联合设计者。可见,Go语言支持UTF-8和国际字符集,而Go源代码也是UTF-8的。

​ 要更深入地理解字符串以及如何操作它们,必须首先知道Go语言中的字符串实际上是只读的字节切片。要获悉字符串包含多少个字节,可使用Go语言的内置函数len

s := "hello"
fmt.Printf(len(s))// outputs 5

​ 西语字符(如a、b、c)通常映射到单个字节。单词hello为5个字节,由于Go字符串为字节切片,因此可输出字符串中特定位置的字节值。在下面的示例中,输出了字符串“hello”的第一个字节。

s := "hello"
fmt.Printf(s[0])//outputs 104

​ 您可能认为结果应为字符h,但由于通过索引访问字符串时,访问的是字节而不是字符,因此显示的是以十进制表示的字节值。下表列出了多个字母的十进制和二进制表示。

hello
十进制104101108108111
二进制11010001100101110110011011001101111

​ 在Go语言中,可使用格式设置将十进制值转换为字符和二进制表示。

s := "hello"
fmt.Println("%q", s[0])// outputs 'h'
fmt.Println("%b", s[0])// outputs 1101000

在Go语言中,字符串实际上是字节切片,这意味着可以像操作其他字节切片一样操作字符串。

处理字符串

小写
strings.ToLower("VERY IMPORTANT MESSAGE")
查找子串

方法Index提供了这样的功能,它接受的第二个参数是要查找的子串。如果找到,就返回第一个子串的索引号;如果没有找到,就返回-1。别忘了,索引从0开始!

fmt.Println(strings.Index("surface", "face"))
fmt.Println(strings.Index("moon", "aer"))

3
-1
删除空格

strings包提供了很多将字符串的某些部分删除的方法。处理来自用户或数据源的输入时,一种常见的任务是确保开头和末尾没有空格。方法TrimSpace提供了这样的功能

fmt.Println(strings.TrimSpace(" I don't need this space ")
            
I don't need this space

在Go语言中,字符串是不可变的,这意味着创建后就不能修改。如果您试图重新声明变量,将导致编译错误。您可使用复合赋值运算符+=来拼接字符串。

处理错误

错误处理及Go语言的独特之处

在Go语言中,一种约定是在调用可能出现问题的方法或函数时,返回一个类型为错误的值。这意味着如果出现问题,函数通常不会引发异常,而让调用者决定如何处理错误。函数ioutil.Readfile在出现问题时返回一个错误值。

func main() {
    file, err := ioutil.ReadFile("foo.txt")
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Println("%s", file)
}
//输出
open foo.txt: The system cannot find the file specified.
  • 使用标准库中io/ioutil包内的函数ReadFile读取文件。
  • file是一个字节切片,而err是一个错误,如果有错误,就意味着返回的错误值不为nil。
  • 打印错误,程序就此结束
  • 如果没有错误,就打印文件的内容。

函数ReadFile接受一个字符串参数,并返回一个字节切片和一个错误值。这个函数的定义如下。

func ReadFile(filename string) ([]byte, error)

这意味着函数ReadFile总是会返回一个错误值,可对其进行检查。在前述示例的main函数中,将方法ReadFile的返回值存储到了两个变量(file和err)中,这是Go代码中常见的模式

在Go语言中,有一种约定是,如果没有发生错误,返回的错误值将为nil。

理解错误类型

在Go语言中,错误是一个值。标准库声明了接口error,如下所示。

type error interface {
  Error() string
}

这个接口只有一个方法——Error,它返回一个字符串。

创建错误

标准库中的errors包支持创建和操作错误。

func main() {
    err := errors.New("Something went wrong")
    if err != nil {
        fmt.Println(err)
    }
}

//输出
Something went wrong

设置错误格式

除errors包外,标准库中的fmt包还提供了方法Errorf,可用于设置返回的错误字符串的格式。这能够让您将多个值合并成更有意义的错误字符串,从而动态地创建错误字符串。

func main() {    
    name, role := "Richard Jupp", "Drummer"    
    err := fmt.Errorf("The %v %v quit", role, name)    
    if err != nil {        
        fmt.Println(err)    
    }
}
//输出
The Drummer Richard Jupp quit

从函数返回错误

func Half(numberToHalf int) (int, error) {
    if numberToHalf%2 != 0 {
        return -1, fmt.Errorf("Cannot half %v", numberToHalf)
    }
    return numberToHalf / 2, nil
}

func main() {
    n, err := Half(19)
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Println(n)
}

错误的可用性

  • 具体地指出了问题所在。
  • 提供了问题解决方案。
  • 对用户更有礼貌。

慎用panic

panic是Go语言中的一个内置函数,它终止正常的控制流程并引发恐慌(panicking),导致程序停止执行。出现普通错误时,并不提倡这种做法,因为程序将停止执行,并且没有任何回旋余地

func main() {
    fmt.Println("This is executed")
    panic("Oh no. I can do no more. Goodbye.")
    fmt.Println("This is not executed")
}

调用panic后,程序将停止执行,因此打印This is not executed的代码行根本没有机会执行。

在下面的情形下,使用panic可能是正确的选择。

  • 程序处于无法恢复的状态。这可能意味着无路可走了,或者再往下执行程序将带来更多的问题。在这种情况下,最佳的选择是让程序崩溃。
  • 发生了无法处理的错误。

使用Goroutine

并发与并行

  • 顺序执行

    在最简单的计算机程序中,操作是依次执行的,执行顺序与出现顺序相同。想一想脚本中的代码行吧,这些代码行按出现顺序执行,一行执行完毕后才执行下一行。

  • 并发

    不必等到一个操作执行完毕后再执行下一个,编程任务和编程环境越复杂,这种理念就越重要。提出这种理念旨在让程序能够应对更复杂的情形,避免执行完一行代码后再执行下一行,从而提高程序的执行速度。程序完全按顺序执行时,如果某行代码需要很长时间才能执行完毕,那么整个程序将可能因此而停止,导致用户长时间等待事件的发生。

    现代编程必须考虑众多时间不可预测的变数。例如,您无法确定网络调用需要多长时间才能完成,也无法确定读取磁盘文件需要多长时间。鉴于所有这些因素都不是发出请求的程序能够控制的,因此完全有理由认为响应速度是无法预测的。另外,每次请求得到响应的时间都可能不同。面对这样的情形,程序员可选择等待响应——阻塞程序直到响应返回为止,也可继续执行其他有用的任务。大多数现代编程语言都提供了选择空间,让程序员可等待响应,也可继续做其他事情。

  • 并行

    烤蛋挞:如果采用顺序方式,就意味着每次只能在一个烤箱中烤一个蛋挞。这种做法的效率显然很低,因为这将需要很长时间:烤好一个蛋挞后再将另一个蛋挞放入烤箱。另外,时间上也无法预测,因为有的蛋挞烤得快,有的烤得慢。

    ​ 一种并发方式是,使用烘烤托盘每次烤多个蛋挞。这样做的效率要高得多,但还是不能同时烤好所有的蛋挞。例如,根据蛋挞的大小和所处烤箱的位置,烤好每个蛋挞的时间可能不同。相比于顺序方法,这种做法的速度更快,快多少取决于可同时烘烤多少个蛋挞。

    ​ 并发方法的速度受制于众多因素,其中一个是烤箱的尺寸。如果有位朋友家也有烤箱,就可两家同时烤,从而进一步提高效率。同时烤多个蛋挞被称为并发;而将烤蛋挞的任务分为两部分,由两家分别烤,烤好后再放在一起,这被称为并行。以并行的方式执行任务时,可利用并发性,也可不利用;它相当于将工作分成多个部分,各部分的工作完成后再将结果合并。

并发和并行的差别:并发就是同时处理很多事情,而并行就是同时做很多事情。

概念介绍:
进程/线程:

进程是程序在操作系统中的一次执行过程,系统进行资源分配和调度的一个独立单位。

线程是进程的一个执行实体,是 CPU 调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。

一个进程可以创建和撤销多个线程,同一个进程中的多个线程之间可以并发执行。

并发/并行

多线程程序在单核心的 cpu 上运行,称为并发;多线程程序在多核心的 cpu 上运行,称为并行。

并发与并行并不相同,并发主要由切换时间片来实现“同时”运行,并行则是直接利用多核实现多线程的运行,Go程序可以设置使用核心数,以发挥多核计算机的能力。

协程/线程:

协程:独立的栈空间,共享堆空间,调度由用户自己控制,本质上有点类似于用户级线程,这些用户级线程的调度也是自己实现的。

线程:一个线程上可以跑多个协程,协程是轻量级的线程。

使用Goroutine处理并发操作

Go语言提供了Goroutine,让您能够处理并发操作。Goroutine使用起来非常简单,只需在要让Goroutine执行的函数或方法前加上关键字go即可。

import (
    "fmt"
    "time"
)

func slowFunc() {
    time.Sleep(time.Second * 2)
    fmt.Println("sleeper() finished")
}

func main() {
    go slowFunc()
    fmt.Println("I am now shown straightaway!")
    time.Sleep(time.Second * 3)

}
//输出
I am now shown straightaway!
sleeper() finished

如果运行上述代码,结果为I am now shown straightaway!。根本看不到调用slowFunc的结果,这是因为Goroutine立即返回,这意味着程序将接着执行后面的代码,然后退出。如果没有其他因素阻止,则程序将在Goroutine返回前就退出。

goroutine 和系统线程

Goroutine是Go语言特有的并发体,是一种轻量级的线程,由go关键字启动。在真实的Go语言的实现中,Goroutine和系统线程也不是等价的。尽管两者的区别实际上只是一个量的区别,但正是这个量变引发了Go语言并发编程质的飞跃。

​ 首先,每个系统级线程都会有一个固定大小的栈(一般默认可能是2MB),这个栈主要用来保存函数递归调用时参数和局部变量。固定了栈的大小导致了两个问题:一是对于很多只需要很小的栈空间的线程来说是一个巨大的浪费,二是对于少数需要巨大栈空间的线程来说又面临栈溢出的风险。针对这两个问题的解决方案是:要么降低固定的栈大小,提升空间的利用率;要么增大栈的大小以允许更深的函数递归调用,但这两者是没法同时兼得的。相反,一个Goroutine会以一个很小的栈启动(可能是2KB或4KB),当遇到深度递归导致当前栈空间不足时,Goroutine会根据需要动态地伸缩栈的大小(主流实现中栈的最大值可达到1GB)。因为启动的代价很小,所以我们可以轻易地启动成千上万个Goroutine。

​ Go的运行时还包含了其自己的调度器,这个调度器使用了一些技术手段,可以在n个操作系统线程上多工调度m个Goroutine。Go调度器的工作和内核的调度是相似的,但是这个调度器只关注单独的Go程序中的Goroutine。Goroutine采用的是半抢占式的协作调度,只有在当前Goroutine发生阻塞时才会导致调度;同时发生在用户态,调度器会根据具体函数只保存必要的寄存器,切换的代价要比系统线程低得多。运行时有一个runtime.GOMAXPROCS变量,用于控制当前运行正常非阻塞Goroutine的系统线程数目。

​ 在Go语言中启动一个Goroutine不仅和调用函数一样简单,而且Goroutine之间调度代价也很低,这些因素极大地促进了并发编程的流行和发展。

顺序一致性模型

var a string
var done bool

func setup() {
    a = "hello, world"
    done = true
}

func main() {
    go setup()
    for !done {}
    print(a)
}

我们创建了setup线程,用于对字符串a的初始化工作,初始化完成之后设置done标志为truemain函数所在的主线程中,通过for !done {}检测done变为true时,认为字符串初始化工作完成,然后进行字符串的打印工作。

​ 但是Go语言并不保证在main函数中观测到的对done的写入操作发生在对字符串a的写入的操作之后,因此程序很可能打印一个空字符串。更糟糕的是,因为两个线程之间没有同步事件,setup线程对done的写入操作甚至无法被main线程看到,main函数有可能陷入死循环中。

在Go语言中,同一个Goroutine线程内部,顺序一致性内存模型是得到保证的。但是不同的Goroutine之间,并不满足顺序一致性内存模型,需要通过明确定义的同步事件来作为同步的参考。如果两个事件不可排序,那么就说这两个事件是并发的。为了最大化并行,Go语言的编译器和处理器在不影响上述规定的前提下可能会对执行语句重新排序(CPU也会对一些指令进行乱序执行)。

​ 因此,如果在一个Goroutine中顺序执行a = 1; b = 2;两个语句,虽然在当前的Goroutine中可以认为a = 1;语句先于b = 2;语句执行,但是在另一个Goroutine中b = 2;语句可能会先于a = 1;语句执行,甚至在另一个Goroutine中无法看到它们的变化(可能始终在寄存器中)。也就是说在另一个Goroutine看来, a = 1; b = 2;两个语句的执行顺序是不确定的。如果一个并发程序无法确定事件的顺序关系,那么程序的运行结果往往会有不确定的结果。

func main() {
    go println("hello, world")
}

根据Go语言规范,main函数退出时程序结束,不会等待任何后台线程。因为Goroutine的执行和main函数的返回事件是并发的,谁都有可能先发生,所以什么时候打印,能否打印都是未知的。

​ 用前面的原子操作并不能解决问题,因为我们无法确定两个原子操作之间的顺序。解决问题的办法就是通过同步原语来给两个事件明确排序:

func main() {
    done := make(chan int)
    go func(){
        println("hello, world")
        done <- 1
    }()
    <-done
}

​ 当<-done执行时,必然要求done <- 1也已经执行。根据同一个Goroutine依然满足顺序一致性规则,我们可以判断当done <- 1执行时,println("hello, world")语句必然已经执行完成了。因此,现在的程序确保可以正常打印结果。

使用通道

使用通道

如果说Goroutine是一种支持并发编程的方式,那么通道就是一种与Goroutine通信的方式。通道让数据能够进入和离开Goroutine,可方便Goroutine之间进行通信。

Go语言的并发实现理念:

“不要通过共享内存来通信,而通过通信来共享内存。”

在其他编程语言中,并发编程通常是通过在多个进程或线程之间共享内存实现的。共享内存能够让程序同步,确保程序以合乎逻辑的方式执行。在程序执行过程中,进程或线程可能对共享内存加锁,以禁止其他进程或线程修改它。

虽然使用共享内存有其用武之地,但Go语言使用通道在Goroutine之间收发消息,避免了使用共享内存。严格地说,Goroutine并不是线程,但您可将其视为线程,因为它们能够以非阻塞方式执行代码。在前面关于两人持有一个联合账户的例子中,如果使用Goroutine,将在账户持有人之间打开一个通信通道,让他们能够通信并采取相应的措施。例如,一个交易可能向通道发送一条消息,而通道可能限制后续交易或另一个账户持有人的行为。通过收发消息,使得能够以推送方式协调并发事件。事件发生时,可将触发的消息推送给接收者。使用共享内存时,程序必须检查共享内存。在变化频繁的并发编程环境中,很多人都认为使用消息是一种更佳的通信方式。

为了管理Goroutine和并发,Go语言提供了通道。如果能够在Goroutine和程序之间通信,并让Goroutine结束时能够告诉主程序就好了

通道的创建语法如下。

c := make(chan string)

对这种语法解读如下。

  • 使用简短变量赋值,将变量c初始化为:=右边的值。
  • 使用内置函数make创建一个通道,这是使用关键字chan指定的。
  • 关键字chan后面的string指出这个通道将用于存储字符串数据,这意味着这个通道只能用于收发字符串值。

向通道发送消息的语法如下。

c <- "Hello World"

请注意其中的<-,这表示将右边的字符串发送给左边的通道。如果通道被指定为收发字符串,则只能向它发送字符串消息,如果向它发送其他类型的消息将导致错误。

从通道那里接收消息的语法如下。

msg := <-c

要从通道那里接收消息,需要在<-后面加上通道名。可使用简短变量赋值,将来自通道的消息直接赋给变量。箭头向左表示数据离开通道(接收),箭头向右表示数据进入通道(发送)。

close用来关闭通道,禁止再向通道发送消息。

close(messages)
package main

import (
    "fmt"
    "time"
)

func slowFunc(c chan string) {
    time.Sleep(time.Second * 2)
    c <- "slowFunc() finished"
}

func main() {
    c := make(chan string)
    go slowFunc(c)
    msg := <-c
    fmt.Println(msg)
}

//
slowFunc() finished

使用缓冲通道

通常,通道收到消息后就可将其发送给接收者,但有时候可能没有接收者。在这种情况下,可使用缓冲通道。缓冲意味着可将数据存储在通道中,等接收者准备就绪再交给它。要创建缓冲通道,可向内置函数make传递另一个表示缓冲区长度的参数。

messages := make(chan string, 2)

这些代码创建一个可存储两条消息的缓冲通道。现在可在这个通道中添加两条消息了——虽然没有接收者。请注意,缓冲通道最多只能存储指定数量的消息,如果向它发送更多的消息将导致错误。

messages <- "hello"
messages <- "world"

消息将存储在通道中,直到接收者准备就绪。

package main

import (
    "fmt"
    "time"
)

func receiver(c chan string) {
    for msg := range c {
        fmt.Println(msg)
    }
}

func main() {
    messages := make(chan string, 2)
    messages <- "hello"
    messages <- "world"
    close(messages)
    fmt.Println("Pushed two messages onto Channel with no receivers")
    time.Sleep(time.Second * 1)
    receiver(messages)
}
//
Pushed two messages onto Channel with no receivers
hello
world

在知道需要启动多少个Goroutine或需要限制调度的工作量时,缓冲通道很有效。

阻塞与流程控制

Goroutine是Go语言提供的一种并发编程方式。速度缓慢的网络调用或函数会阻塞程序的执行,而Goroutine能够让您对此进行管理。在并发编程中,通常应避免阻塞式操作,但有时需要让代码处于阻塞状态。例如,需要在后台运行的程序必须阻塞,这样才不会退出。

Goroutine会立即返回(非阻塞),因此要让进程处于阻塞状态,必须采用一些流程控制技巧。例如,从通道接收并打印消息的程序需要阻塞,以免终止。

给通道指定消息接收者是一个阻塞操作,因为它将阻止函数返回,直到收到一条消息为止。

package main
import (
    "fmt"
    "time"
)
func pinger(c chan string) {
    t := time.NewTicker(1 * time.Second)
    for {
        c <- "ping"
        <-t.C
    }
}

func main() {
    messages := make(chan string)
    go pinger(messages)
    msg := <-messages
    fmt.Println(msg)
}
//
ping

收到一条消息后,阻塞操作将返回,而程序将退出。那么,如果创建不断监听通道中消息的监听器呢?通过使用for语句,可永久性地阻塞进程,也可让阻塞时间持续特定的迭代次数。

for i := 0; i < 5; i++ {
		msg := <-messages
		fmt.Println(msg)
	}

将通道用作函数参数

已在前面的示例中了解过,可将通道作为参数传递给函数,并在函数中向通道发送消息。要进一步指定在函数中如何使用传入的通道,可在传递通道时将其指定为只读、只写或读写的。指定通道是只读、只写、读写的语法差别不大。

func channelReader(messages <-chan string) {
    msg := <-messages
    fmt.Println(msg)
}
func channelWriter(messages chan<- string) {
    messages <- "Hello world"
}
func channelReaderAndWriter(messages chan string) {
    msg := <-messages
    fmt.Println(msg)
    messages <- "Hello world"
}

<-位于关键字chan左边时,表示通道在函数内是只读的;<-位于关键字chan右边时,表示通道在函数内是只写的;没有指定<-时,表示通道是可读写的。

​ 通过指定通道访问权限,有助于确保通道中数据的完整性,还可指定程序的哪部分可向通道发送数据或接收来自通道的数据。

使用select语句

假设有多个Goroutine,而程序将根据最先返回的Goroutine执行相应的操作,此时可使用select语句。select语句类似于switch语句,它为通道创建一系列接收者,并执行最先收到消息的接收者。select语句看起来和switch语句很像。

channel1 := make(chan string)
channel2 := make(chan string)
select {
    case msg1 := <-channel1:
       fmt.Println("received", msg1)
    case msg2 := <-channel2:
       fmt.Println("received", msg2)
    case <-time.After(500 * time.Millisecond):
     fmt.Println("no messages received.giving up.")
}

如果从通道channel1那里收到了消息,将执行第一条case语句;如果从通道channel2那里收到了消息,将执行第二条case语句。具体执行哪条case语句,取决于消息到达的时间,哪条消息最先到达决定了将执行哪条case语句。通常,接下来收到的其他消息将被丢弃。收到一条消息后,select语句将不再阻塞。

可使用超时时间。这让select语句在指定时间后不再阻塞,以便接着往下执行。

func ping1(c chan string){
	time.Sleep(time.Second *2)
	c <- "ping on channel1"
}

func ping2(c chan string){
     time.Sleep(time.Second *1)
      c <- "ping on channel2"
}

func main(){
	channel1 := make(chan string)
	channel2 := make(chan string)

	go ping1(channel1)
	go ping2(channel2)

	select {
	case msg1 := <-channel1:
		fmt.Println("received", msg1)
	case msg2 := <-channel2:
		fmt.Println("received", msg2)
	case <-time.After(2000 * time.Millisecond):
		fmt.Println("no messages received.giving up.")
	}
}
//
received ping on channel2

退出通道

在已知需要停止执行的时间的情况下,使用超时时间是不错的选择,但在有些情况下,不确定select语句该在何时返回,因此不能使用定时器。在这种情况下,可使用退出通道。这种技术并非语言规范的组成部分,但可通过向通道发送消息来理解退出阻塞的select语句。

通过在select语句中添加一个退出通道,可向退出通道发送消息来结束该语句,从而停止阻塞。可将退出通道视为阻塞式select语句的开关。对于退出通道,可随便命名,但通常将其命名为stop或quit。在下面的示例中,在for循环中使用了一条select语句,这意味着它将无限制地阻塞,并不断地接收消息。通过向通道stop发送消息,可让select语句停止阻塞:从for循环中返回,并继续往下执行。

package main
import (
    "fmt"
    "time"
)
func sender(c chan string) {
    t := time.NewTicker(1 * time.Second)
    for {
        c <- "I'm sending a message"
        <-t.C
    }
}
func main() {
    messages := make(chan string)
    stop := make(chan bool)
    go sender(messages)
    go func() {
        time.Sleep(time.Second * 2)
        fmt.Println("Time's up!")
        stop <- true
    }()
    for {
        select {
            case <-stop:
            return
            case msg := <-messages:
            fmt.Println(msg)
        }
    }
}
//第一种情况
I'm sending a message
I'm sending a message
Time's up!
I'm sending a message
//第二种
I'm sending a message
I'm sending a message
I'm sending a message
Time's up!

使用包实现代码重用

创建包

除使用第三方包外,有时还可能需要创建包。下面将创建一个示例包,这是一个处理温度的包,提供了在不同温度格式之间进行转换的函数。先创建一个名为temperature.go的文件,并添加下方代码。

package temperature
func CtoF(c float64) float64 {
    return (c * 9 / 5) + 32)
}
func FtoC(f float64) float64 {
    return (f−32) * 9 / 5
}

导入这个包后,就可使用其中所有以大写字母打头的标识符了。要创建私有标识符(变量、函数等),可让它们以小写字母打头。

​ 为测试这个包,可创建一个测试文件,并将其命名为temperature_test.go,这个文件用来对这个包进行测试,如下所示。

package temperature

import (
    "testing"
)

type temperatureTest struct {
    i float64
    expected float64
}

var CtoFTests = []temperatureTest{
    {4.1, 39.38},
    {10, 50},
    {10, 14},
}
var FtoCTests = []temperatureTest{
    {32, 0},
    {50, 10},
    {5,15},
}

func TestCtoF(t *testing.T) {
    for _, tt := range CtoFTests {
        actual := CtoF(tt.i)
        if actual != tt.expected {
            t.Errorf("expected %v, actual %v", tt.expected, actual)
        }
    }
}

func TestFtoC(t *testing.T) {
    for _, tt := range FtoCTests {
        actual := FtoC(tt.i)
        if actual != tt.expected {
            t.Errorf("expected %v, actual %v", tt.expected, actual)
        }
    }
}

​ 如果您运行这些测试,将发现它们都通过了。

go test
PASS
ok   github.com/shapeshed/temperature   0.001s

​ 对于要发布到网上的包,从用户的角度考虑问题很重要。因此,推荐在包中包含如下3个文件。

  • 指出用户如何使用代码的LICENSE文件。

  • 包含有关包的说明信息的README文件。

  • 详细说明包经过了哪些修改的Changelog文件。

在README文件中,应包含有关包的信息、如何安装包以及如何使用包。您可能还想在其中包含有关如何参与改进项目的信息。如果要将包发布到Github,应考虑使用Markdown格式编写,因为Github会自动设置markdown文件的格式。

在Changelog文件中,应列出对包所做的修改,这可能包含功能添加情况和API删除情况。通常,使用git标签来指示发布情况,能够让用户轻松地下载特定版本。

go 语言命名规定

go代码格式设置

代码格式设置指的是如何在文件中设置代码的格式。具体地说,它指的是如何缩进代码以及如何使用回车。在代码格式设置方面,Go语言没有强制的约定,但存在被整个Go社区广泛采用并遵循的事实标准。

在代码格式设置方面,Go语言采取了实用而严格的态度。Go语言指定了格式设置约定,这种约定虽然并非强制性的,但命令gofmt可以实现它。虽然编译器不要求按命令gofmt指定的那样设置代码格式,但几乎整个Go社区都使用gofmt,并希望按这个命令指定的方式设置代码格式。

使用gofmt

为确保按要求的约定设置Go代码的格式,Go提供了gofmt。这个工具的优点在于,让您甚至都无须了解代码格式设置约定

可使用工具gofmt来设置其格式,使其遵循代码格式设置约定。

对文件运行gofmt时,将把结果打印到标准输出,但不修改原来的文件。

D:\study\go_study\first_go>gofmt quit.go
package main

import (
        "fmt"
        "time"
)

func sender(c chan string) {
        t := time.NewTicker(1 * time.Second)
        for {
                c <- "I'm sending a message"
                <-t.C
        }
}
func main() {
        messages := make(chan string)
        stop := make(chan bool)
        go sender(messages)
        go func() {
                time.Sleep(time.Second * 2)
                fmt.Println("Time's up!")
                stop <- true
        }()
        for {
                select {
                case <-stop:
                        return
                case msg := <-messages:
                        fmt.Println(msg)
                }
        }
}

要让工具gofmt修改文件,可使用标志 -w。这将使设置格式后的结果覆盖当前文件。

gofmt -w example01.go
usage: gofmt [flags] [path ...]
  -cpuprofile string
        write cpu profile to this file
  -d    display diffs instead of rewriting files
  -e    report all errors (not just the first 10 on different lines)
  -l    list files whose formatting differs from gofmt's
  -r string
        rewrite rule (e.g., 'a[b:len(a)] -> a[b:]')
  -s    simplify code
  -w    write result to (source) file instead of stdout

命名约定

以大写字母打头的标识符将被导出,而以小写字母打头的不会。

var Foo := "bar" // Exported
var foo := "bar" // Not Exported

在Go语言中,对于包含多个单词的变量名,约定是使用骆驼拼写法或帕斯卡拼写法,具体使用哪种拼写法,取决于变量是否需要导出。

var fileName // Camel Case
var FileName // Pascal Case

在Go程序中,经常使用指出了数据类型的简短变量名,这让程序员能够专注于逻辑而不是变量。在这种情况下,i表示整型(Integer)数据类型,s表示字符串数据类型,等等。一开始,您可能觉得这样做不好,但时间久了就会习惯这种被普遍接受的约定。

var i int = 3
var s string = "hello"
var b bool = true

在Go源代码中,接口名通常是这样得到的:在动词后面加上后缀er,形成一个名词。后缀er通常表示操作,因此这种命名方式表示操作,如Reader、Writer和ByteWriter。在有些情况下,这样生成的接口名可能不是现成的英语单词,如果您在Go源代码中搜索,将找到诸如Twoer这样的接口名。

对于要导出的函数,命名约定是尊重这样的事实,即导入包后,就可通过包名和函数名来访问它。例如,在标准库中,math包就遵守了这样的约定:将计算平方根的函数命名为Sqrt而不是MathSqrt。这合乎情理,因为使用这个函数时,代码为math.Sqrt而不是math.MathSqrt,另外,只要通过这个函数名就能知道它是做什么的。

命名总是带有一定的主观性,但花点时间考虑如何命名总是值得的。给变量、函数和接口命名时,需要考虑的一些因素包括以下几点。

  • 谁将使用这些代码?只是我自己还是整个团队?
  • 是否为当前项目制定了命名约定?
  • 不熟悉代码的人是否只需看一眼就能大致知道它是做什么的?

遵循命名约定很重要,但过于教条也是有害的。您需要考虑代码所在的上下文、其他相关人员以及小组的稳定性。在大多数情况下,都应该兼顾约定以及代码所在的上下文。

使用golint

golint是Go语言提供的一个官方工具。gofmt根据指定的约定设置代码的格式,而命令golint根据Go项目本身的约定查找风格方面的错误。默认不会安装golint,但可像下面这样安装它。

go get -u github.com/golang/lint/golint

要核实是否正确地安装了这个工具,可在终端中执行命令golint --help。

测试与性能

测试:软件开发最重要的方面

通常,软件测试是从概述功能的用户故事或规范衍生而来的。例如,如果有一个用户故事,指出一个函数接受两个数字,将它们相加并返回结果,就可轻松地编写对此进行检查的测试。有些项目还要求新代码有配套的测试。

编写良好的测试可充当文档。鉴于测试描述了程序期望的运行方式,新加入项目的开发人员通常可通过阅读测试来了解程序的运行方式。

常用的测试有多种。

  • 单元测试。
  • 功能测试。
  • 集成测试。
单元测试

单元测试针对一小部分代码,并独立地对它们进行测试。通常,这一小部分代码可能是单个函数,而要测试的是其输入和输出。

典型的单元测试可能指出,如果给函数x提供这些值,它应返回这个值。在确认程序最小的构件按期望的方式运行方面,这种测试很有用。在程序增大和变化过程中,单元测试是发现衰退的绝佳方式。衰退是修改过程中引入的Bug或错误。衰退意味着代码在修改前有效,但修改后无效了。单元测试通常能够发现衰退,因为它们测试的是程序的最小组成部分。

集成测试

集成测试通常测试的是应用程序各部分协同工作的情况。如果说单元测试检查的是程序的最小组成部分,那么集成测试检查的就是应用程序各个组件协同工作的情况。集成测试还检查诸如网络调用和数据库连接等方面,以确保整个系统按期望的那样工作。通常,集成测试比单元测试更难编写,因为这些测试需要评估应用程序依赖的各个部分。

功能测试

功能测试通常被称为端到端测试或由外向内的测试。这些测试从最终用户的角度核实软件按期望的那样工作,它们评估从外部看到的程序的运行情况,而不关心软件内部的工作原理。对用户来说,功能测试可能是最重要的测试。下面是一些功能测试的例子。

  • 测试命令行工具,确定在用户提供特定的输入时,它将显示特定的输出。
  • 对网页运行自动化测试。
  • 对API运行从外到内的测试,并检查响应代码和报头。
测试驱动开发

很多开发人员都提倡采用测试驱动开发(TDD)。这种做法从测试的角度考虑新功能,先编写测试来描述代码片段的功能,再着手编写代码。这很有多优点。

  • 有助于描述代码设计,因为考虑清楚代码片段的工作原理后,可改善代码设计。
  • 有助于提供有关功能工作原理的定义。
  • 未来可使用现成的测试来确定没有发生衰退。
  • 可使用现成的测试来核实正确地实现了代码。

通过采用TDD,工程师可改善设计,并根据确保测试得以通过来确认代码是有效的。

testing 包

为支持测试,Go语言在标准库中提供了testing包,它还支持命令go。

与Go语言的其他众多方面一样,您也许理解一些与testing包相关的设计良好的约定。

第一个约定是,Go测试与其测试的代码在一起。测试不是放在独立的测试目录中,而是与它们要测试的代码放在同一个目录中。测试文件是这样命名的:在要测试的文件的名称后面加上后缀_test,因此如果要测试的文件名为strings.go,则测试它的文件将名为strings_test.go,并位于文件strings.go所在的目录中。

Project
 |————strings.go
 |————strings_test.go

第二个约定是,测试为名称以单词Test打头的函数。

func TestTruth(t *testing.T)
  • 导入了testing包
  • 函数名TestTruth表明这是一个测试,因为它以单词Test打头。
  • 向这个函数传递了类型T,它包含很多用于测试代码的函数。

测试失败时,命令go test提供了一些很有用的信息:测试名、文件名以及导致测试失败的代码所在行。这些信息有助于避免测试失败,因为它们明确地指出了问题出在什么地方。

另一个约定是,在测试包中创建两个变量:got和want,它们分别表示要测试的值以及期望的值。

image-20211108103312854
package example

func Greeting(s string) string{
	return("Hello "+s)
}
package example

 import "testing"

func TestGreeting(t *testing.T) {
	got :=Greeting("George")
	want := "Hello George"
	if got != want {
		t.Fatalf("Expected %q, got %q", want, got)
	}
}
  • 创建了一个名为TestGreeting的测试,它指出了测试针对的函数。这能够提高可读性,因为很轻易就能知道TestGreeting测试的是函数Greeting。
  • 将函数Greeting返回的值赋给了变量got,这个变量表示要测试的值。
  • 变量want表示期望的输出。
  • 使用if语句检查got和want是否相同。如果不同,就引发错误。
D:\study\go_study\example>go test
PASS
ok      _/D_/study/go_study/example     0.051s
//如果测试失败
D:\study\go_study\example>go test
--- FAIL: TestGreeting (0.00s)
    greeting_test.go:9: Expected "Hell George", got "Hello George"
FAIL
exit status 1
FAIL    _/D_/study/go_study/example     0.053s

运行表格驱动测试

通常,函数和方法的响应随收到的输入而异,在这种情况下,如果每个测试只使用一个值,将导致大量重复的代码。

package example02

func translate(s string) string {
	switch s {
	case "en-US":
		return "Hello "
	case "fr-FR":
		return "Bonjour "
	case "it-IT":
		return "Ciao "
	default:
		return "Hello "
	}
}
 func Greeting(name, local string) string {
 	salutation := translate(local)
 	return (salutation+name)
 }
package example02

import "testing"

type GreetingTest struct {
	name string
	local string
	want string
}
var greetingTests = []GreetingTest{
	{"George", "en-US", "Hello George"},
	{"Chloé", "fr-FR", "Bonjour Chloé"},
	{"Giuseppe", "it-IT", "Ciao Giuseppe"},
}
func TestGreeting(t *testing.T) {
	for _, test := range greetingTests {
		got := Greeting(test.name, test.local)
		if got != test.want {
			t.Errorf("Greeting(%s,%s) = %v; want %v", test.name, test.local, got, test.want)
		}
	}
}
  • 创建一个结构体,用于存储编写测试所需的数据,包括输入和期望的输出。
  • 创建一个由结构体组成的切片,用于存储要测试的所有情形,包括期望输出。
  • 在测试中,遍历切片中的所有结构体,并测试实际输出是否与期望输出相同。
  • 只要有测试失败,就向控制台打印一条消息。

基准测试

在Go语言中,提供了测试函数性能(CPU和Memory)的测试方法,基准测试。

基准测试主要用来测试CPU和内存的效率问题,来评估被测代码的性能。测试人员可以根据这些性能指标的反馈,来优化我们的代码,进而提高性能问题。

第9章介绍了字符串以及如何拼接它们。您已经知道,拼接字符串的方法有多种,包括赋值、使用join附加以及使用缓冲区。第9章给出的建议是,使用缓冲区来拼接字符串,因为这种方法的性能最佳。这一点能够被证明吗?

Go提供了功能强大的基准测试框架,能够让您使用基准测试程序来确定完成特定任务时性能最佳的方式是哪一种。程序清单15.6显示了3种拼接字符串的方式,请不要过度关注其中的函数,因为这里的重点是性能。

基准测试和普通单元测试类似。 唯一的区别是基准测试接收的参数是*testing.B 而不是 *testing.T。 这两种类型都实现了 testing.TB 接口,这个接口提供了一些比较常用的方法 Errorf(), Fatalf(), and FailNow()

package strings

import (
    "bytes"
    "strings"
)
func StringFromAssignment(j int) string {
    var s string
    for i := 0; i < j; i++ {
        s += "a"
    }
    return s
}
func StringFromAppendJoin(j int) string {
    s := []string{}
    for i := 0; i < j; i++ {
        s = append(s, "a")
    }
    return strings.Join(s, "")
}
func StringFromBuffer(j int) string {
    var buffer bytes.Buffer
    for i := 0; i < j; i++ {
        buffer.WriteString("a")
    }
    return buffer.String()
}

这些函数根据传入的整数值生成相应长度的字符串。事实上,这些函数的功能完全相同。那么,如何确定哪种字符串拼接方式的性能是最佳的呢?

testing包包含一个功能强大的基准测试框架,它能够让您反复地运行函数,从而建立基准。您无须指定运行函数的次数,因为基准测试框架将通过调整它来获得可靠的数据集。基准测试结束后,将生成一个报告,指出每次操作耗用了多少ns。

基准测试名以关键字Benchmark打头,它们接受一个类型为B的参数,并对函数进行基准测试。

测试代码需要写在for循环中,并且循环中的最大之是b.N。


package strings

import "testing"

func BenchmarkStringFromAssignment(b *testing.B) {
	for n := 0; n < b.N; n++ {
		StringFromAssignment(100)
	}
}
func BenchmarkStringFromAppendJoin(b *testing.B) {
	for n := 0; n < b.N; n++ {
		StringFromAppendJoin(100)
	}
}
func BenchmarkStringFromBuffer(b *testing.B) {
	for n := 0; n < b.N; n++ {
		StringFromBuffer(100)
	}
}

这些基准测试使用循环反复地调用函数,以便建立基准。要运行这些测试,必须在命令go test中指定标志-bench。注意 : go test 会在运行基准测试之前之前执行包里所有的单元测试,所有如果你的包里有很多单元测试,或者它们会运行很长时间,你也可以通过 go test-run 标识排除这些单元测试,不让它们执行; 比如: go test -run=^$

基准测试函数会被一直调用直到b.N无效,它是基准测试循环的次数

b.N 从 1 开始,如果基准测试函数在1秒内就完成 (默认值),则 b.N 增加,并再次运行基准测试函数。

b.N 在近似这样的序列中不断增加;1, 2, 3, 5, 10, 20, 30, 50, 100 等等。 基准框架试图变得聪明,如果它看到当b.N较小而且测试很快就完成的时候,它将让序列增加地更快。

D:\study\go_study\example03>go test -bench=.
goos: windows
goarch: amd64
BenchmarkStringFromAssignment-6           147888              7663 ns/op
BenchmarkStringFromAppendJoin-6           283845              3954 ns/op
BenchmarkStringFromBuffer-6              1000000              1130 ns/op
PASS
ok      _/D_/study/go_study/example03   3.580s

这里运行了基准测试并显示基准值。从这些测试可知,赋值的性能最糟,使用join的性能居中,而使用缓冲区的性能最佳。这个基准测试表明,使用缓冲区来拼接字符串的速度最快!

-bench=. :表示的是运行所有的基准测试,. 表示全部。

-benchtime=5s:表示的是运行时间为5s,默认的时间是1s。

-benchmem:表示显示memory的指标。

-run=none:表示过滤掉单元测试,不去跑UT的cases。

对于每次操作是以10或个位数纳秒为单位计算的函数来说,指令重新排序和代码对齐的相对效应都将对结果产生影响。

可以使用-count 标识多次运行基准测试来解决这个问题:

除了查看时间性能之外,还可以查看内存,cpu等使用情况,也可以指定运行时间。这些通过运行测试程序时增加命令项来实现,比如:go test -bench=. -benchmem,即可查看每次调用的时间和分配的内存。

D:\study\go_study\example03>go test -bench=. -benchmem
goos: windows
goarch: amd64
BenchmarkStringFromAssignment-6           161838              7090 ns/op            5728 B/op         99 allocs/op
BenchmarkStringFromAppendJoin-6           317802              4057 ns/op            4192 B/op          9 allocs/op
BenchmarkStringFromBuffer-6              1000000              1138 ns/op             320 B/op          3 allocs/op
PASS
ok      _/D_/study/go_study/example03   3.762s

输出:

goos: windows:表示的是操作系统是windows。

goarch: amd64:表示目标平台的体系架构是amd64。

BenchmarkStringFromAssignment-6 6表示的是,运行时对应的GOMAXPROCS的值。此数字默认为启动时Go进程可见的CPU数。 你可以使用-cpu标识更改此值,可以传入多个值以列表形式来运行基准测试。

99 allocs/op:表示执行一次这个函数,分配内存的次数为99次。

提供测试覆盖率

测试覆盖率是度量代码测试详尽程度的指标,它指出了被测试执行了的代码所在的百分比值。go test命令提供了标志 -cover,可指出测试覆盖率。

testing包的例子

D:\study\go_study\example01>go test -cover
PASS
coverage: 50.0% of statements
ok      _/D_/study/go_study/example01   0.049s

调试

调试是确定程序为何不像预期那样工作的过程。程序不像预期那样工作的迹象有很多,包括编译错误、运行阶段错误、文件权限问题以及数据不正确等。

日志

日志指的是记录程序执行期间发生的情况。无论程序需不需要调试,都会产生日志,这对于理解程序的执行情况很有帮助。很多常见的应用程序都提供了日志功能,这些日志可用来监视应用程序的健康状况、跟踪问题以及发现问题。

日志并非为报告Bug而提供的,而是可供在Bug发生时使用的基础设施。

在Web服务器Nginx中,日志是一种未雨绸缪的调试方法,因为发生意外的事件时,有大量的信息可供用来调试问题。如果没有日志,问题将难以调试。另外,日志还可帮助完成应用程序的日常管理,确保程序像预期的那样运行。

Go语言提供了log包,让应用程序能够将日志写入终端或文件。日志消息中包含日期和时间,这在以后查看日志时很有用。log包还可用来记录发生的致命错误。

package main

import (
    "errors"
    "log"
}
func main() {
    var errFatal = errors.New("We only just started and we are crashing")
    log.Printf("This is a log message")
    log.Fatal(errFatal)
}
   
 //
 D:\study\go_study\first_go>go run log_sty.go
2021/11/09 11:28:44 This is a log message
2021/11/09 11:28:44 We only just started and we are crashing
exit status 1

日志可用来了解程序的执行情况,它对调试能起到一定的作用,但是,查看日志对了解发生的意外事件更有帮助。这意味着需要将日志写入文件,以便以后能够访问它们。要将日志写入文件,可使用Go语言本身提供的功能,也可使用操作系统提供的功能。要将日志写入文件,只需命令log包这样做即可

f, err := os.OpenFile("example03.log", os.O_APPEND|os.O_CREATE|os.O_RDWR, 0666)
if err != nil {
    log.Fatal(err)
}
defer f.Close()
log.SetOutput(f)

最后一行让log包使用文件example03.log来记录应用程序的日志。

​ 要将日志写入文件,还可使用操作系统提供的功能将日志输出从终端重定向到文件。这种方法不需要编写任何Go语言代码,因为它使用的是操作系统提供的功能。

package main
import (
    "log"
)
func main() {
    for i := 1; i <= 5; i++ {
        log.Printf("Log iteration %d", i)
    }
}

​ 以正常方式运行这个程序时,将向终端输出5条消息。然而,通过使用重定向功能,可将输出重定向到文件。在Linux和Windows系统中都可这样做。

go run example04.go > example04.log 2>&1

​ 通常,最好使用操作系统将日志重定向到文件,而不要使用Go语言代码。因为这种方法更灵活,能够让其他工具在必要时使用日志。

使用fmt包

fmt包可用来设置格式,因此必要时可使用它来输出数据,以方便调试。通过使用函数Printf,可创建要打印的字符串,并使用百分符号在其中引用变量。fmt包将对变量进行分析,并输出字符串。

s := "Hello World"
fmt.Printf("String is %v\n", s)

请注意,这个字符串末尾是字符\n,这表示换行,因为函数Printf默认不会添加回车。动词%v是类型的默认格式。

使用Delve

Go语言没有官方调试器,但很多社区项目都提供了Go语言调试器。Delve就是一个这样的项目,它为Go项目提供了丰富的调试环境。要安装Delve,可像下面这样做。

go get github.com/derekparker/delve/cmd/dlv

要检查是否正确地安装了Delve,可在终端中执行命令dlv --help

创建HTTP服务器

通过Hello World Web服务器宣告您的存在

标准库中的net/http包提供了多种创建HTTP服务器的方法,它还提供了一个基本路由器。传统上,通过创建一个Hello World程序来宣告您的存在。

package main

import "net/http"

func helloWorld(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("Hello World\n"))
}

func main() {
    http.HandleFunc("/", helloWorld)
    http.ListenAndServe(":8000", nil)
}
  • 导入net/http包。
  • 在main函数中,使用方法HandleFunc创建了路由/。这个方法接受一个模式和一个函数,其中前者描述了路径,而后者指定如何对发送到该路径的请求做出响应。
  • 函数helloWorld接受一个http.ResponseWriter和一个指向请求的指针。这意味着在这个函数中,可查看或操作请求,再将响应返回给客户端。在这里,使用了方法Write来生成响应。这个方法生成的HTTP响应包含状态、报头和响应体。[ ]byte声明一个字节切片并将字符串值转换为字节。这意味着方法Write可以使用[ ]byte,因为这个方法将一个字节切片作为参数。
  • 为响应客户端,使用了方法ListenAndServe来启动一个服务器,这个服务器监听localhost和端口8000。
image-20211111092346303

查看请求与响应

curl是一款用于发起HTTP请求的命令行工具

安装curl后,就可在开发和调试Web服务器时使用它。您可不使用Web浏览器,而使用curl来向Web服务器发送各种请求以及查看响应。

在macOS或Linux系统中,再打开一个终端;在Windows系统中,切换到Git Bash。执行下面的命令,其中的选项 -is指定打印报头,并忽略一些不感兴趣的内容。

curl -is http://localhost:8000

如果这个命令成功了,您将看到来自Web服务器的响应,其中包含报头和响应体。

HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Content-Length: 9
Date: Thu, 11 Nov 2021 01:30:36 GMT
Connection: keep-alive

Hello World
  • 这个响应使用的协议为HTTP 1.1,状态码为200。
  • 报头Date详细地描述了响应的发送时间。
  • 报头Content-Length详细地指出了响应的长度,这里是12字节。
  • 报头Content-Type指出了内容的类型以及使用的编码。在这里,响应的内容类型为text/plain,是使用utf-8进行编码的。
  • 最后输出的是响应体,这里是Hello World。

路由

HandleFunc用于注册对URL地址映射进行响应的函数。简单地说,HandleFunc创建一个路由表,让HTTP服务器能够正确地做出响应。

http.HandleFunc("/", helloWorld)
http.HandleFunc("/users/", usersHandler)
http.HandleFunc("/projects/", projectsHandler)

每当用户向 / 发出请求时,都将调用函数helloWorld,每当用户向 /users/发出请求时,都将调用函数usersHandler,依此类推。

有关路由器的行为,有以下几点需要注意。

  • 路由器默认将没有指定处理程序的请求定向到 /。
  • 路由必须完全匹配。例如,对于向 /users发出的请求,将定向到 /,因为这里末尾少了斜杆。
  • 路由器不关心请求的类型,而只管将与路由匹配的请求传递给相应的处理程序。

使用处理程序函数

在Go语言中,路由器负责将路由映射到函数,但如何处理请求以及如何向客户端返回响应,是由处理程序函数定义的。很多编程语言和Web框架都采用这样的模式,即先由函数来处理请求和响应,再返回响应。在这方面,Go语言也如此。处理程序函数负责完成如下常见任务。

  • 读写报头。

  • 查看请求的类型。

  • 从数据库中取回数据。

  • 分析请求数据。

  • 验证身份。

    ​处理程序函数能够访问请求和响应,因此一种常见的模式是,先完成对请求的所有处理,再将响应返回给客户端。响应生成后,就不能再对其做进一步的处理了。

在下面的示例中,使用了方法Write来发送请求。接下来的一行设置了响应的一个报头。鉴于响应已经发送,这行代码不会有任何作用,但能够通过编译。这里要说的是,发送响应应是最后一步。

func helloWorld(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("Hello World\n"))  // This has no effect, as the response is already written
    w.Header().Set("X-My-Header", "I am setting a header!")
}

处理404错误

默认路由器的行为是将所有没有指定处理程序的请求都定向到 /。回到第一个示例,如果用户请求的页面不存在,将调用给 / 指定的处理程序,从而返回响应Hello World和状态码200。

curl -is http://localhost:8000/asdfa
HTTP/1.1 200 OK
Date: Thu, 17 Nov 2016 09:07:51 GMT
Content-Length: 12
Content-Type: text/plain; charset=utf-8

然而,鉴于请求的路由不存在,原本应返回404错误(页面未找到)。为此,可在处理默认路由的函数中检查路径,如果路径不为 /,就返回404错误,

func helloWorld(w http.ResponseWriter, r *http.Request) {
	if r.URL.Path != "/" {
		http.NotFound(w, r)
		return
	}
	w.Write([]byte("Hello World\n"))
}

func main() {
	http.HandleFunc("/", helloWorld)
	http.ListenAndServe(":8000", nil)
}
  • 在处理程序函数helloWorld中,检查路径是否是 /。
  • 如果不是,就调用http包中的方法NotFound,并将响应和请求传递给它。这将向客户端返回一个404响应。
  • 如果路径与 / 匹配,则if语句将被忽略,进而发送响应Hello World

设置报头

创建HTTP服务器时,经常需要设置响应的报头。在创建、读取、更新和删除报头方面,Go语言提供了强大的支持。在下面的示例中,假设服务器将发送一些JSON数据。通过设置Content-Type报头,服务器可告诉客户端,发送的是JSON数据。

w.Header().Set("Content-Type", "application/json; charset=utf-8")

只要这行代码是在响应被发送给客户端之前执行的,这个报头就会被添加到响应中。程序清单18.3中,在发送的JSON内容前添加了报头。请注意,为简单起见,将数据设置成了一个字符串,但数据通常是从别的地方读取的,并编码为JSON格式。

func helloWorld(w http.ResponseWriter, r *http.Request) {
  if r.URL.Path != "/" {
      http.NotFound(w, r)
      return
  }
  w.Header().Set("Content-Type", "application/json; charset=utf-8")
  w.Write([]byte(`{"hello": "world"}`))
}
image-20211111095510897

curl -is http://localhost:8000/

   application/json.
  HTTP/1.1 200 OK
  Content-Type: application/json; charset=utf-8
  Date: Thu, 17 Nov 2016 09:28:44 GMT
  Content-Length: 18
  {"hello": "world"}

响应不同类型的内容

响应客户端时,HTTP服务器通常提供多种类型的内容。一些常用的内容类型包括text/plain、text/html、application/json和application/xml。如果服务器支持多种类型的内容,客户端可使用Accept报头请求特定类型的内容。这意味着同一个URL可能向浏览器提供HTML,而向API客户端提供JSON。只需对本章的示例稍作修改,就可让它查看客户端发送的Accept报头,并据此提供不同类型的内容,

func helloWorld(w http.ResponseWriter, r *http.Request) {
  if r.URL.Path != "/" {
       http.NotFound(w, r)
       return
  }
  switch r.Header.Get("Accept") {
  case "application/json":
      w.Header().Set("Content-Type", "application/json; charset=utf-8")
      w.Write([]byte(`{"message": "Hello World"}`))
  case "application/xml":
      w.Header().Set("Content-Type", "application/xml; charset=utf-8")
      w.Write([]byte(`<?xml version="1.0" encoding="utf-8"?><Message>Hello World</Message>`)
  default:
      w.Header().Set("Content-Type", "text/plain; charset=utf-8")
      w.Write([]byte("Hello World\n"))
  }
}
  • 在函数helloWorld中,添加了一条switch语句,用户检查客户端发送的Accept报头。
  • 这条switch语句根据Accept报头的内容相应地设置响应。
  • 如果没有找到这个报头,服务器默认将发送text/plain响应。

curl -si -H 'Accept: application/json' http://localhost:8000

   HTTP/1.1 200 OK
  Content-Type: application/json; charset=utf-8
  Date: Thu, 17 Nov 2016 09:28:44 GMT
  Content-Length: 18

  {"hello": "world"}

响应不同类型的请求

除响应以不同类型的内容外,HTTP服务器通常也需要能够响应不同类型的请求。客户端可发出的请求类型是HTTP规范中定义的,包括GET、POST、PUT和DELETE。要使用Go语言创建能够响应不同类型请求的HTTP服务器,可采用类似于提供多种类型内容的方法,

switch r.Method {
  case "GET":
      w.Write([]byte("Received a GET request\n"))
  case "POST":
      w.Write([]byte("Received a POST request\n"))
  default:
      w.WriteHeader(http.StatusNotImplemented)
      w.Write([]byte(http.StatusText(http.StatusNotImplemented)) + "\n")
  }
  • 服务器不是根据内容类型来生成响应,而是根据请求方法来生成响应。
  • switch语句根据请求类型发送不同的响应。
  • 在这个示例中,发送一个plain/text响应来指出请求的类型。
  • 如果请求方法不是GET或POST,就执行default子句,发送501(Not Implemented HTTP)响应。代码501意味着服务器不明白或不支持客户端使用的HTTP请求方法

curl -si -X GET http://localhost:8000

  HTTP/1.1 200 OK
  Date: Thu, 17 Nov 2016 10:02:49 GMT
  Content-Length: 23
  Content-Type: text/plain; charset=utf-8

  Received a GET request

获取GET和POST请求中的数据

HTTP客户端可在HTTP请求中向HTTP服务器发送数据,这样的典型示例包括以下几点。

  • 提交表单。
  • 设置有关要返回的数据的选项。
  • 通过API管理数据。

在Go语言中,获取客户端请求中的数据很简单,但获取方式随请求类型而异。对于GET请求,其中的数据通常是通过查询字符串设置的。一个通过GET请求发送数据的例子是,在Google网站上搜索。在这个示例中,URL包含以查询字符串的方式指定的搜索关键字。

https://www.google.com/?q=golang

Web服务器可能读取查询字符串中的数据,并使用它来做些事情,如从数据库获取相应的数据,再将其返回给客户端。在Go语言中,以字符串映射的方式提供了请求中的查询字符串参数,您可使用range子句来遍历它们。

func queryParams(w http.ResponseWriter, r *http.Request) {
  for k, v := range r.URL.Query() {
    fmt.Printf("%s: %s\n", k, v)
  }
}

在POST请求中,数据通常是在请求体中发送的。要读取并使用这些数据,可像下面这样做。

func queryParams(w http.ResponseWriter, r *http.Request) {
  reqBody, err := ioutil.ReadAll(r.Body)
  if err != nil {
    log.Fatal(err)
  }
  fmt.Printf("%s", reqBody)
}

警告:可别信任用户的输入。

这里简单地说一说安全问题。在服务器上,应将收到的数据视为不可信任的。攻击者可能发送请求,企图窃取信息、获取对服务器的访问权或删除数据库。所有进入服务器的数据都应视为不安全的,应过滤后再使用。本章的这些示例中,在未经过滤的情况下就直接使用了收到的数据,但编写用于生产环境的代码时,务必确保接收的数据没问题后再使用。

package main

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

func helloWorld(w http.ResponseWriter, r *http.Request) {
	if r.URL.Path != "/" {
		http.NotFound(w, r)
		return
	}

	switch r.Method {
	case "GET":
		for k, v := range r.URL.Query() {
			fmt.Printf("%s: %s\n", k, v)
		}
	case "POST":
		reqBody, err := ioutil.ReadAll(r.Body)
		if err != nil {
		   log.Fatal(err)
		}
		fmt.Printf("%s", reqBody)
	default:
		w.WriteHeader(http.StatusNotImplemented)
		w.Write([]byte(http.StatusText(http.StatusNotImplemented)))
	}
}

func main() {
	http.HandleFunc("/", helloWorld)
	http.ListenAndServe(":8000", nil)
}

GET请求: curl -si "http://localhost:8000/?foo=1&bar=2"

  foo: [1]
  bar: [2]

POST请求:curl -si -X POST -d "some data to send" http://localhost:8000/

some data to send

http包提供了方法ListenAndServeTLS,可用来创建HTTPS(TLS)服务器。这个方法的工作原理与ListenAndServe相同,但必须向它传递证书和密钥文件。

GET请求向指定资源请求数据,而POST请求向指定资源提交数据。GET请求可能通过查询字符串参数发送数据,而POST请求通过消息体向服务器发送数据。

创建HTTP客户端

发出GET请求

Go语言在net/http包中提供了一个快捷方法,可用于发出简单的GET请求。使用这个方法意味着不需要考虑如何配置HTTP客户端以及如何设置请求报头。如果只是要从远程网站获取一些数据,那么默认配置完全够用。

在下方程序中,客户端访问网站ifconfig.io的主页。这个网站报告请求网页客户端的IP地址。

package main
import (
    "fmt"
    "io/ioutil"
    "log"
    "net/http"
)
func main() {
    response, err := http.Get("https://ifconfig.io/")
    if err != nil {
        log.Fatal(err)
    }
    defer response.Body.Close()
    body, err := ioutil.ReadAll(response.Body)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("%s", body)
}
  • 使用net/http包向https://ifconfig.io/ 发出GET请求。
  • 如果请求出错(如没有网络连接),就将错误写入日志再退出。
  • 客户端读取所有数据后,将连接关闭。
  • 将响应体读取到变量中以便打印。
  • 如果读取响应体时出错,就将错误写入日志再退出。
  • 打印响应体

发出POST请求

标准库中的net/http包也提供了用于发出简单POST请求的快捷方法——Post,它支持设置内容类型以及发送数据。

客户端向https://httpbin.org/post发出POST请求。httpbin是一个用于测试HTTP客户端的工具,而端点 /post返回客户端发送给它的数据,以及有关客户端的信息。

package main
import (
    "fmt"
    "io/ioutil"
    "log"
    "net/http"
    "strings"
)
func main() {
    postData := strings.NewReader(`{ "some": "json" }`)
    response, err := http.Post("https://httpbin.org/post", "application/json",postData)
    if err != nil {
        log.Fatal(err)
    }
    defer response.Body.Close()
    body, err := ioutil.ReadAll(response.Body)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("%s", body)
}
  • 声明变量postData并将一个JSON字符串赋给它。这里使用了标准库strings将其转换为io.Reader,为传输做好准备。

  • 使用方法Post发出POST请求。其中第一个参数是要发送到的URL,第二个为数据的内容类型,第三个为数据本身。

  • 如果请求出错(如没有网络连接),就将错误写入日志再退出。

  • 客户端读取所有数据后,将连接关闭。

  • 将响应体读取到变量中以便打印。

  • 如果读取响应体时出错,就将错误写入日志再退出。

  • 打印响应体

{
	"args": {},	
	"data": "{ \"some\": \"json\" }",
	"files": {},
	"form": {},
	"headers": {
		"Accept-Encoding": "gzip",
		"Connection": "close",
		"Content-Length": "18",
		"Content-Type": "application/json",
		"Host": "httpbin.org",
		"User-Agent": "Go-http-client/1.1"
	},
	"json": {  "some": "json" },
	"origin": "68.235.53.83",
	"url": "https://httpbin.org/post"
}

进一步控制HTTP请求

要进一步控制HTTP请求,应使用自定义的HTTP客户端。您可使用net/http包提供的默认HTTP客户端,但这将自动使用默认设置,除非您手工修改这些设置。

package main
import (
    "fmt"
    "io/ioutil"
    "log"
    "net/http"
)
func main() {
    client := &http.Client{}
    request, err := http.NewRequest("GET", "https://ifconfig.co", nil)
    if err != nil {
        log.Fatal(err)
    }
    response, err := client.Do(request)
    defer response.Body.Close()
    body, err := ioutil.ReadAll(response.Body)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("%s", body)
}

对为使用自定义HTTP客户端所做的修改解读如下。

  • 不使用net/http包的快捷方法Get,而创建一个HTTP客户端。
  • 使用方法NewRequest向https://ifconfig.co发出GET请求。
  • 使用方法Do发送请求并处理响应。

使用自定义HTTP客户端意味着可对请求设置报头、基本身份验证和cookies。鉴于使用快捷方法和自定义HTTP客户端时,发出请求所需代码的差别很小,建议除非要完成的任务非常简单,否则都使用自定义HTTP客户端。

调试HTTP请求

创建HTTP客户端时,了解收发请求和响应的报头和数据对整个流程很有用。为此,可使用标准库中的fmt包来输出各项数据,但net/http/httputil也提供了能够让您轻松调试HTTP客户端和服务器的方法。这个包中的方法DumpRequestOutDumpResponse能够让您查看请求和响应。

可对前一个示例进行改进,以使用net/http/httputil包中的DumpRequestOut和DumpResponse方法来支持日志功能。这些方法显示请求和响应的报头,还有返回的响应体。

可在调试时添加这些方法,并在调试完毕后删除它们,但还有一种选择,那就是使用环境变量来开关调试。标准库中的os包支持读取环境变量,这能够让您轻松地开关调试。

package main

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

func main() {
    debug := os.Getenv("DEBUG")
    client := &http.Client{}
    request, err := http.NewRequest("GET", "https://ifconfig.co", nil)
    if err != nil {
        log.Fatal(err)
    }
    if debug == "1" {
        debugRequest, err := httputil.DumpRequestOut(request, true)
        if err != nil {
            log.Fatal(err)
        }
        fmt.Printf("%s", debugRequest)
    }
    response, err := client.Do(request)
    defer response.Body.Close()
    if debug == "1" {
        debugResponse, err := httputil.DumpResponse(response, true)
        if err != nil {
            log.Fatal(err)
        }
        fmt.Printf("%s", debugResponse)
    }
    body, err := ioutil.ReadAll(response.Body)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("%s\n", body)
}
# For OSX and Linux
$ DEBUG=1 go run example04.go
# For Windows
$ set DEBUG=1
$ go run example04.go

要请求JSON数据,可修改客户端,设置一个请求JSON的报头。为此,可像下面这样做。

request.Header.Add("Accept", "application/json")

处理超时

HTTP事务会为接收响应等待一定的时间。客户端向服务器发送请求后,完全无法知道响应会在多长时间内返回。在底层,有大量影响响应速度的变数。

  • DNS查找速度。
  • 打开到服务器IP地址的TCP套接字的速度。
  • 建立TCP连接的速度。
  • TLS握手的速度(如果连接是TLS的)。
  • 向服务器发送数据的速度。
  • 重定向的速度。
  • Web服务器返回响应的速度。
  • 将数据传输到客户端的速度。

HTTP响应的速度无法预测就够了。例如,向同一个Web服务器发出请求时,一次可能需要1000 ms,而另一次可能需要10000ms。对HTTP客户端来说,这是一个问题,因为每条连接都会占用一些内存,还会使用底层操作系统中的一个套接字。如果连接的速度很慢,则程序很快就会出现内存泄露,或耗尽底层操作系统的资源。

使用默认的HTTP客户端时,没有对请求设置超时时间。这意味着如果服务器没有响应,则请求将无限期地等待或挂起。对于任何请求,都建议设置超时时间。这样如果请求在指定的时间内没有完成,将返回错误。

client := &http.Client{
  Timeout: 1 * time.Second,
}

通过创建一个传输(transport)并将其传递给客户端,可更细致地控制超时:控制HTTP连接的各个阶段。在大多数情况下,使用Timeout就足以控制整个HTTP事务,但在Go语言中,还可通过创建传输来控制HTTP事务的各个部分。

tr := &http.Transport{
  DialContext: (&net.Dialer{
    Timeout:  30 * time.Second,
    KeepAlive: 30 * time.Second,
  }).DialContext,
  TLSHandshakeTimeout:  10 * time.Second,
  IdleConnTimeout:    90 * time.Second,
  ResponseHeaderTimeout: 10 * time.Second,
  ExpectContinueTimeout: 1 * time.Second,
}

client := &http.Client{
  Transport: tr,

处理JSON

json简介

JavaScript对象表示法(JavaScript Object Notation,JSON)是一种用于存储和交换数据的格式。JSON可以键值对的方式表示数据,也可以数组的方式表示数据。JSON最初是一个JavaScript子集,但它现在已独立于语言,实际上,大多数语言都支持JSON数据编码和解码。

{
  "name":"George",
  "age":40,
  "children":[ "Bea", "Fin"]
}

在go语言中使用JSON

​ Go语言非常适合用来创建收发JSON的客户端和服务器。标准库提供了encoding/json包,可用于编码和解码JSON数据。encoding/json包提供了函数Marshal,可用于将Go数据编码为JSON。

import (
  "encoding/json"
  "fmt"
  "log"
)

type Person struct {
  Name  string
  Age   int
  Hobbies []string
}

func main() {
  hobbies := []string{"Cycling", "Cheese", "Techno"}
  p := Person{
    Name:   "George",
    Age:   40,
    Hobbies: hobbies,
  }
  fmt.Printf("%+v\n", p)
  jsonByteData, err := json.Marshal(p)
  if err != nil {
    log.Fatal(err)
  }
  jsonStringData := string(jsonByteData)
  fmt.Println(jsonStringData)
}
//
D:\study\go_study\first_go>go run json_sty.go
{Name:George Age:40 Hobbies:[Cycling Cheese Techno]}
{"Name":"George","Age":40,"Hobbies":["Cycling","Cheese","Techno"]}

​ 对于结构体,可给其数据字段指定标签,对于带JSON标签的数据,将使用标签中的数据替换它。要正确地转换为JSON要求的骆驼拼写法,只需给结构体的字段加上标签即可。

type Person struct {
    FirstName string `json:"firstName"`
    Age   int   `json:"age"`
    Hobbies []string `json:"hobbies"`
}

//
{FirstName:George Age:40 Hobbies:[Cycling Cheese Techno]}
{"firstName":"George","age":40,"hobbies":["Cycling","Cheese","Techno"]}

​ 结构体标签还可用于指定在编码为JSON时忽略结构体中的空字段。默认情况下,如果结构体的字段被设置为空值,则编码为JSON格式后,将包含Go语言零值规则指定的值。

​ 要指定在编码为JSON格式时忽略零值,可使用结构体标签指出字段可能为空,如果确实为空就忽略它。为此,可在JSON键名后面加上omitempty。

type Person struct {
    Name  string  `json:"name,omitempty"`
    Age   int   `json:"age,omitempty"`
    Hobbies []string `json:"hobbies,omitempty"`
}

//
{Name: Age:40 Hobbies:[Cycling Cheese Techno]}
{"age":40,"hobbies":["Cycling","Cheese","Techno"]}

只要是可导出成员(变量首字母大写),都可以转成json。

解码json

收到的数据可能来自数据库、API调用或配置文件。原始JSON就是文本格式的数据,在Go语言中可表示为字符串。函数Unmarshal接受一个字节切片以及一个指定要将数据解码为何种格式的接口。根据数据是如何收到的,它可能是字节切片,也可能不是。如果不是字节切片,就必须先进行转换,再将其传递给函数Unmarshal。

unmarshal
var jsonStringData := {
	"name":"George",
	"age":40,
	"hobbies":["Cycling","Cheese","Techno"]
}
jsonByteData := []byte(jsonStringData)

与将数据编码为JSON格式一样,必须定义一个接口,以指定要将数据解码为何种格式。与将数据编码为JSON格式一样,可使用结构体标签来告诉解码器如何将键映射到字段。

type Person struct {
	Name  string  `json:"name"`
    Age   int   `json:"age"`
    Hobbies []string `json:"hobbies"`
}
type StuRead struct {
    Name  interface{} `json:"name"`
    Age   interface{}
    HIgh  interface{}
    sex   interface{}
    Class interface{} `json:"class"`
    Test  interface{}
}

type Class struct {
    Name  string
    Grade int
}

func main() {
    //json字符中的"引号,需用\进行转义,否则编译出错
    //json字符串沿用上面的结果,但对key进行了大小的修改,并添加了sex数据
    data:="{\"name\":\"张三\",\"Age\":18,\"high\":true,\"sex\":\"男\",\"CLASS\":{\"naME\":\"1班\",\"GradE\":3}}"
    str:=[]byte(data)

    //1.Unmarshal的第一个参数是json字符串,第二个参数是接受json解析的数据结构。
    //第二个参数必须是指针,否则无法接收解析的数据,如stu仍为空对象StuRead{}
    stu:=StuRead{}
    err:=json.Unmarshal(str,&stu)

    //解析失败会报错,如json字符串格式不对,缺"号,缺}等。
    if err!=nil{
        fmt.Println(err)
    }

    fmt.Println(stu)
}
//
{张三 18 true <nil> map[GradE:3 naME:1] <nil>}
  • json字符串解析时,需要一个“接收体”接受解析后的数据,且Unmarshal时接收体必须传递指针。否则解析虽不报错,但数据无法赋值到接受体中。如这里用的是StuRead{}接收。

  • 解析时,接收体可自行定义。json串中的key自动在接收体中寻找匹配的项进行赋值。匹配规则是:

    • 先查找与key一样的json标签,找到则赋值给该标签对应的变量(如Name)。
    • 没有json标签的,就从上往下依次查找变量名与key一样的变量,如Age。或者变量名忽略大小写后与key一样的变量。如HIgh,Class。第一个匹配的就赋值, 后面就算有匹配的也忽略。
      (前提是该变量必需是可导出的,即首字母大写)。
  • 不可导出的变量无法被解析(如sex变量,虽然json串中有key为sex的k-v,解析后其值仍为nil,即空值)

  • 当接收体中存在json串中匹配不了的项时,解析会自动忽略该项,该项仍保留原值。如变量Test,保留空值nil。

解析后的变量类型:
func printType(stu *StuRead){
    nameType:=reflect.TypeOf(stu.Name)
    ageType:=reflect.TypeOf(stu.Age)
    highType:=reflect.TypeOf(stu.HIgh)
    sexType:=reflect.TypeOf(stu.sex)
    classType:=reflect.TypeOf(stu.Class)
    testType:=reflect.TypeOf(stu.Test)

    fmt.Println("nameType:",nameType)
    fmt.Println("ageType:",ageType)
    fmt.Println("highType:",highType)
    fmt.Println("sexType:",sexType)
    fmt.Println("classType:",classType)
    fmt.Println("testType:",testType)
}
///
nameType: string
ageType: float64
highType: bool
sexType: <nil>
classType: map[string]interface {}
testType: <nil>
  • json解析后,json串中value,只要是”简单数据”,都会按照默认的类型赋值,如”张三”被赋值成string类型到Name变量中,数字18对应float64,true对应bool类型

  • “简单数据”:是指不能再进行二次json解析的数据,如”name”:”张三”只能进行一次json解析。
    “复合数据”:类似”CLASS\”:{\”naME\”:\”1班\”,\”GradE\”:3}这样的数据,是可进行二次甚至多次json解析的,因为它的value也是个可被解析的独立json。即第一次解析key为CLASS的value,第二次解析value中的key为naME和GradE的value

  • 对于”复合数据”,如果接收体中配的项被声明为interface{}类型,go都会默认解析成map[string]interface{}类型。如果我们想直接解析到struct Class对象中,可以将接受体对应的项定义为该struct类型。如下所示:

type StuRead struct {
...
普通struct类型
	Class Class `json:"class"`
	//指针类型
	Class *Class `json:"class"`
}
{张三 18 true <nil> {13} <nil>}
nameType: string
ageType: float64
highType: bool
sexType: <nil>
classType: main.Class
testType: <nil>


{张三 18 true <nil> 0xc0000985a0 <nil>}
nameType: string
ageType: float64
highType: bool
sexType: <nil>
classType: *main.Class
testType: <nil>
解析时,如果接受体中同时存在多个匹配的项

测试1

type StuReadName struct {
	NAme interface{}
	Name  interface{}
	NAMe interface{}
}

data:="{\"NAmE\":\"张三\"}"
str:=[]byte(data)

{张三 <nil> <nil>}


type StuRead struct {
    NAme interface{}
    Name  interface{}
    NAMe interface{}    `json:"name"`
}
//
{张三 <nil> <nil>}
type StuReadName struct {
	NAme interface{}
	Name  interface{} `json:"name"`
	NAMe interface{}
}

{张三 <nil> <nil>}
type StuReadName struct {
	NAme interface{}`json:"name"`
	Name  interface{}
	NAMe interface{}
}
///
{张三 <nil> <nil>}

测试2

type StuReadName struct {
	NAme interface{} `json:"name"`
	Name  interface{}
	NAMe interface{} `json:"name"`
}
///
{<nil> 张三 <nil>}

测试3

type StuReadName struct {
	NAme interface{} `json:"name"`
	Name  interface{} `json:"name"`
	NAMe interface{} `json:"name"`
}

{<nil> <nil> <nil>}

测试4

type StuReadName struct {
	NAme interface{}
	Name  interface{}
	NAMe interface{}
}

data:="{\"NAmE\":\"张三\",\"NAME\":\"李四\"}"
str:=[]byte(data)
\\\
{李四 <nil> <nil>}

映射数据类型

​ 编码和解码JSON时,必须考虑Go和JavaScript表示数据类型的方式,这很重要。Go是一种强类型语言,而JavaScript是一种弱类型语言,即不显式地声明变量的数据类型。这些数据类型不会自动映射到Go语言中的数据类型,因此encoding/json包执行显式的数据类型转换。

JSONGo
Booleanbool
Numberfloat64
Stringstring
Array[ ]interface{}
Objectmap[string]interface{}
Nullnil

​ 创建用于编码和解码JSON的结构体时,必须对上述数据类型的对应关系做到心中有数,因为如果数据类型不匹配,encoding/ json包将引发错误。

处理通过HTTP收到的JSON

​ 在Go语言中,通过HTTP请求获取JSON时,收到的数据为流而不是字符串或字节切片。在这种情况下,应使用encoding/json包中的另一个方法。

​ 由于获取的数据为流,因此可使用encoding/json包中的函数NewDecoder。这个函数接受一个io.Reader(这正是http.Get返回的类型),并返回一个Decoder。通过对返回的Decoder调用方法Decode,可将数据解码为结构体。与以前一样,Decode也接受一个结构体,因此必须创建一个结构体实例,并将其作为参数传递给Decode。

type User struct {
    Name string `json:"name"`
    Blog string `json:"blog"`
}

func main() {
    var u User
    res, err := http.Get("https://api.github.com/users/shapeshed")
    if err != nil {
        log.Fatal(err)
    }
    defer res.Body.Close()
    err = json.NewDecoder(res.Body).Decode(&u)
    if err != nil {
        log.Fatal(err)
  }
    fmt.Printf("%+v\n", u)
}

处理文件

使用ioutil包读写文件

​ 鉴于处理文件是一种很常见的任务,标准库提供了ioutil包,能够快速执行众多涉及读写文件的操作。实际上,这个包几乎就是一个os模块包装器,使用它需要编写的代码更少,且无须执行清理操作。如果您需要执行下述任何操作,且无须做细致的控制,则使用ioutil包是不错的选择。

  • 读取文件。
  • 列出目录的内容。
  • 创建临时目录。
  • 创建临时文件。
  • 创建文件。
  • 写入文件。
读取文件

ioutil包提供了函数ReadFile,这个函数将一个文件名作为参数,并以字节切片的方式返回文件的内容。这意味着如果要将文件内容作为字符串使用,则必须将返回的字节切片转换为字符串

func main() {
  fileBytes, err := ioutil.ReadFile("example01.txt")
  if err != nil {
    log.Fatal(err)
  }
  fmt.Println(fileBytes)

  fileString := string(fileBytes)
  fmt.Println(fileString)
}

[104 101 108 108 111 32 119 111 114 108 100]
hello world
创建文件

函数WriteFile接受一个文件名、要写入文件的数据以及应用于文件的权限。

文件权限是从UNIX权限衍生而来的,它们对应于3类用户:文件所有者、与文件位于同一组的用户、其他用户。处理文件时,理解文件权限是确保安全的重要方面,因为错误地设置权限意味着数据可能被原本不应该让他读取的人获得。

​ Go语言使用UNIX权限的数字表示法,很多用于处理文件的函数都将权限值作为参数。

符号表示法数字表示法说明
----------0000无权限
-rwx------0700只有所有者能够读取、写入和执行
-rwxrwx—0770所有者及其所在的用户组能够读取、写入和执行
-rwxrwxrwx0777所有人都能够读取、写入和执行
—x–x–x0111所有人都能够执行
–w–w–w-0222所有人都能够写入
–wx-wx-wx0333所有人都能够写入和执行
-r–r–r–0444所有人都能够读取
-r-xr-xr-x0555所有人都能够读取和执行
-rw-rw-rw-0666所有人都能够读取和写入
-rwxr-----0740所有者能够读取、写入和执行,而所有者所在的用户组能够读取

​ 符号表示法是数字表示法的视觉表示。符号表示法总共包含10个字符。最左边的字符指出了文件是普通文件、目录还是其他东西,如果这个字符为-,就表示文件为普通文件;接下来的3个字符指定了文件所有者的权限;再接下来的3个字符表示所有者所在用户组的权限;而最后3个字符表示其他人的权限。

在UNIX型系统中,文件的默认权限为0644,即所有者能够读取和写入,而其他人只能读取。在文件系统中创建文件时,应考虑如何给它指定权限。如果不确定该如何指定权限,使用默认的UNIX权限就可以了。

​ 下方示例程序将创建一个空文件,并将其权限设置为0644。

package main

import (
  "fmt"
  "io/ioutil"
  "log"
)

func main() {
  b := make([]byte, 0)
  err := ioutil.WriteFile("example02.txt", b, 0644)
  if err != nil {
    log.Fatal(err)
  }
}

写入文件

函数WriteFile也可用来写入文件

示例:将字符串写入文件,必须先将其转换为字节切片。如果指定的文件不存在,将创建它。

func main() {
  s := "Hello World"
   err := ioutil.WriteFile("example03.txt", []byte(s), 0644)
  if err != nil {
   log.Fatal(err)
  }

列出目录的内容

​ 要处理文件系统中的文件,必须知道目录结构。为此,ioutil包提供了便利函数ReadDir,它接受以字符串方式指定的目录名,并返回一个列表,其中包含按文件名排序的文件。文件名的类型为FileInfo,包含如下信息。

  • Name:文件的名称。
  • Size:文件的长度,单位为字节。
  • Mode:用二进制位表示的权限。
  • ModTime:文件最后一个被修改的时间。
  • IsDir:文件是否是目录。
  • Sys:底层数据源。
files, ers := ioutil.ReadDir(".")
	 if ers != nil {
		 log.Fatal(err)
		 }

	 for _, file := range files {
		 fmt.Println(file.Mode(), file.Name(),file.Size(),file.IsDir())
		 }
///
drwxrwxrwx .idea 0 true
-rw-rw-rw- example01.txt 11 false
-rw-rw-rw- example02.txt 11 false
-rw-rw-rw- ioutl_sty.go 549 false

复制文件

ioutil包可用于执行一些常见的文件处理操作,但要执行更复杂的操作,应使用os包。os包运行在稍低的层级,因此使用它时,必须手工关闭打开的文件。如果您阅读os包的源代码,将发现ioutil包中的很多函数都是os包包装器。

要复制文件,只需结合使用os包中的几个函数。以编程方式复制文件的步骤如下。

1.打开要复制的文件。

2.读取其内容。

3.创建并打开要将这些内容复制到其中的文件。

4.将内容写入这个文件。

5.关闭所有已打开的文件。

func main() {
    from, err := os.Open("./example01.txt")
    if err != nil {
        log.Fatal(err)
    }
    defer from.Close()
    to, err := os.OpenFile("./example05.copy.txt", os.O_RDWR|os.O_CREATE, 0666)
    if err != nil {
        log.Fatal(err)
    }
    defer to.Close()
    _, err = io.Copy(to, from)
    if err != nil {
        log.Fatal(err)
    }
}
  • 使用os包中的函数Open来读取磁盘文件。
  • 使用defer语句在程序完成其他所有操作后关闭文件。
  • 使用函数OpenFile打开文件。第一个参数是要打开(如果不存在,就创建)的文件的名称;第二个参数是用于文件的标志,在这里指定的是读写文件,并在文件不存在时创建它;最后一个参数设置文件的权限。
  • 再次使用defer语句在执行完其他操作后关闭文件。
  • 使用io包中的函数Copy复制源文件的内容,并将其写入目标文件。

删除文件

os包提供了函数Remove。需要指出的是,使用这个函数时,不会发出警告,您也无法将删除的文件恢复,因此务必要谨慎。

err := os.Remove("./deleteme.txt")

正则表达式

在Go语言中,正则表达式功能是由regex包提供的,这个包实现了正则表达式的查找和模式匹配功能。它使用的是RE2语法,这大致与Perl和Python使用的语法相同。它操作的目标可以是字符串,也可以是字节。

函数MatchString,它接受一个正则表达式模式和一个字符串,并根据是否匹配返回true或false

import (
	"fmt"
	"log"
	"regexp"
)

func main() {
	 needle := "chocolate"
	 haystack := "Chocolate is my favorite!"
	 match, err := regexp.MatchString(needle, haystack)
	 if err != nil {
		 log.Fatal(err)
		 }
	 fmt.Println(match)
}
//
false

正则表达式区分大小写

指定查找时不区分大小写,需要使用一种特殊语法。

needle := "(?i)chocolate"

语法

字符含义
.与除换行符之前的其他任何字符都匹配
*与零个或多个指定的字符匹配
^表示行首
$表示行尾
+匹配一次或多次
?匹配零或一次
[ ]与方括号内指定的任何字符都匹配
{n}匹配n次
{n,}匹配n次或更多次
{m,n}最少匹配m次,最多匹配n次

^[a-zA-Z0-9]{5-12}$

  • 字符 ^ 表示从字符串开头开始匹配。
  • 方括号([ ])内的字符集表示与其中的任何字符都匹配。
  • 大括号({})内的数字表示应至少匹配5次,但最多不超过12次。

使用正则表达式验证数据

正则表达式可用于验证程序的输入数据,这是一种分析和理解数据的高效方式。要将正则表达式赋给变量,必须先对其进行分析。用于分析正则表达式的函数有两个。

  • Compile:在正则表达式未能通过编译时返回错误。
  • MustCompile:在正则表达式无法编译时引发panic。
re := regexp.MustCompile("^[a-zA-Z0-9]{5,12}")
fmt.Println(re.MatchString("slimshady99"))
fmt.Println(re.MatchString("!asdf£33£3"))

使用正则表达式来变换数据

正则表达式也可用于完成这种任务——匹配模式并进行替换。

一些有关用户名的规则:包含的字符不能超过12个。要清洗太长的用户名,可先将其截断为只包含12个字符。然后根据正则表达式对其进行评估,看看它是否包含非法字符。如果包含非法字符,可将其替换为合法字符。

func main() {
  usernames := [4]string{
    "slimshady99",
    "!asdf£33£3",
    "roger",
    "Iamthebestuserofthisappevaaaar",
  }

  re := regexp.MustCompile("^[a-zA-Z0-9]{5,12}")
  an := regexp.MustCompile("[[:^alnum:]]")

  for _, username := range usernames {
    if len(username) > 12 {
      username = username[:12]
      fmt.Printf("trimmed username to %v\n", username)
    }
    if !re.MatchString(username) {
      username = an.ReplaceAllString(username, "x")
      fmt.Printf("rewrote username to %v\n", username)
    }
  }
}
///
rewrote username to xasdfx33x3
trimmed username to Iamthebestus

时间编程

时间元素编程

Go语言标准库提供了time包,其中包含用于同当前时间交互以及测量时间的函数和方法。

fmt.Println(time.Now())
///
2021-11-16 18:48:23.6178737 +0800 CST m=+0.004198601

实际上,它来自底层操作系统。取决于底层操作系统中的时间准确度,这种时间可能很有用,也可能没有用。在大多数操作系统中,用户都可设置时间。

时间受众多变数的影响,其中包括在操作系统中设置的时间不正确。鉴于此,很多系统管理员会安装将时间与网络时钟同步的服务。网络时间协议(Network Time Protocol,NTP)是一种在整个网络中同步时间的网络协议,使用NTP的不同计算机更有可能就时间达成一致,但在本地它们依然可以设置不同的时区。

在计算中,要消除时区的影响,可参照世界标准时间(Coordinated Universal Time,UTC)。UTC是时间标准而非时区,它让不同地区的计算机有相同的参照物,而不用考虑相对时区。

让程序休眠

在计算机程序中,休眠意味着暂停执行程序。在休眠期间,程序什么都不做。但在Go语言中,如果Goroutine处于休眠状态,则程序的其他部分可继续执行。可通过休眠来等待其他任务完成或让程序暂停执行。但除非只是想让程序暂停一会儿,否则使用Goroutine来管理执行流程是更佳的选择。

time.Sleep(3 * time.Second)

设置超时时间

第12章介绍了通道,您知道了可在select语句中指定超时时间。这是使用time包在过去特定的时间后向通道发送一条消息实现的。要在特定的时间过后执行某项操作,可使用函数After。

func main() {
  fmt.Println("You have two seconds to calculate 19 * 4")
  for {
    select {
    case <-time.After(2 * time.Second):
      fmt.Println("Time's up! The answer is 74.Did you get it?")
      return
    }
  }
}
//
You have two seconds to calculate 19 * 4
Time's up! The answer is 74.Did you get it?

使用ticker

使用ticker可让代码每隔特定的时间就重复执行一次。需要在很长的时间内定期执行任务时,这么做很有用

c := time.Tick(5 * time.Second)
for t := range c {
    fmt.Printf("The time is now %v\n", t)
}

///
The time is now 2021-11-17 09:30:42.5361005 +0800 CST m=+5.002742001
The time is now 2021-11-17 09:30:47.5367309 +0800 CST m=+10.003372401
The time is now 2021-11-17 09:30:52.5363288 +0800 CST m=+15.002970301
The time is now 2021-11-17 09:30:57.5369906 +0800 CST m=+20.003632101
The time is now 2021-11-17 09:31:02.5365965 +0800 CST m=+25.003238001
The time is now 2021-11-17 09:31:07.536076 +0800 CST m=+30.002717501

使用字符串格式表示时间

不同计算机上的时间差别很多,使用字符串表示时间时,也有多种不同的方式。

类型字符串
ANSICMon Jan _2 15:04:05 2006
UnixDateMon Jan _2 15:04:05 MST 2006
RubyDateMon Jan 02 15:04:05 -0700 2006
RFC82202 Jan 06 15:04 MST
RFC822Z02 Jan 06 15:04 -0700
RFC850Monday, 02-Jan-06 15:04:05 MST
RFC1123Mon, 02 Jan 2006 15:04:05 MST
RFC1123ZMon, 02 Jan 2006 15:04:05 -0700
RFC33392006-01-02T15:04:05Z07:00
RFC3339Nano2006-01-02T15:04:05.999999999Z07:00

通常,使用上述标准将日期存储在数据库中,并以字符串的方式提供它们。Go语言支持时间标准,也支持定义不符合这些标准的时间格式。

在下方程序中,将一个使用RFC3339格式的时间字符串传递给了结构体Time,再将这个结构体打印到终端。

func main() {
  s := "2021-11-21T15:04:05+07:00"
  t, err := time.Parse(time.RFC3339, s)
  if err != nil {
    log.Fatal(err)
  }
  fmt.Println(t)
}
//
2021-11-21 15:04:05 +0700 +0700

使用结构体Time

将表示时间的字符串分析为结构体Time后,就可使用很多方法来使用它。

func main() {
  s := "2021-11-21T15:04:05+07:00"
  t, err := time.Parse(time.RFC3339, s)
  if err != nil {
    log.Fatal(err)
  }
  fmt.Printf("The hour is %v\n", t.Hour())
  fmt.Printf("The minute is %v\n", t.Minute())
  fmt.Printf("The second is %v\n", t.Second())
  fmt.Printf("The day is %v\n", t.Day())
  fmt.Printf("The month is %v\n", t.Month())
  fmt.Printf("UNIX time is %v\n", t.Unix())
  fmt.Printf("The day of the week is %v\n", t.Weekday())
}
//
The hour is 15
The minute is 4
The second is 5
The day is 21
The month is November
UNIX time is 1637481845
The day of the week is Sunday

时间加减

方法Add是要计算加上某个正负时间长度,您可将其结果赋给变量。

s := "2006-01-02T15:04:05+07:00"
t, err := time.Parse(time.RFC3339, s)
if err != nil {
  log.Fatal(err)
}
nt := t.Add(2 * time.Second)

方法Sub是要计算两个时间之间差,您也可将其结果赋给变量。

ParseDuration解析一个时间段字符串。一个时间段字符串是一个序列,每个片段包含可选的正负号、
十进制数、可选的小数部分和单位后缀,如"300ms"、"-1.5h"、"2h45m"。
合法的单位有"ns"纳秒,"us","µs"、"ms"毫秒、"s"秒、"m"分钟、"h"。
    k := time.Now()
	//定义一个负24小时
	sd, _ := time.ParseDuration("-24h")
	//定义一个正24小时
	ad, _ := time.ParseDuration("24h")
	// 负60分钟=1h
	sm, _ := time.ParseDuration("-60m")
	// 正3600秒=1h
	am, _ := time.ParseDuration("60m")
	// add是要计算加上某个正负时间长度
	fmt.Println(k.Add(ad))
	fmt.Println(k.Add(sd))
	fmt.Println(k.Add(am))
	fmt.Println(k.Add(sm))
	// sub是要计算两个时间之间差
	fmt.Println(k.Sub(k.Add(ad)))
	fmt.Println(k.Sub(k.Add(sd)))

比较两个不同的Time结构体

我们经常需要确定一个事件发生在另一个事件之前、之后还是它们是同时发生的。为让您能够这样做,time包提供了方法Before、After和Equal。这些方法都比较两个Time结构体,并返回一个布尔值。

func main() {
  s1 := "2017-09-03T18:00:00+00:00"
  s2 := "2017-09-04T18:00:00+00:00"
  today, err := time.Parse(time.RFC3339, s1)
  if err != nil {
    log.Fatal(err)
  }
  tomorrow, err := time.Parse(time.RFC3339, s2)
  if err != nil {
  log.Fatal(err)
  }
  fmt.Println(today.After(tomorrow))
  fmt.Println(today.Before(tomorrow))
  fmt.Println(today.Equal(tomorrow))
}
//
false
true
false

部署go语言

理解目标

Go的优点之一是可在众多不同的操作系统和体系结构中运行。操作系统是Windows和macOS等系统,而体系结构指的是用于运行程序的计算机处理器的体系结构。体系结构决定了处理器的计算速度以及支持的内存量。当前市面上的计算机大都是64位的,但有些环境通常是32位的。使用Go语言编程时,编写的代码只需做少量的修改甚至无须修改就可在最常见的平台中运行。

​ 如果您不知道所处的环境是什么样的,但已经安装了Go,则可使用命令go env来获悉有关操作系统和体系结构的详细信息。

​ 默认情况下,编译器将针对当前操作系统和体系结构进行编译。然而,如果要指定目标平台,可以环境变量的方式给编译器指定操作系统和体系结构。要指定操作系统,可使用环境变量GOOS;要指定体系结构,可使用环境变量GOARCH。

GOOS=linux GOARCH=386 go build example01.go

要为64位的Windows系统编译程序,可执行下面的命令。

GOOS=windows GOARCH=amd64 go build example01.go

压缩二进制文件的大小

发布Go语言代码很简单:通过编译生成一个二进制文件,其中包含运行程序所需的一切,因此您无须考虑依赖的问题。

go build example01.go
image-20211117100949499

对于这个简单程序,其生成的二进制文件为2.2MB。怎么会这样呢?因为它包含执行这个程序所需的一切,其中包括Go语言run-time

通过指定一些编译标志,可压缩编译得到的二进制文件的大小。这些标志指定省略符号表、调试信息和DWARF符号表。您无须理解这些省略的东西,只需知道发布程序时不需要它们就够了。

go build -ldflags="-s -w" example01.go
image-20211117101430885

这些设备的存储空间通常有限,在这种情况下,可使用工具upx。在Linux系统中,通常使用包管理器就能安装它。它对二进制文件进行压缩,这意味着运行时必须解压缩。虽然解压缩的速度很快,但这意味着启动时间会稍长些。通过对这个1574KB的二进制文件运行工具upx,可进一步压缩小文件的大小。

​ 使用upx压缩后,程序的大小被压缩到只有374KB。对原本为1.5MB的程序来说,这样的效果很不错!

下划线_

在 Golang 里, _ (下划线)是个特殊的标识符。

用在 import

在导包的时候,常见这个用法,尤其是项目中使用到 mysql 或者使用 pprof 做性能分析时,比如

import _ "net/http/pprof"
import _ "github.com/go-sql-driver/mysql"

这种用法,会调用包中的init()函数,让导入的包做初始化,但是却不使用包中其他功能。

【import _ 包路径】只是引用该包,仅仅是为了调用init()函数,所以无法通过包名来调用包中的其他函数。

用在返回值

该用法也是一个常见用法。Golang 中的函数返回值一般是多个,err 通常在返回值最后一个值。但是,有时候函数返回值中的某个值我们不关心,如果接收了这个值但不使用,代码编译会报错,因此需要将其忽略掉。比如

for _, val := range Slice {}
_, err := func()

用在变量

type I interface {
    Sing()
}

type T struct { 
}

func (t T) Sing() {
}
// 编译通过
var _ I = T{}

 // 编译通过
var _ I = &T{}

在这里下划线用来判断结构体是否实现了接口,如果没有实现,在编译的时候就能暴露出问题,如果没有这个判断,后代码中使用结构体没有实现的接口方法,在编译器是不会报错的

flag

Flag 包实现了命令行标志解析。

使用 flag.String(),Bool(),Int() 等定义标志。

定义命令行参数对应的变量,这变量是指针类型

servicePort = flag.String("service.port", bootstrap.HttpConfig.Port, "service port")
//service.port

// 把用户传递的命令行参数解析为对应变量的值
flag.Parse()
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值