本文是慕课网上郝林的《Go语言第一课》的学习笔记。作为一名老码农,最近才下定决心来学习新的语言,有点惭愧,也有点兴奋。 本文是课程的学习笔记,重点把GO基本语法学习中的精要点做了下总结,也是给郝林老师的一个汇报。 学习GO语言,欢迎从郝林的《Go语言第一课》开始。
1)import "fmt"最常用的一种形式
import "./test"导入同一目录下test包中的内容
2)import f "fmt"导入fmt,并给他启别名f
3)import . "fmt",将fmt启用别名".",这样就可以直接使用其内容,而不用再添加fmt,如fmt.Println可以直接写成Println
4)import _ "fmt" 表示不使用该包,而是只是使用该包的init函数,并不显示的使用该包的其他内容。注意:这种形式的import,当import时就执行了fmt包中的init函数,而不能够使用该包的其他函数。
在Go语言中,我们对程序实体的访问权限控制只能通过它们的名字来实现。名字首字母为大写的程序实体可以被任何代码包中的代码访问到。而名字首字母为小写的程序实体则只能被同一个代码包中的代码所访问。
绝大多数的数据类型的值都可以被赋给一个变量,包括函数。而常量则不同,它只能被赋予基本数据类型的值本身。
// 注释:普通赋值,由关键字var、变量名称、变量类型、特殊标记=,以及相应的值组成。
// 若只声明不赋值,则去除最后两个组成部分即可。
var num1 int = 1
var num2, num3 int = 2, 3 // 注释:平行赋值
var ( // 注释:多行赋值
num4 int = 4
num5 int = 5
)
2) 其他8个可以显式表达自身宽度的整数类型
3) 整数类型的表示范围
2)num1 = 014 // 用“0”作为前缀以表明这是8进制表示法。
3)num1 = 0xC // 用“0x”作为前缀以表明这是16进制表示法。
1) 浮点数类型有两个,即float32和float64。存储这两个类型的值的空间分别需要4个字节和8个字节。
2)3.7E-2表示浮点数0.037。又比如,3.7E+1表示浮点数37。在Go语言里,浮点数的相关部分只能由10进制表示法表示,而不能由8进制表示法或16进制表示法表示。
package main
import "fmt"
import "os"
type point struct {
x, y int
}
func main() {
//Go 为常规 Go 值的格式化设计提供了多种打印方式。例如,这里打印了 point 结构体的一个实例。
p := point{1, 2}
fmt.Printf("%v\n", p) // {1 2}
//如果值是一个结构体,%+v 的格式化输出内容将包括结构体的字段名。
fmt.Printf("%+v\n", p) // {x:1 y:2}
//%#v 形式则输出这个值的 Go 语法表示。例如,值的运行源代码片段。
fmt.Printf("%#v\n", p) // main.point{x:1, y:2}
//需要打印值的类型,使用 %T。
fmt.Printf("%T\n", p) // main.point
//格式化布尔值是简单的。
fmt.Printf("%t\n", true)
//格式化整形数有多种方式,使用 %d进行标准的十进制格式化。
fmt.Printf("%d\n", 123)
//这个输出二进制表示形式。
fmt.Printf("%b\n", 14)
//这个输出给定整数的对应字符。
fmt.Printf("%c\n", 33)
//%x 提供十六进制编码。
fmt.Printf("%x\n", 456)
//对于浮点型同样有很多的格式化选项。使用 %f 进行最基本的十进制格式化。
fmt.Printf("%f\n", 78.9)
//%e 和 %E 将浮点型格式化为(稍微有一点不同的)科学技科学记数法表示形式。
fmt.Printf("%e\n", 123400000.0)
fmt.Printf("%E\n", 123400000.0)
//使用 %s 进行基本的字符串输出。
fmt.Printf("%s\n", "\"string\"")
//像 Go 源代码中那样带有双引号的输出,使用 %q。
fmt.Printf("%q\n", "\"string\"")
//和上面的整形数一样,%x 输出使用 base-16 编码的字符串,每个字节使用 2 个字符表示。
fmt.Printf("%x\n", "hex this")
//要输出一个指针的值,使用 %p。
fmt.Printf("%p\n", &p)
//当输出数字的时候,你将经常想要控制输出结果的宽度和精度,可以使用在 % 后面使用数字来控制输出宽度。默认结果使用右对齐并且通过空格来填充空白部分。
fmt.Printf("|%6d|%6d|\n", 12, 345)
//你也可以指定浮点型的输出宽度,同时也可以通过 宽度.精度 的语法来指定输出的精度。
fmt.Printf("|%6.2f|%6.2f|\n", 1.2, 3.45)
//要最对齐,使用 - 标志。
fmt.Printf("|%-6.2f|%-6.2f|\n", 1.2, 3.45)
//你也许也想控制字符串输出时的宽度,特别是要确保他们在类表格输出时的对齐。这是基本的右对齐宽度表示。
fmt.Printf("|%6s|%6s|\n", "foo", "b")
//要左对齐,和数字一样,使用 - 标志。
fmt.Printf("|%-6s|%-6s|\n", "foo", "b")
//到目前为止,我们已经看过 Printf了,它通过 os.Stdout输出格式化的字符串。Sprintf 则格式化并返回一个字符串而不带任何输出。
s := fmt.Sprintf("a %s", "string")
fmt.Println(s)
//你可以使用 Fprintf 来格式化并输出到 io.Writers而不是 os.Stdout。
fmt.Fprintf(os.Stderr, "an %s\n", "error")
}
复数类型同样有两个,即complex64和complex128。存储这两个类型的值的空间分别需要8个字节和16个字节。实际上,complex64类型的值会由两个float32类型的值分别表示复数的实数部分和虚数部分。而complex128类型的值会由两个float64类型的值分别表示复数的实数部分和虚数部分。
例如:var num3 = 3.7E+1 + 5.98E-2i
byte是uint8的别名类型。
rune则是int32的别名类型。
一个rune类型的值即可表示一个Unicode字符。一个Unicode代码点通常由“U+”和一个以十六进制表示法表示的整数表示。例如,英文字母“A”的Unicode代码点为“U+0041”。
字符串的表示法有两种,即:原生表示法和解释型表示法。若用原生表示法,需用反引号“`”把字符序列包裹起来。若用解释型表示法,则需用双引号“"”包裹字符序列。
二者的区别是,前者表示的值是所见即所得的(除了回车符)。在那对反引号之间的内容就是该字符串值本身。而后者所表示的值中的转义符会起作用并在程序编译期间被转义。
例如: "\""所代表的字符串值是"
一个数组(Array)就是一个可以容纳若干类型相同的元素的容器。
1) type MyNumbers [3]int
2)var numbers = [3]int{1, 2, 3}
3)var numbers = [...]int{1, 2, 3}
4)var length = len(numbers)
5) 如果我们只声明一个数组类型的变量而不为它赋值,那么该变量的值将会是指定长度的、其中各元素均为元素类型的零值(或称默认值)的数组值。
1) 切片(Slice)与数组一样,也是可以容纳若干类型相同的元素的容器。与数组不同的是,无法通过切片类型来确定其值的长度。
2)
var numbers3 = [5]int{1, 2, 3, 4, 5}
var slice1 = numbers3[1:4]
var slice2 = slice1[1:3]
一个切片值的容量即为它的第一个元素值在其底层数组中的索引值与该数组长度的差值的绝对值。
对底层数组容量是 k 的切片 slice2[i:j]来说
长度: len(slice2) = j - i = 3 -1 = 2
容量: cap(slice2) = k - i = 4 - 1 =3
3)切片类型属于引用类型。它的零值即为nil,即空值。
4) make 和切片字面量
// 创建一个字符串切片, 其长度和容量都是 5 个元素
slice := make([]string, 5)
或者
type slice []string
5)// 创建一个整型切片, 其长度为 3 个元素,容量为 5 个元素
slice := make([]int, 3, 5)
Go 语言内置的 append函数会处理增加长度时的所有操作细节。
// 创建一个整型切片, 其长度和容量都是 5 个元素
slice := []int{10, 20, 30, 40, 50}
// 创建一个新切片,其长度为 2 个元素,容量为 4 个元素
newSlice := slice[1:3]
// 使用原有的容量来分配一个新元素, 将新元素赋值为 60
newSlice = append(newSlice, 60)
因为 newSlice 在底层数组里还有额外的容量可用, append 操作将可用的元素合并到切片的长度,并对其进行赋值。
当这个 append 操作完成后, newSlice 拥有一个全新的底层数组,这个数组的容量是原来
的两倍。
// 创建一个整型切片, 其长度和容量都是 4 个元素
slice := []int{10, 20, 30, 40}// 向切片追加一个新元素
// 将新元素赋值为 50
newSlice := append(slice, 50)
函数 append 会智能地处理底层数组的容量增长。在切片的容量小于 1000 个元素时,总是会成倍地增加容量。一旦元素个数超过 1000,容量的增长因子会设为 1.25,也就是会每次增加 25%的容量。
8)切片复制
复制操作的实施方法是调用copy函数。该函数接受两个类型相同的切片值作为参数,并会把第二个参数值中的元素复制到第一个参数值中的相应位置(索引值相同)上。被复制的元素的个数总是等于长度较短的那个参数值的长度。
举例如下:
var slice4 = []int{0, 0, 0, 0, 0, 0, 0}
copy(slice4, slice1)
通过上述复制操作,slice4会变为[]int{2, 3, 4, 6, 7, 0, 0}。
9) 容量上界索引
numbers3[1:4:4] ,这第三个正整数被称为容量上界索引。它的意义在于可以把作为结果的切片值的容量设置得更小。
var numbers3 = [5]int{1, 2, 3, 4, 5}
var slice1 = numbers3[1:4]
slice1 = slice1[:cap(slice1)]
通过此操作,变量slice1的值变为了[]int{2, 3, 4, 5},且其长度和容量均为4。
其中,“K”意为键的类型,而“T”则代表元素(或称值)的类型。如果我们要描述一个键类型为int、值类型为string的字典类型的话,应该这样写:map[int]string
map[int]string{1: "a", 2: "b", 3: "c"}
3)变量赋值
mm := map[int]string{1: "a", 2: "b", 3: "c"}
4)探测键值是否存在的复制方法
e, ok := mm[5]
对字典的索引表达式可以有两个求值结果。第二个求值结果是bool类型的。它用于表明字典值中是否存在指定的键值对。在上例中,变量ok必为false。因为mm中不存在以5为键的键值对。
5)删除键值对的方法
delete(mm, 4)
无论mm中是否存在以4为键的键值对,delete都会“无声”地执行完毕。我们用“有则删除,无则不做”可以很好地概括它的行为。
1) 通道定义 : make函数可接受两个参数。第一个参数是代表了将被初始化的值的类型的字面量(比如chan int),而第二个参数则是值的长度。
T := make(chan int, 5)
2)使用接收操作符<-向通道值发送数据了。
ch1 <- "value1"
3)向通道接收数据
value := <- ch1
value, ok := <- ch1
4)关闭通道值
close(ch1)
5) 通道分为缓冲通道和非缓冲通道。
发送方在向通道值发送数据的时候会立即被阻塞,直到有某一个接收方已从该通道值中接收了这条数据。
make(chan int, 0)
6)通道分为单向通道(只接收通道,只发送通道),双向通道
//只接收,
type Receiver <-chan int
//只发送
type Sender chan<- int
单向通道可以复制给双向通道,双向通道不能赋值给单向通道。
var myChannel = make(chan int, 3)
var sender Sender = myChannel
var receiver Receiver = myChannel
1)Go 语言函数定义格式如下:
func function_name( [parameter list] ) [return_types] {
函数体
}
2)函数定义的几种方法
func(input1 string ,input2 string) string
如果我们在它的左边加入type关键字和一个标识符作为名称的话,那就变成了一个函数类型声明
type MyFunc func(input1 string ,input2 string) string
func myFunc(part1 string, part2 string) (result string) {
result = part1 + part2
return
}
如果结果声明是带名称的,那么它就相当于一个已被声明但未被显式赋值的变量。我们可以为它赋值且在return语句中省略掉需要返回的结果值。
func myFunc(part1 string, part2 string) string {
return part1 + part2
}
注意,函数myFunc是函数类型MyFunc的一个实现。
var result = func(part1 string, part2 string) string {
return part1 + part2
}("1", "2")
函数类型的零值是nil。
1)结构体类型的字面量由关键字type、类型名称、关键字struct,以及由花括号包裹的若干字段声明组成。
type Person struct {
Name string
Gender string
Age uint8
}
2)赋值结果:
Person{Name: "Robert", Gender: "Male", Age: 33}
或者
Person{"Robert", "Male", 33}
3)匿名结构体:
p := struct {
Name string
Gender string
Age uint8
}{"Robert", "Male", 33}
4)结构体类型可以拥有若干方法(注意,匿名结构体是不可能拥有方法的)。所谓方法,其实就是一种特殊的函数。它可以依附于某个自定义类型。
func (person *Person) Move(target string) string {
oldAddress := person.Address
person.Address = target
return oldAddress
}
在关键字func和名称Grow之间的那个圆括号及其包含的内容就是接收者声明(person *Person) 。
1)一个接口类型的声明通常会包含关键字type、类型名称、关键字interface以及由花括号包裹的若干方法声明。示例如下:
type Animal interface {
Grow()
Move(string) string
}
注意,接口类型中的方法声明是普通的方法声明的简化形式。它们只包括方法名称、参数声明列表和结果声明列表。
2)所谓实现一个接口中的方法是指,具有与该方法相同的声明并且添加了实现部分(由花括号包裹的若干条语句)。
我们无需在一个数据类型中声明它实现了哪个接口。只要满足了“方法集合为其超集”的条件,就建立了“实现”关系。这是典型的无侵入式的接口实现方法。
3)可以使用空接口断言一个变量是否实现了接口定义。
p := Person{"Robert", "Male", 33, "Beijing"}
v := interface{}(&p)
之后,我们就可以在v上应用类型断言了
h, ok := v.(Animal)
指针操作涉及到两个操作符——&和*。
当地址操作符&被应用到一个值上时会取出指向该值的指针值,而当地址操作符*被应用到一个指针值上时会取出该指针指向的那个值。
type Person struct {
Name string
Gender string
Age uint8
Address string
}
//Person类型的指针方法,会导致调用变量值发生改变。这儿不会如c一样用->,而是还是用.表示引用的。
func (person *Person) Grow() {
person.Age++
}
//Person类型的值方法,不会导致调用变量值发生改变。
func (person Person) Grow() {
person.Age++
}
一个指针类型拥有以它以及以它的基底类型为接收者类型的所有方法,而它的基底类型却只拥有以它本身为接收者类型的方法。
var number int
if number := 4; 100 > number {
number += 3
} else if 100 < number {
number -= 2
} else {
fmt.Println("OK!")
}
标识符的重声明:只要对同一个标识符的两次声明各自所在的代码块之间存在包含的关系,就会形成对该标识符的重声明。
所例中,if语句内部对number的访问和赋值都只会涉及到第二次声明的那个number变量,这种现象也被叫做标识符的遮蔽。
上述代码被执行完毕之后,第二次声明的number变量的值会是7,而第一次声明的number变量的值仍会是0。
names := []string{"Golang", "Java", "Rust", "C"}
switch name := names[0]; name {
case "Golang":
fmt.Println("A programming language from Google.")
case "Rust":
fmt.Println("A programming language from Mozilla.")
default:
fmt.Println("Unknown!")
}
fallthrough。它既是一个关键字,又可以代表一条语句。
fallthrough语句可被包含在表达式switch语句中的case语句中。它的作用是使控制权流转到下一个case。
不过要注意,fallthrough语句仅能作为case语句中的最后一条语句出现。并且,包含它的case语句不能是其所属switch语句的最后一条case语句。
1) 格式
for i := 0; i < 10; i++ {
fmt.Print(i, " ")
}
可以省略掉初始化子句、条件表达式、后置子句中的任何一个或多个,不过起到分隔作用的分号一般需要被保留下来
2) range
for i, v := range "Go语言" {
fmt.Printf("%d: %c\n", i, v)
}
range表达式的结果值的类型应该是能够被迭代的,包括:字符串类型、数组类型、数组的指针类型、切片类型、字典类型和通道类型。
break语句和continue语句。
它们都可以被放置在for语句的代码块中。前者被执行时会使其所属的for语句的执行立即结束,而后者被执行时会使当次迭代被中止(当次迭代的后续语句会被忽略)而直接进入到下一次迭代。
1)select语句中的case关键字只能后跟用于通道的发送操作的表达式以及接收操作的表达式或语句。
1、如果多个通道都阻塞了,会等待知道其中一个通道可以处理。
2、如果多个通道都可以处理,随机选取一个处理。
3、如果没有通道操作可以操作并且写了default语句,会执行:default(永远是可以运行的)
4、如果防止select堵塞,可以写default来确保发送不被堵塞,没有case的select就会一直堵塞。
5、当select做选择case和default操作时,case的优先级大于default。
6、select语句实现了一种监听模式,通常在无限循环中使用,通过在某种情况下,通过break退出循环。
2)如果该select语句被执行时通道ch1和ch2中都没有任何数据,那么肯定只有default case会被执行。
ch1 := make(chan int, 1)
ch2 := make(chan int, 1)
// 省略若干条语句
select {
case e1 := <-ch1:
fmt.Printf("1th case is selected. e1=%v.\n", e1)
case e2 := <-ch2:
fmt.Printf("2th case is selected. e2=%v.\n", e2)
default:
fmt.Println("No data!")
}
3)如果一条select语句中不存在default case, 并且在被执行时其中的所有case都不满足执行条件,那么它的执行将会被阻塞!
break语句也可以被包含在select语句中的case语句中。它的作用是立即结束当前的select语句的执行,不论其所属的case语句中是否还有未被执行的语句。
4)
package main
import "fmt"
func main() {
ch4 := make(chan int, 1)
for i := 0; i < 4; i++ {
select {
case e, ok := <-ch4:
if !ok {
fmt.Println("End.")
return
}
fmt.Println(e)
close(ch4)
default:
fmt.Println("No Data!")
ch4 <- 1
}
}
}
会打印:
No Data!
1
End.
假设有调用函数A、被调用函数B,其关系如下:
func A(){//调用函数
...
defer B()//被调用函数
...
return//B将延迟到return前执行
}
*defer是延迟执行关键字,将使B延迟到A return前执行。
*可在A中添加多个defer,在函数将要return时,这些defer语句按逆序执行。
*defer通常用于释放资源。
异常处理——error
errors.New用于创建一个错误。
os.ErrPermission、io.EOF是常见的错误值。
panic可被意译为运行时恐慌。因为它只有在程序运行的时候才会被“抛出来”。并且,恐慌是会被扩散的。
在Go语言中,内建函数recover可以恢复恐慌panic。recover函数必须要在defer语句中调用才有效。
defer func() {
if p := recover(); p != nil {
fmt.Printf("Fatal error: %s\n", p)
}
}()