[golang] 语言基础

496 阅读15分钟

Go 基础

内置基础类型

数值类型

runeint8int16int32int64byteuint8uint16uint32uint64,其中 runeint32 的别称,byteuint8 的别称。
浮点数 的类型有 float32float64 两种(没有 float 类型)。
复数 的类型有 complex128(64 位实数 + 64 位虚数)和 complex64(32 位实数 + 32 位虚数)。复数的形式为 RE + IMi,其中 RE 是实数部分,IM 是虚数部分,而最后的 i 是虚数单位。

var c complex64 = 5 + 5i
// output: (5+5i)
fmt.Printf("Value is: %v", c)

string

在 Go 中字符串是不可变的,但如果真的想要修改怎么办呢?

s := "hello"
c := []byte(s) // 将字符串 s 转换为 []byte 类型
c[0] = 'c'
s2 := string(c) // 再转换回 string 类型
fmt.Printf("%s\n", s2)

当需要对一个字符串进行频繁的操作时,谨记在 go 语言中字符串是不可变的(类似 java 和 c#)。使用诸如 a += b 形式连接字符串效率低下,尤其在一个循环内部使用这种形式。这会导致大量的内存开销和拷贝。应该使用一个字符数组代替字符串,将字符串内容写入一个缓存中。

var b bytes.Buffer
...
for condition {
    b.WriteString(str) // 将字符串 str 写入缓存 buffer
}
return b.String()

注意:由于编译优化和依赖于使用缓存操作的字符串大小,当循环次数大于 15 时,效率才会更佳。

数组

数组之间的赋值是值的赋值,即当把一个数组作为参数传入函数的时候,传入的其实是该数组的副本,而不是它的指针。

var variable_name [SIZE] variable_type
var balance = [5]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
balance := [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
balance := [5]float32{1:2.0,3:7.0}

多维数组

var variable_name [SIZE1][SIZE2]...[SIZEN] variable_type
func main() {
    // Step 1:创建数组
    values := [][]int{}

    // Step 2:使用 appped() 函数向空的二维数组添加两行一维数组
    row1 := []int{1, 2, 3}
    row2 := []int{4, 5, 6}
    values = append(values, row1)
    values = append(values, row2)

    // Step 3:显示两行数据
    fmt.Println("Row 1")
    fmt.Println(values[0])
    fmt.Println("Row 2")
    fmt.Println(values[1])

    // Step 4:访问第一个元素
    fmt.Println("第一个元素为:")
    fmt.Println(values[0][0])
}

slice

slice 是引用类型,所以当引用改变其中元素的值时,其它的所有引用都会改变该值。
slice 是一个结构体,这个结构体包含了三个元素:

  • 引用数组指针地址;
  • 切片的目前使用长度;
  • 切片的容量;
// 默认是 nil
var identifier []type
s := make([]int, len, cap)
y := s[low:high:max]
func main() {
    s := []int{1, 2, 3}
    a := s[0:1]
    s[0] = 888
    fmt.Printf("a: %p\n", a)
    fmt.Println("a", a, len(a), cap(a))
    fmt.Printf("s: %p\n", s)
    fmt.Println("s", s, len(s), cap(s))
    s = append(s, 4)
    fmt.Println("扩容后")
    s[0] = 666
    fmt.Printf("a: %p\n", a)
    fmt.Println("a", a, len(a), cap(a))
    fmt.Printf("s: %p\n", s)
    fmt.Println("s", s, len(s), cap(s))
}
a: 0xc00011a000
a [888] 1 3
s: 0xc00011a000
s [888 2 3] 3 3
扩容后
a: 0xc00011a000
a [888] 1 3
s: 0xc000120000
s [666 2 3 4] 4 6
func main() {
    var (
        arr   = [3]int{1, 2, 3}
        slice = []int{1, 2, 3}
    )

    changeArr(&arr)
    changeSlice(slice)

    fmt.Println("arr", arr)
    fmt.Println("slice", slice)
}

func changeArr(arr *[3]int) {
    arr[0] = 100
}

func changeSlice(slice []int) {
    slice[0] = 100
}
arr [100 2 3]
slice [100 2 3]

slice 和垃圾回收

切片的底层指向一个数组,该数组的实际容量可能要大于切片所定义的容量。只有在没有任何切片指向的时候,底层的数组内存才会被释放,这种特性有时会导致程序占用多余的内存。

var digitRegexp = regexp.MustCompile("[0-9]+")

// 返回的 []byte 指向的底层是整个文件的数据,只要该返回的切片不被释放,垃圾回收器就不能释放整个文件所占用的内存
func FindDigits(filename string) []byte {
    b, _ := ioutil.ReadFile(filename)
    return digitRegexp.Find(b)
}

// 可以通过拷贝需要的部分到一个新的切片中
func FindFileDigits(filename string) []byte {
   fileBytes, _ := ioutil.ReadFile(filename)
   b := digitRegexp.FindAll(fileBytes, len(fileBytes))
   c := make([]byte, 0)
   for _, bytes := range b {
      c = append(c, bytes...)
   }
   return c
}

map

// 默认是 nil,nil map 不能用来存放键值对
var map_variable map[key_data_type]value_data_type
map_variable := make(map[key_data_type]value_data_type)

make & new

make 只能创建 slicemapchannel,并且返回一个有初始值(非零)的 T 类型,而不是 *T。本质来讲,导致这三个类型有所不同的原因是指向数据结构的引用在使用前必须被初始化。例如,一个 slice,是一个包含指向数据(内部 array)的指针、长度和容量的三项描述符;在这些项目被初始化之前,slicenil。对于 slicemapchannel来说,make 初始化了内部的数据结构,填充适当的值。
内建函数 new 本质上说跟其它语言中的同名函数功能一样,new(T) 分配了零值填充 T 类型的内存空间,并且返回其地址,即一个 *T 类型的值。用 Go 的术语说,它返回了一个指针,指向新分配的类型 T 的零值。

// OK
y := new(Bar)
(*y).thingOne = "hello"
(*y).thingTwo = 1

// NOT OK
z := make(Bar) // 编译错误:cannot make type Bar
(*z).thingOne = "hello"
(*z).thingTwo = 1

// OK
x := make(Foo)
x["x"] = "goodbye"
x["y"] = "world"

// NOT OK
u := new(Foo)
(*u)["x"] = "goodbye" // 运行时错误!! panic: assignment to entry in nil map
(*u)["y"] = "world"

使用 new 和 make 创建 map 时的差异

使用 new 来创建 map 时,返回的内容是一个指针,这个指针指向了一个所有字段全为 0 的值 map 对象,需要初始化后才能使用,而使用 make 来创建 map 时,返回的内容是一个引用,可以直接使用。

image.png

// 使用 new 创建一个 map 指针
ma := new(map[string]int)
// 第一种初始化方法
*ma = map[string]int{}
(*ma)["a"] = 44
fmt.Println(*ma)

// 第二种初始化方法
*ma = make(map[string]int, 0)
(*ma)["b"] = 55
fmt.Println(*ma)

// 第三种初始化方法
mb := make(map[string]int, 0)
mb["c"] = 66
*ma = mb
(*ma)["d"] = 77
fmt.Println(*ma)
// 使用 make 来创建并使用 map
ma := make(map[string]int)
ma["a"] = 33
fmt.Println(ma)

结论:

  • 切片、map 和通道,使用 make
  • 数组、结构体和所有的值类型,使用 new

iota

常量中的数据类型只可以是布尔型、数值型(整数型、浮点型和复数)和字符串型。

const (
    a = "abc"
    b = len(a)
    c = unsafe.Sizeof(a)
)

const (
    a = iota   // 0
    b          // 1
    c          // 2
    d = "ha"   // 独立值,iota += 1
    e          // "ha",iota += 1
    f = 100    // iota += 1
    g          // 100,iota +=1
    h = iota   // 7,恢复计数
    i          // 8
)

const (
    h, i, j = iota, iota, iota // h = 0、i = 0、j = 0,因为 iota 在同一行
)

error

err := errors.New("emit macho dwarf: elf header corrupted")
if err != nil {
    fmt.Print(err)
}
type User struct {
    username string
    password string
}

func (p *User) init(username string, password string) (*User, string) {
    if "" == username || "" == password {
        return p, p.Error()
    }
    p.username = username
    p.password = password
    return p, ""
}

func (p *User) Error() string {
    return "Usernam or password shouldn't be empty!"
}

func main() {
    var user User
    user1, _ := user.init("", "")
    fmt.Println(user1)
}
Usernam or password shouldn't be empty!

流程和函数

流程控制

switch

func main() {

    switch {
    case false:
            fmt.Println("1、case 条件语句为 false")
            fallthrough
    case true:
            fmt.Println("2、case 条件语句为 true")
            fallthrough
    case false:
            fmt.Println("3、case 条件语句为 false")
            fallthrough
    case true:
            fmt.Println("4、case 条件语句为 true")
    case false:
            fmt.Println("5、case 条件语句为 false")
            fallthrough
    default:
            fmt.Println("6、默认 case")
    }
}
2case 条件语句为 true
3case 条件语句为 false
4case 条件语句为 true

for

sum := 1
for sum < 1000 {
    sum += sum
}

break / continue

func main() {

    // 不使用标记
    fmt.Println("---- break ----")
    for i := 1; i <= 3; i++ {
        fmt.Printf("i: %d\n", i)
        for i2 := 11; i2 <= 13; i2++ {
            fmt.Printf("i2: %d\n", i2)
            break
        }
    }

    // 使用标记
    fmt.Println("---- break label ----")
re:
    for i := 1; i <= 3; i++ {
        fmt.Printf("i: %d\n", i)
        for i2 := 11; i2 <= 13; i2++ {
            fmt.Printf("i2: %d\n", i2)
            break / continue re
        }
    }
}

goto

func main() {
    /* 定义局部变量 */
    var a int = 10

    /* 循环 */
LOOP:
    for a < 20 {
        if a == 15 {
            /* 跳过迭代 */
            a = a + 1
            goto LOOP
        }
        fmt.Printf("a的值为 : %d\n", a)
        a++
    }
}
a的值为 : 10
a的值为 : 11
a的值为 : 12
a的值为 : 13
a的值为 : 14
a的值为 : 16
a的值为 : 17
a的值为 : 18
a的值为 : 19

函数

变参

// 变量 arg 是一个 int 的 slice
func myfunc(arg ...int) {}

函数作为实参

在 Go 中函数也是一种变量,可以通过 type 来定义它,它的类型就是所有拥有相同的参数,相同的返回值的一种类型。

func main() {
    /* 声明函数变量 */
    getSquareRoot := func(x float64) float64 {
        return math.Sqrt(x)
    }

    /* 使用函数 */
    fmt.Println(getSquareRoot(9))
}
2
type testInt func(int) bool // 声明了一个函数类型

func isOdd(integer int) bool {
    if integer%2 == 0 {
        return false
    }
    return true
}

func isEven(integer int) bool {
    if integer%2 == 0 {
        return true
    }
    return false
}

// 声明的函数类型在这个地方当做了一个参数
func filter(slice []int, f testInt) []int {
    var result []int
    for _, value := range slice {
        if f(value) {
            result = append(result, value)
        }
    }
    return result
}

func main() {
    slice := []int{1, 2, 3, 4, 5, 7}
    fmt.Println("slice = ", slice)
    odd := filter(slice, isOdd) // 函数当做值来传递了
    fmt.Println("Odd elements of slice are: ", odd)
    even := filter(slice, isEven) // 函数当做值来传递了
    fmt.Println("Even elements of slice are: ", even)
}
slice = [1 2 3 4 5 7]
Odd elements of slice are: [1 3 5 7]
Even elements of slice are: [2 4] 

闭包

Go 语言支持匿名函数,可作为闭包。匿名函数是一个“内联”语句或表达式。匿名函数的优越性在于可以直接使用函数内的变量,不必申明。

func getSequence() func() int {
    i := 0
    return func() int {
        i += 1
        return i
    }
}

func main() {
    /* nextNumber 为一个函数,函数 i 为 0 */
    nextNumber := getSequence()

    /* 调用 nextNumber 函数,i 变量自增 1 并返回 */
    fmt.Println(nextNumber())
    fmt.Println(nextNumber())
    fmt.Println(nextNumber())

    /* 创建新的函数 nextNumber1,并查看结果 */
    nextNumber1 := getSequence()
    fmt.Println(nextNumber1())
    fmt.Println(nextNumber1())
}
1
2
3
1
2
func add(x1, x2 int) func(int, int) (int, int, int) {
    i := 0
    return func(x3, x4 int) (int, int, int) {
        i += 1
        return i, x1 + x2, x3 + x4
    }
}

func main() {
    add_func := add(1, 2)
    fmt.Println(add_func(1, 1))
    fmt.Println(add_func(0, 0))
    fmt.Println(add_func(2, 2))
}
1 3 2
2 3 0
3 3 4

工厂函数案例。

func main() {
    addBmp := MakeAddSuffix(".bmp")
    addJpeg := MakeAddSuffix(".jpeg")

    fmt.Println(addBmp("file"))
    fmt.Println(addJpeg("file"))
}

func MakeAddSuffix(suffix string) func(string) string {
    return func(name string) string {
        if !strings.HasSuffix(name, suffix) {
            return name + suffix
        }
        return name
    }
}
file.bmp
file.jpeg

defer

当函数执行到最后时,defer语句会按照逆序执行。

func trace(s string) string {
    fmt.Println("entering:", s)
    return s
}

func un(s string) {
    fmt.Println("leaving:", s)
}

func a() {
    defer un(trace("a"))
    fmt.Println("in a")
}

func b() {
    defer un(trace("b"))
    fmt.Println("in b")
    a()
}

func main() {
    b()
}
entering: b
in b
entering: a
in a
leaving: a
leaving: b

使用 defer 语句来记录函数的参数与返回值。

func func1(s string) (n int, err error) {
    defer func() {
        log.Printf("func1(%q) = %d, %v", s, n, err)
    }()
    return 7, io.EOF
}

func main() {
    func1("Go")
}
2022/02/28 22:36:18 func1("Go") = 7, EOF

循环内的 defer 没有执行,所以文件一直没有关闭。垃圾回收机制可能会自动关闭文件,但是这会产生一个错误。

for _, file := range files {
    if f, err = os.Open(file); err != nil {
        return
    }
    // 这是错误的方式,当循环结束时文件没有关闭
    defer f.Close()
    // 对文件进行操作
    f.Process(data)
}

defer 仅在函数返回时才会执行,在循环的结尾或其他一些有限范围的代码内不会执行。

for _, file := range files {
    if f, err = os.Open(file); err != nil {
        return
    }
    // 对文件进行操作
    f.Process(data)
    // 关闭文件
    f.Close()
}

panic & recover

panic 是一个内建函数,可以中断原有的控制流程,进入一个 panic 状态中。当函数 F 调用 panic,函数 F 的执行被中断,但是 F 中的延迟函数会正常执行,然后 F 返回到调用它的地方。在调用的地方,F 的行为就像调用了 panic。这一过程继续向上,直到发生 panicgoroutine 中所有调用的函数返回,此时程序退出。可以直接调用 panic 产生,也可以由运行时错误产生,例如访问越界的数组。
recover 是一个内建函数,可以让进入 panic 状态的 goroutine 恢复过来。recover 仅在延迟函数中有效。在正常的执行过程中,调用 recover 会返回 nil,并且没有其它任何效果。如果当前的 goroutine 陷入 panic 状态,调用 recover 可以捕获到 panic 的输入值,并且恢复正常的执行。

func throwsPanic(f func()) (b bool) {
    defer func() {
        if x := recover(); x != nil {
            b = true
        }
    }()
    f() // 执行函数 f,如果 f 中出现了 panic,那么就可以恢复回来
    return
}

main 函数 & init 函数

Go 里面有两个保留的函数:init 函数(能够应用于所有的 package)和 main 函数(只能应用于 main package),这两个函数在定义时不能有任何的参数和返回值。虽然一个 package 里面可以写任意多个 init 函数,但强烈建议在一个 package 中每个文件只写一个 init 函数。
Go 程序会自动调用 init()main(),所以不需要在任何地方调用这两个函数。
程序的初始化和执行都起始于 main 包,如果 main 包还导入了其它的包,那么就会在编译时将它们依次导入。有时一个包会被多个包同时导入,那么它只会被导入一次(例如很多包可能都会用到 fmt 包,但它只会被导入一次,因为没有必要导入多次)。当一个包被导入时,如果该包还导入了其它的包,那么会先将其它包导入进来,然后再对这些包中的包级常量和变量进行初始化,接着执行 init 函数(如果有的话),依次类推。等所有被导入的包都加载完毕了,就会开始对 main 包中的包级常量和变量进行初始化,然后执行 main 包中的 init 函数(如果存在的话),最后执行 main 函数。
image.png

import

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

  • 包名与文件名没有直接关系;
  • 包名与文件夹名没有直接关系;
  • 同一个文件夹下的文件只能有一个包名,否则编译报错;
  • 只有包名为 main 的源码文件可以包含 main 函数;
  • 一个可执行程序有且仅有一个 main 包;
// 点操作,调用的时候只需要 Println(),而不需要 fmt.Println()
// fmt 是 Go 语言的标准库,其实是去 GOROOT 环境变量指定目录下去加载该模块
import . "fmt"

// _ 操作,引入该包,而不直接使用包里面的函数,而是调用了该包里面的 init 函数
import _ "github.com/ziutek/mymysql/godrv"

// 相对路径,当前文件同一目录的 model 目录,但是不建议这种方式来 import
import "./model"

// 绝对路径,加载 gopath/src/shorturl/model 模块
import "shorturl/model"

struct

方法

Go 语言中同时有函数和方法。一个方法就是一个包含了接受者的函数,接受者可以是命名类型或者结构体类型的一个值或者是一个指针。

type Foo struct {
    name string
}

func (f *Foo) PointerMethod() {
    fmt.Println("pointer method on", f.name)
}

func (f Foo) ValueMethod() {
    fmt.Println("value method on", f.name)
}

func NewFoo() Foo { // 返回一个右值
    return Foo{name: "right value struct"}
}

func main() {
    f1 := Foo{name: "value struct"}
    f1.PointerMethod() // 编译器会自动插入取地址符,变为 (&f1).PointerMethod()
    f1.ValueMethod()

    f2 := &Foo{name: "pointer struct"}
    f2.PointerMethod()
    f2.ValueMethod() // 编译器会自动解引用,变为 (*f2).PointerMethod()

    NewFoo().ValueMethod()
    NewFoo().PointerMethod() // Error!!!
}
# command-line-arguments
.\main.go:33:10: cannot call pointer method on NewFoo()
.\main.go:33:10: cannot take the address of NewFoo()

看来编译器首先试着给 NewFoo() 返回的右值调用 pointer method,出错;然后试图给其插入取地址符,未果,就只能报错了。
可以被寻址的是左值,既可以出现在赋值号左边也可以出现在右边;不可以被寻址的即为右值,比如函数返回值、字面值、常量值等等,只能出现在赋值号右边。

指针或值作为接收者

在值和指针上调用方法:可以有连接到类型的方法,也可以有连接到类型指针的方法。

type B struct {
    thing int
}

func (b *B) change() { b.thing = 1 }

func (b B) write() string { return fmt.Sprint(b) }

func main() {
    var b1 B // b1 是值
    b1.change()
    fmt.Println(b1.write())

    b2 := new(B) // b2 是指针
    b2.change()
    fmt.Println(b2.write())
}
{1}
{1}

将一个值类型作为一个参数传递给函数或者作为一个方法的接收者,似乎是对内存的滥用,因为值类型一直是传递拷贝。但是另一方面,值类型的内存是在栈上分配,内存分配快速且开销不大。如果你传递一个指针,而不是一个值类型,go 编译器大多数情况下会认为需要创建一个对象,并将对象移动到堆上,所以会导致额外的内存分配:因此当使用指针代替值类型作为参数传递时,并不一定有太大的收获。

匿名字段、继承、重写

在 Go 中,类型就是类(数据和关联的方法)。继承有两个好处:代码复用和多态。在 Go 中,代码复用通过组合和委托实现,多态通过接口的使用来实现:有时这也叫组件编程(Component Programming)。

type Skills []string

type Human struct {
    name   string
    age    int
    weight int
}

func (h *Human) SayHi() {
    fmt.Printf("Hi, I am %s\n", h.name)
}

type Student struct {
    Human      // 匿名字段,struct
    Skills     // 匿名字段,自定义的类型 string slice
    int        // 内置类型作为匿名字段
    speciality string
    age        int
}

func (s *Student) SayHi() {
    fmt.Printf("Hi, I Student am %s\n", s.name)
}

func main() {
    jane := Student{Human: Human{"Jane", 35, 100}, speciality: "Biology"}

    jane.Human.name = "Jane1"
    jane.Human.age = 23
    jane.age = 22
    fmt.Println("Her name is ", jane.Human.name)
    fmt.Println("Her Human.age is ", jane.Human.age)
    fmt.Println("Her age is ", jane.age)
    fmt.Println("Her speciality is ", jane.speciality)

    jane.Skills = []string{"anatomy"}
    fmt.Println("Her skills are ", jane.Skills)
    jane.Skills = append(jane.Skills, "physics", "golang")
    fmt.Println("Her skills now are ", jane.Skills)

    jane.int = 3
    fmt.Println("Her preferred number is", jane.int)

    jane.Human.SayHi()
    jane.SayHi()
}
Her name is  Jane1
Her Human.age is  23
Her age is  22
Her speciality is  Biology
Her skills are  [anatomy]
Her skills now are  [anatomy physics golang]
Her preferred number is 3
Hi, I am Jane1

interface

type Phone interface {
    call() string
}

type Android struct {
    brand string
}

type IPhone struct {
    version string
}

func (android Android) call() string {
    return "I am Android " + android.brand
}

func (iPhone IPhone) call() string {
    return "I am iPhone " + iPhone.version
}

func printCall(p Phone) {
    fmt.Println(p.call() + ", I can call you!")
}

func main() {
    var vivo = Android{brand: "Vivo"}
    var hw = Android{"HuaWei"}

    i7 := IPhone{"7 Plus"}
    ix := IPhone{"X"}

    printCall(vivo)
    printCall(hw)
    printCall(i7)
    printCall(ix)
}