golang - yaokun123/php-wiki GitHub Wiki

一、变量

在 Go 中,有多种语法用于声明变量

// 1、声明单个变量
var name type           // 如果变量未被赋值,Go 会自动地将其初始化,赋值该变量类型的零值

// 2、声明变量并初始化
var name type = initialvalue 

// 3、类型推断
var name = initialvalue
如果变量有初始值,那么 Go 能够自动推断具有初始值的变量的类型。因此,如果变量有初始值,就可以在变量声明中省略 type。

// 4、声明多个变量
var name1, name2 type = initialvalue1, initialvalue2

var (  
    name1 = initialvalue1,
    name2 = initialvalue2
)

// 5、简短声明
name := initialvalue
// 注意:简短声明要求 := 操作符左边的所有变量都有初始值
// 注意:简短声明的语法要求 := 操作符的左边至少有一个变量是尚未声明的

二、类型

在 Printf 方法中,使用 %T 格式说明符(Format Specifier),可以打印出变量的类型。

Go 的 [unsafe]包提供了一个 [Sizeof]函数,该函数接收变量并返回它的字节大小。unsafe 包应该小心使用,因为使用 unsafe 包可能会带来可移植性问题。

Go 有着非常严格的强类型特征。Go 没有自动类型提升或类型转换。

// 1、bool
    - true|false

// 2、数字类型
    - int8, int16, int32, int64, int
    - uint8, uint16, uint32, uint64, uint
    - float32, float64
    - complex64, complex128
    - byte // 是 uint8 的别名
    - rune // 是 int32 的别名

// 3、string

// 4、类型转换
T(v)         // int(i)、rune[]()、string()....

三、常量

关键字 const 被用于表示常量

常量的值会在编译的时候确定。因为函数调用发生在运行时,所以不能将函数的返回值赋值给常量。

四、函数

// 1、函数的声明
func functionname(parametername type) returntype {  
    // 函数体(具体实现的功能)
    // 函数中的参数列表和返回值并非是必须的
}

// 2、多返回值
Go 语言支持一个函数可以有多个返回值。

// 3、命名返回值
从函数中可以返回一个命名值。一旦命名了返回值,可以认为这些值在函数第一行就被声明为变量了

// 4、空白符
_ 在 Go 中被用作空白符,可以用作表示任何类型的任何值。

// 5、可变参数函数
可变参数函数是一种参数个数可变的函数
如果函数最后一个参数被记作 ...T ,这时函数可以接受任意个 T 类型参数作为最后一个参数。
请注意只有函数的最后一个参数才允许是可变的。
可变参数函数的工作原理是把可变参数转换为一个新的切片。

// 6、给可变参数函数传入切片
有一个可以直接将切片传入可变参数函数的语法糖,你可以在在切片后加上 ... 后缀。如果这样做,切片将直接传入函数,不再创建新的切片

五、包

包用于组织 Go 源代码,提供了更好的可重用性与可读性。

// 1、main 函数和 main 包
所有可执行的 Go 程序都必须包含一个 main 函数。这个函数是程序运行的入口。main 函数应该放置于 main 包中。
package packagename 这行代码指定了某一源文件属于一个包。它应该放在每一个源文件的第一行。

// 2、创建自定义的包
属于某一个包的源文件都应该放置于一个单独命名的文件夹里。按照 Go 的惯例,应该用包名命名该文件夹。

// 3、导入自定义包
导入自定义包的语法为 import path。
我们必须指定自定义包相对于工作区内 src 文件夹的相对路径。

// 4、导出名字
在 Go 中,任何以大写字母开头的变量或者函数都是被导出的名字。其它包只能访问被导出的函数和变量。

// 5、init 函数
所有包都可以包含一个 init 函数。init 函数不应该有任何返回值类型和参数,在我们的代码中也不能显式地调用它。
init 函数可用于执行初始化任务,也可用于在开始执行之前验证程序的正确性。

// 6、包的初始化顺序如下:
    - 首先初始化包级别(Package Level)的变量
    - 紧接着调用 init 函数。包可以有多个 init 函数(在一个文件或分布于多个文件中),它们按照编译器解析它们的顺序进行调用。
如果一个包导入了另一个包,会先初始化被导入的包。
尽管一个包可能会被导入多次,但是它只会被初始化一次。

// 7、使用空白标识符
导入了包,却不在代码中使用它,这在 Go 中是非法的。当这么做时,编译器是会报错的。其原因是为了避免导入过多未使用的包,从而导致编译时间显著增加。
然而,在程序开发的活跃阶段,又常常会先导入包,而暂不使用它。遇到这种情况就可以使用空白标识符 _。

六、分支

// else 语句应该在 if 语句的大括号 } 之后的同一行中。如果不是,编译器会不通过。
if condition {  
} else if condition {
} else {
}

七、循环

for 是 Go 语言唯一的循环语句。Go 语言中并没有其他语言比如 C 语言中的 while 和 do while 循环。

// 1、for 循环语法
for initialisation; condition; post {  
}

// 2、break
break 语句用于在完成正常执行之前突然终止 for 循环,之后程序将会在 for 循环下一行代码开始执行。

// 3、continue
continue 语句用来跳出 for 循环中当前循环。在 continue 语句后的所有的 for 循环语句都不会在本次循环中执行。循环体会在一下次循环中继续执行。

// 4、无限循环
for {  
}

八、switch 语句

// 1、在选项列表中,case 不允许出现重复项。

// 2、多表达式判断
    - 通过用逗号分隔,可以在一个 case 中包含多个表达式。

// 3、无表达式的 switch
    - 在 switch 语句中,表达式是可选的,可以被省略。如果省略表达式,则表示这个 switch 语句等同于 switch true
    - 并且每个 case 表达式都被认定为有效,相应的代码块也会被执行。

// 4、Fallthrough 语句
    - 在 Go 中,每执行完一个 case 后,会从 switch 语句中跳出来,不再做后续 case 的判断和执行。
    - 使用 fallthrough 语句可以在已经执行完成的 case 之后,把控制权转移到下一个 case 的执行代码中。

九、数组

数组不能调整大小

// 1、数组的声明
一个数组的表示形式为 [n]T。n 表示数组中元素的数量,T 代表每个元素的类型。元素的数量 n 也是该类型的一部分

// 2、数组的简略声明
a := [3]int{12, 78, 50}
a := [...]int{12, 78, 50}  // 你甚至可以忽略声明数组的长度,并用 ... 代替,让编译器为你自动计算长度

// 3、数组是值类型
Go 中的数组是值类型而不是引用类型。这意味着当数组赋值给一个新的变量时,该变量会得到一个原始数组的一个副本。如果对新变量进行更改,则不会影响原始数组。

// 4、数组的长度
通过将数组作为参数传递给 len 函数,可以得到数组的长度。

// 5、数组的遍历
for、range
for i, v := range a {
}

// 6、多维数组

十、切片

// 1、创建一个切片
带有 T 类型元素的切片由 []T 表示
    - 使用语法 a[start:end] 创建一个从 a 数组索引 start 开始到 end - 1 结束的切片。
    - c := []int{6, 7, 8}
    - make([]int, len, cap)

// 2、切片的修改
切片自己不拥有任何数据。它只是底层数组的一种表示。对切片所做的任何修改都会反映在底层数组中。

// 3、切片的长度和容量
切片的长度是切片中的元素数。切片的容量是从创建切片索引开始的底层数组中元素数。

// 4、追加切片元素
切片是动态的,使用 append 可以将新元素追加到切片上。append 函数的定义是 func append(s[]T,x ... T)[]T。

有一个问题可能会困扰你。如果切片由数组支持,并且数组本身的长度是固定的,那么切片如何具有动态长度。以及内部发生了什么?
当新的元素被添加到切片时,会创建一个新的数组。现有数组的元素被复制到这个新数组中,并返回这个新数组的新切片引用。现在新切片的容量是旧切片的两倍。

// 5、切片的函数传递
我们可以认为,切片在内部可由一个结构体类型表示。这是它的表现形式
type slice struct {  
    Length        int
    Capacity      int
    ZerothElement *byte
}
切片包含长度、容量和指向数组第零个元素的指针。当切片传递给函数时,即使它通过值传递,指针变量也将引用相同的底层数组。
因此,当切片作为参数传递给函数时,函数内所做的更改也会在函数外可见

十一、Map

// 1、如何创建 map ?
make(map[type of key]type of value)
map 的零值是 nil。如果你想添加元素到 nil map 中,会触发运行时 panic。因此 map 必须使用 make 函数初始化。

// 2、给 map 添加元素
- 你也可以在声明的时候初始化 map
personSalary := map[string]int {
        "steve": 12000,
        "jamie": 15000,
}

// 3、获取 map 中的元素
获取 map 元素的语法是 map[key]
如果我们想知道 map 中到底是不是存在这个 key使用value, ok := map[key]
如果 ok 是 true,表示 key 存在,key 对应的值就是 value ,反之表示 key 不存在。

// 4、删除 map 中的元素
删除 map 中 key 的语法是 delete(map, key)。这个函数没有返回值。

// 5、获取 map 的长度
获取 map 的长度使用 [len](https://golang.org/pkg/builtin/#len) 函数。

// 6、Map 是引用类型
和slices类似,map 也是引用类型。当 map 被赋值为一个新变量的时候,它们指向同一个内部数据结构。因此,改变其中一个变量,就会影响到另一变量。

// 7、Map 的相等性
map 之间不能使用 == 操作符判断,== 只能用来检查 map 是否为 nil。

十二、字符串

由于和其他语言相比,字符串在 Go 语言中有着自己特殊的实现,因此在这里需要被特别提出来。

// 1、什么是字符串?
    - Go 语言中的字符串是一个字节切片。把内容放在双引号""之间,我们可以创建一个字符串。
    - Go 中的字符串是兼容 Unicode 编码的,并且使用 UTF-8 进行编码

// 2、单独获取字符串的每一个字节
    - 由于字符串是一个字节切片,所以我们可以获取字符串的每一个字节。

// 3、rune
    - rune 是 Go 语言的内建类型,它也是 int32 的别称。在 Go 语言中,rune 表示一个代码点。代码点无论占用多少个字节,都可以用一个 rune 来表示。

// 4、字符串的 for range 循环
s := "hhhhhh"
for index, rune := range s {}

// 5、用字节切片构造字符串
byteSlice := []byte{0x43, 0x61, 0x66, 0xC3, 0xA9}
str := string(byteSlice)

/// 6、用 rune 切片构造字符串
runeSlice := []rune{0x0053, 0x0065, 0x00f1, 0x006f, 0x0072}
str := string(runeSlice)

// 7、字符串的长度
utf8 包中的 func RuneCountInString(s string) (n int) 方法用来获取字符串的长度。这个方法传入一个字符串参数然后返回字符串中的 rune 的数量。

// 8、字符串是不可变的
Go 中的字符串是不可变的。一旦一个字符串被创建,那么它将无法被修改。
为了修改字符串,可以把字符串转化为一个 rune 切片。然后这个切片可以进行任何想要的改变,然后再转化为一个字符串。

十三、指针

// 1、什么是指针?
    - 指针是一种存储变量内存地址(Memory Address)的变量。

// 2、指针的声明
    - 指针变量的类型为 *T,该指针指向一个 T 类型的变量。

// 3、指针的零值
    - 指针的零值是 nil

// 4、指针的解引用
    - 针的解引用可以获取指针所指向的变量的值。将 a 解引用的语法是 *a。

// 5、向函数传递指针参数
    - 

// 6、不要向函数传递数组的指针,而应该使用切片

// 7、Go 不支持指针运算
    - Go 并不支持其他语言(例如 C)中的指针运算。可以使用unsafe包越过这层限制

十四、结构体

// 1、什么是结构体?
    - 结构体是用户定义的类型,表示若干个字段(Field)的集合。

// 2、结构体的声明
type Employee struct {
    firstName string
    lastName  string
    age       int
}
Employee 称为 命名的结构体。我们创建了名为 Employee 的新类型,而它可以用于创建 Employee 类型的结构体变量。
    - 声明结构体时也可以不用声明一个新类型,这样的结构体类型称为 匿名结构体
emp3 := struct {
        firstName, lastName string
        age, salary         int
    }{
        firstName: "Andreah",
        lastName:  "Nikola",
        age:       31,
        salary:    5000,
    }

// 3、结构体的零值
    - 当定义好的结构体并没有被显式地初始化时,该结构体的字段将默认赋为零值。

// 4、访问结构体的字段
    - 点号操作符 . 用于访问结构体的字段。

// 5、结构体的指针
    - emp8 := &Employee{"Sam", "Anderson", 55, 6000}
    - emp8 是一个指向结构体 Employee 的指针。
    - Go 语言允许我们在访问 firstName 字段时,可以使用 emp8.firstName 来代替显式的解引用 (*emp8).firstName

// 6、匿名字段
    - 当我们创建结构体时,字段可以只有类型,而没有字段名。这样的字段称为匿名字段
    - 虽然匿名字段没有名称,但其实匿名字段的名称就默认为它的类型

// 7、嵌套结构体
    - 结构体的字段有可能也是一个结构体。这样的结构体称为嵌套结构体。

// 8、提升字段
    - 如果是结构体中有匿名的结构体类型字段,则该匿名结构体里的字段就称为提升字段。这是因为提升字段就像是属于外部结构体一样,可以用外部结构体直接访问。

// 9、导出结构体和字段
    - 如果结构体名称以大写字母开头,则它是其他包可以访问的导出类型(Exported Type)。同样,如果结构体里的字段首字母大写,它也能被其他包访问到。

// 10、结构体相等性
    - 结构体是值类型。如果它的每一个字段都是可比较的,则该结构体也是可比较的。如果两个结构体变量的对应字段相等,则这两个变量也是相等的。

十五、方法

// 1、什么是方法?
    - 方法其实就是一个函数,在 func 这个关键字和方法名中间加入了一个特殊的接收器类型。接收器可以是结构体类型或者是非结构体类型。接收器是可以在方法的内部访问的。
    - func (t Type) methodName(parameter list) {}

// 2、指针接收器与值接收器
    - 值接收器和指针接收器之间的区别在于,在指针接收器的方法内部的改变对于调用者是可见的,然而值接收器的情况不是这样的。
    - 当一个方法有一个值接收器,它可以接受值接收器和指针接收器。
    - 当一个方法有一个指针接收器,它可以接受值接收器和指针接收器。

// 3、匿名字段的方法
    - 属于结构体的匿名字段的方法可以被直接调用,就好像这些方法是属于定义了匿名字段的结构体一样。

十六、接口

// 1、接口的声明与实现
    - 在 Go 中,如果一个类型包含了接口中声明的所有方法,那么它就隐式地实现了 Go 接口。
type VowelsFinder interface {  
    FindVowels() []rune
}

// 2、空接口
    - 没有包含方法的接口称为空接口。空接口表示为 interface{}。由于空接口没有方法,因此所有类型都实现了空接口。

// 3、类型断言
    - 在语法 v, ok := i.(T) 中,接口 i 的具体类型是 T,该语法用于获得接口的底层值。

// 4、类型选择
    - i.(type)

// 5、指针接受者与值接受者
    - 对于使用指针接受者的方法,用一个指针或者一个可取得地址的值来调用都是合法的。
    - 但接口中存储的具体值并不能取到地址,对于编译器无法自动获取地址,于是程序报错。

// 6、实现多个接口

// 7、接口的嵌套
    - 尽管 Go 语言没有提供继承机制,但可以通过嵌套其他的接口,创建一个新接口。

// 8、接口的零值
    - 接口的零值是 nil。

十七、协程

// 1、Go 协程是什么
    - Go 协程是与其他函数或方法一起并发运行的函数或方法。Go 协程可以看作是轻量级线程。
    - 与线程相比,创建一个 Go 协程的成本很小。因此在 Go 应用中,常常会看到有数以千计的 Go 协程并发地运行。

// 2、Go 协程相比于线程的优势
    - 相比线程而言,Go 协程的成本极低。堆栈大小只有若干 kb,并且可以根据应用的需求进行增减。而线程必须指定堆栈的大小,其堆栈是固定不变的。
    - Go 协程会复用数量更少的 OS 线程。即使程序有数以千计的 Go 协程,也可能只有一个线程。
    - 如果该线程中的某一 Go 协程发生了阻塞(比如说等待用户输入),那么系统会再创建一个 OS 线程,并把其余 Go 协程都移动到这个新的 OS 线程。
    - 所有这一切都在运行时进行,作为程序员,我们没有直接面临这些复杂的细节,而是有一个简洁的 API 来处理并发。
    - Go 协程使用信道(Channel)来进行通信。
    - 信道用于防止多个协程访问共享内存时发生竞态条件。信道可以看作是 Go 协程之间通信的管道。

// 3、如何启动一个 Go 协程?
    - 调用函数或者方法时,在前面加上关键字 go,可以让一个新的 Go 协程并发地运行。
    - 启动一个新的协程时,协程的调用会立即返回。与函数不同,程序控制不会去等待 Go 协程执行完毕。在调用 Go 协程之后,程序控制会立即返回到代码的下一行,忽略该协程的任何返回值。
    - 如果希望运行其他 Go 协程,Go 主协程必须继续运行着。如果 Go 主协程终止,则程序终止,于是其他 Go 协程也不会继续运行。

十八、channel

操作 nil的channel 正常channel 已关闭的channel
阻塞 成功或阻塞 读到零值
阻塞 成功或阻塞 panic
关闭 panic 成功 panic
// 1、什么是信道?
    - 信道可以想像成 Go 协程之间通信的管道。如同管道中的水会从一端流到另一端,通过使用信道,数据也可以从一端发送,在另一端接收。

// 2、信道的声明
    - 所有信道都关联了一个类型。信道只能运输这种类型的数据,而运输其他类型的数据都是非法的。
    - chan T 表示 T 类型的信道。
    - 信道的零值为 nil。信道的零值没有什么用,应该像对 map 和切片所做的那样,用 make 来定义信道。
    - a := make(chan int)

// 3、通过信道进行发送和接收
    - data := <- a // 读取信道 a  
    - a <- data    // 写入信道 a

// 4、发送与接收默认是阻塞的
    - 当把数据发送到信道时,程序控制会在发送数据的语句处发生阻塞,直到有其它 Go 协程从信道读取到数据,才会解除阻塞。
    - 与此类似,当读取信道的数据时,如果没有其它的协程把数据写入到这个信道,那么读取过程就会一直阻塞着。
    - 信道的这种特性能够帮助 Go 协程之间进行高效的通信,不需要用到其他编程语言常见的显式锁或条件变量。

// 5、死锁
    - 当 Go 协程给一个信道发送数据时,照理说会有其他 Go 协程来接收数据。如果没有的话,程序就会在运行时触发 panic,形成死锁。
    - 同理,当有 Go 协程等着从一个信道接收数据时,我们期望其他的 Go 协程会向该信道写入数据,要不然程序就会触发 panic。

// 6、单向信道

// 7、关闭信道
    - 数据发送方可以关闭信道,通知接收方这个信道不再有数据发送过来。
    - 当从信道接收数据时,接收方可以多用一个变量来检查信道是否已经关闭。v, ok := <- ch
    - 从关闭的信道读取到的值会是该信道类型的零值

// 8、使用 for range 遍历信道
    - for range 循环用于在一个信道关闭之前,从信道接收数据。for v := range ch {}
    - for range 循环从信道 ch 接收数据,直到该信道关闭。一旦关闭了 ch,循环会自动结束。

// 9、缓冲信道
    - 只在缓冲已满的情况,才会阻塞向缓冲信道发送数据。同样,只有在缓冲为空的时候,才会阻塞从缓冲信道接收数据
    - ch := make(chan type, capacity)

// 10、长度 vs 容量
    - len() vs cap()

// 11、WaitGroup
    - WaitGroup 用于等待一批 Go 协程执行结束。程序控制会一直阻塞,直到这些协程全部执行完毕。
    - var wg sync.WaitGroup
    - wg.Add(1)
    - wg.Done()
    - wg.Wait()

// 12、select
    - select 语句用于在多个发送/接收信道操作中进行选择。select 语句会一直阻塞,直到发送/接收操作准备就绪。
    - 如果有多个信道操作准备完毕,select 会随机地选取其中之一执行。

十九、Mutex

Mutex 用于提供一种加锁机制,可确保在某时刻只有一个协程在临界区运行,以防止出现竞态条件。

    - Mutex 可以在 sync包内找到。
    - [Mutex]定义了两个方法:[Lock] 和 [Unlock]。所有在 Lock 和 Unlock 之间的代码,都只能由一个 Go 协程执行。

二十、defer

defer 语句的用途是:含有 defer 语句的函数,会在该函数将要返回之前,调用另一个函数。

// 1、实参取值
    - 在 Go 语言中,并非在调用延迟函数的时候才确定实参,而是当执行 defer 语句的时候,就会对延迟函数的实参进行求值。

// 2、defer 栈
    - 当一个函数内多次调用 defer 时,Go 会把 defer 调用放入到一个栈中,随后按照后进先出(Last In First Out, LIFO)的顺序执行。

二十一、错误处理

// 1、什么是错误
    - 错误表示程序中出现了异常情况。比如当我们试图打开一个文件时,文件系统里却并没有这个文件。这就是异常情况,它用一个错误来表示。
    - 错误用内建的 error 类型来表示

// 2、错误类型的表示
type error interface {  
    Error() string
}
    - fmt.Println 在打印错误时,会在内部调用 Error() string 方法来得到该错误的描述。

// 3、自定义错误
创建自定义错误最简单的方法是使用 [errors]包中的 [New] 函数。

二十二、panic 和 recover

// 1、什么是 panic
    - 当程序发生异常时,无法继续运行。在这种情况下,我们会使用 panic 来终止程序。
    - 当[函数]发生 panic 时,它会终止运行,在执行完所有的[延迟]函数后,程序控制返回到该函数的调用方。
    - 这样的过程会一直持续下去,直到当前[协程]的所有函数都返回退出,然后程序会打印出 panic 信息,接着打印出堆栈跟踪(Stack Trace),最后程序终止。

// 2、什么时候应该使用 panic?
    - 发生了一个不能恢复的错误,此时程序不能继续运行。
    - 发生了一个编程上的错误。 

// 3、recover
    - recover 是一个内建函数,用于重新获得 panic 协程的控制。
    - 只有在延迟函数的内部,调用 recover 才有用。

二十三、反射

// 1、什么是反射?
    - 反射就是程序能够在运行时检查变量和值,求出它们的类型。

// 2、为何需要检查变量,确定变量的类型?
    - 在学习反射时,所有人首先面临的疑惑就是:如果程序中每个变量都是我们自己定义的,那么在编译时就可以知道变量类型了,
    - 为什么我们还需要在运行时检查变量,求出它的类型呢?没错,在大多数时候都是这样,但并非总是如此。
func createQuery(q interface{}) string {
}

// 3、reflect 包
    - 在 Go 语言中,[reflect] 实现了运行时反射。
    - reflect 包会帮助识别 [interface{}] 变量的底层具体类型和具体值。

// 4、reflect.Type 和 reflect.Value
    - reflect.Type 表示 interface{} 的具体类型,而 reflect.Value 表示它的具体值。
    - reflect.TypeOf() 和 reflect.ValueOf() 两个函数可以分别返回 reflect.Type 和 reflect.Value。

// 5、reflect.Kind
    - 在反射包中,Kind 和 Type 的类型可能看起来很相似,
    - Type 表示 interface{} 的实际类型(在这里是 main.Order),而 Kind 表示该类型的特定类别(在这里是 struct)。
    - t := reflect.TypeOf(q)
    - k := t.Kind()

// 6、NumField() 和 Field() 方法
    - [NumField()]方法返回结构体中字段的数量,而 [Field(i int)]方法返回字段 i 的 reflect.Value。
    - v.NumField()
    - v.Field(i)

// 7、Int() 和 String() 方法
    - [Int]和 [String]可以帮助我们分别取出 reflect.Value 作为 int64 和 string。
    - v.Int() v.String()

// 8、我们应该使用反射吗?
    - 反射是 Go 语言中非常强大和高级的概念,我们应该小心谨慎地使用它。
    - 使用反射编写清晰和可维护的代码是十分困难的。你应该尽可能避免使用它,只在必须用到它时,才使用反射。

二十四、读取文件

// 1、将整个文件读取到内存
    - 将整个文件读取到内存是最基本的文件操作之一。这需要使用 [ioutil] 包中的 [ReadFile] 函数。
    - io/ioutil
    - ioutil.ReadFile()
    - func ReadFile(filename string) ([]byte, error) {}

// 2、分块读取文件
    - 这可以使用 [bufio] 包来完成
f, err := os.Open()
r := bufio.NewReader(f)    // 新建了一个缓冲读取器
b := make([]byte, 3)      
for {
    _, err := r.Read(b)    // 以 3 个字节的块为单位读取
                           // Read [方法]会读取 len(b) 个字节(达到 3 字节),并返回所读取的字节数。当到达文件最后时,它会返回一个 EOF 错误
}

// 3、逐行读取文件
    - 可以使用 [bufio] 来实现
    - 1、打开文件;2、在文件上新建一个 scanner;3、扫描文件并且逐行读取。
f, err := os.Open()
s := bufio.NewScanner(f)    // 用文件创建了一个新的 scanner
for s.Scan() {
    fmt.Println(s.Text())   // Scan() 方法读取文件的下一行,如果可以读取,就可以使用 Text() 方法
}

二十五、写入文件

// 1、将字符串写入文件
    - 创建文件 -> 将字符串写入文件
f, err := os.Create("test.txt")
l, err := f.WriteString("Hello World")

// 2、将字节写入文件
    - 将字节写入文件和写入字符串非常的类似。我们将使用 [Write] 方法将字节写入到文件。
f, err := os.Create("/home/naveen/bytes")
d2 := []byte{104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100}
n2, err := f.Write(d2)

// 3、追加到文件
    - 这个文件将以追加和写的方式打开。这些标志将通过 [Open] 方法实现。
f, err := os.OpenFile("lines", os.O_APPEND|os.O_WRONLY, 0644)
newLine := "File handling is easy."
_, err = fmt.Fprintln(f, newLine)

// 4、并发写文件
    - 当多个 goroutines 同时(并发)写文件时,我们会遇到[竞争条件]。因此,当发生同步写的时候需要一个 channel 作为一致写入的条件。