🐕Go 语言之旅
Go
发布于: 2021-07-29

golang-logo.png

Go 是一门非常优秀的编程语言,本文记录了一些使用技巧和心得以及 Go 语言的最佳实践或不足之处,文中错误之处也请大家多多指教。

本文不定期保持更新中...

go-banner.png

基础

结构体struct

声明一个结构体,但是不构造成员。如下为 Human 申请内存空间并赋值成员零值,所以fmt.Println(jack.Name)并不会抛出空指针异常,但是在 Java 中由于未实例化对象直接调用 Name 会抛出空指针异常,并仅对基本数据类型初始化零值。

human.go - The Go Playground

package main

import "fmt"

type Human struct {
    Name string
}

func main() {
    var jack Human
    fmt.Println(jack.Name)
}


output:

对于指针类型的成员则会初始化为nil.

根据上面的例子我们稍加改进验证以上思想,如下输出:

humanv2.go - The Go Playground

package main

import "fmt"

type Head struct {
    hairs int64
}

type Human struct {
    Name  string
    Head  Head
    PHead *Head
}

func main() {
    var jack Human
    fmt.Println(jack.Name)
    fmt.Println(jack.Head.hairs)
    fmt.Printf("%v", jack.PHead)
}

output:

0
<nil>

类型系统

理解 Go 类型系统

引用传递还是值传递

值类型:int float bool string 数组 结构体

引用类型:pointer slice channel interface map func

首先结论:Go 语言中没有引用传递,全部都是值传递

Go 存在 "指针"的特性,初学者可能会联想到 C 语言中的指针,但是 Go 指针并非类型 C 的指针,使用方式上也有很大区别,实际上 Go 指针在参数传递的思想上与 Java 类似。在学习 Go 语言的过程中我喜欢通过一些 example 来去验证文档以及自己的理解。

下面通过几个例子,来帮助理解 Go 参数传递是怎样的。

下方代码提供一个user结构体,其中name和age成员用作状态变化参照物,我们讨论 updateName和changeName 等方法主要用于修改实例u的状态。

example1.go

package main

import "fmt"
type user struct {
    name string
    age uint
}
func main() {
    u := user{
        name: "Hao",
        age: 10,
    }
    fmt.Printf("main: u address %p\n", &u)
    fmt.Printf("user name is %s\n", u.name)
updateName(u)
fmt.Printf("update user name is %s \n", u.name)
changeName(&amp;u)
fmt.Printf("change user name is %s \n", u.name)
replaceUser(&amp;u)
fmt.Printf("relace user name is %s \n", u.name)
replaceUser2(u)
fmt.Printf("relace user 2 name is %s \n", u.name)
}
func updateName(u user) {
    fmt.Printf("update: u address %p\n", &u)
    u.name = "Xiao hong"
}
func changeName(u *user) {
    fmt.Printf("change: u address %p \n", u)
    u.name = "Xiao ming"
}
func replaceUser(u *user) {
    u = &user{
        name: "Hua",
        age: 11,
    }
}
func replaceUser2(u user) {
    u = user{
        name: "Kun",
        age: 12,
    }
}

运行+输出:

main: u address 0xc000198000
user name is Hao
update: u address 0xc000198018
update user name is Hao
change: u address 0xc000198000
change user name is Xiao ming
relace user name is Xiao ming
relace user 2 name is Xiao ming

updateName

将 u 传递到 updateName 方法,打印中 name 并没有任何变化,输出的结构体地址为 0xc000198018 与外层 main 方法中打印的地址也不一致的。这种场景下 Go 默认为拷贝传递,updateName 方法接收到的实际是一个拷贝实例,也就是副本。

思考一个问题,这里的传递的副本是深拷贝还是浅拷贝?继续写几个例子:

example2.go

package main
import "fmt"
type user struct {
    name string
    age int
    gender bool
    childrens [2]string
    m map[string]string
}
func main() {
childrens := [...]string{"a", "b"}
hua := user{
    name: "hua",
    age: 10,
    gender: false,
    childrens: childrens,
    m: make(map[string]string),
}
hua.m["title"] = "manager"
fmt.Printf("hua children 0 %s and hua title %s \n", hua.childrens[0], hua.m["title"])
update(hua)
fmt.Printf("hua children 0 %s and hua title %s \n", hua.childrens[0], hua.m["title"])
}
func update(u user) {
    u.childrens[0] = "x"
    u.m["title"] = "none"
}

上方代码特地使用了数组(需正确区分切片和数组)和map来确认参数传递是否为深拷贝,输出结果如下:

hua children 0 a and hua title manager
hua children 0 a and hua title none

children[0] 的名字没有被改变,但是m中的title却发生了变化。

这可以说明 Go 直接传递的是浅拷贝副本吗?如果内部存在struct会被改变吗?如果内部存在指针类型的struct会被改变吗? 再次补充代码:

example3.go

package main
import "fmt"
type phone struct {
    name string
}
type user struct {
    name string
    age int
    gender bool
    childrens [2]string
    m map[string]string
    phone1 phone
    phone2 *phone
}
func main() {
childrens := [...]string{"a", "b"}
phone1 := phone{
    name: "iphone",
}
phone2 := phone{
    name: "vivo",
}
hua := user{
    name: "hua",
    age: 10,
    gender: false,
    childrens: childrens,
    m: make(map[string]string),
    phone1: phone1,
    phone2: &amp;phone2,
}
hua.m["title"] = "manager"
fmt.Printf("hua children 0 %s and hua title %s and hua phone1 name %s and hua phone2 name %s \n", hua.childrens[0], hua.m["title"], hua.phone1.name, hua.phone2.name)
update(hua)
fmt.Printf("hua children 0 %s and hua title %s and hua phone1 name %s and hua phone2 name %s \n", hua.childrens[0], hua.m["title"], hua.phone1.name, hua.phone2.name)
}
func update(u user) {
    u.childrens[0] = "x"
    u.m["title"] = "none"
u.phone1.name = "Huawei meta 40"
u.phone2.name = "Huawei Meta 100"
}

运行+输出:

hua children 0 a and hua title manager and hua phone1 name iphone and hua phone2 name vivo
hua children 0 a and hua title none and hua phone1 name iphone and hua phone2 name Huawei Meta 100

结果是指针类型的 phone2 被改变了,所以到这里可以清晰的知道,默认下传递的内容包括指针都是拷贝类型,由于指针直接指结构体地址,所以导致 phone2 被改变。

注意,数组为值类型传递和赋值皆为拷贝。

相关补充链接

深入解析 Go 中 Slice 底层实现 (halfrost.com)

make 和 new

make的作用是初始化内置的数据结构,如:切片、哈希表和 Channel

new的作用是根据传入的类型分配一片内存空间并返回指向这片内存空间的指针

Go 语言设计与实现 - make 和 new

Channel 的使用

Channel 几乎是 Go 最有趣的特性了。

区分切片和数组

数组是定长的一旦创建长度就不可以被改变,而切片是在创建后长度依旧可以变化。数组和切片最直观的区别是,数组带有长度声明 [10]int这个一个长度为 10 的数组。

创建数组

var a [4]int
letters := [2]string{"a", "b"}

创建切片

letters := []string{"a", "b", "c", "d"}
// 使用 make 创建并指定切片长度
var s1 = make([]byte, 5)
// 使用 make 创建并指定切片长度和容量
var s2 = make([]byte, 5, 10)

反射 reflect

如何在 Go 中使用反射
go-banner.png

奇技淫巧

defer 顺序和作用域

Golang 中常用 defer 来关闭资源,这与 Java 中的 try finally 基本思想一致。举例:

Java 关闭流

BufferedReader r = new BufferedReader(...);
try {
  // ...
} finally {
  r.close()
}

上述代码保证 BufferedReader 在使用完成后关闭流。

Golang 使用 defer 实现上述功能比较优雅,只需要在 defer 后追加相关方法或逻辑即可。

package main
func main() {
  done := make(chan bool)
  // 关闭 channel
  defer close(done)
  // do somethings
}

多个 defer 的执行顺序

在工程项目中可能要关闭多个 channel并且要求按照指定顺序关闭

package main
import "fmt"
func main() {
  defer func() {
     fmt.Printf("Hello ")
  }()
  defer func() {
     fmt.Printf("World ")
  }()
  // do sometings
}

在程序结束后会首先打印出 World 接着是 Hello 可见多个defer是倒序执行的。

多个不同作用域下的 defer 执行顺序

下面写几行刁钻的代码,生产环境请不要这样使用,此处仅为了帮助我们理解在不同作用域下 defer 的执行规则是怎么样。

package main
import "fmt"
func main() {
    defer fmt.Println("defer 1")
    defer fmt.Println("defer 2")
    defer fmt.Println("defer 3")
if 1 &gt; 0 {
    defer fmt.Println("if defer 1")
    fmt.Println("1 &gt; 0")
    defer fmt.Println("if defer 2")
    defer fmt.Println("if defer 3")
}
if 1 &gt; -1 {
    defer fmt.Println("if2 defer 1")
    fmt.Println("1 &gt; -1")
    defer fmt.Println("if2 defer 2")
    defer fmt.Println("if2 defer 3")
}
fmt.Println("hello world!")
}

上述代码会依次输出

1 > 0
1 > -1
hello world!
if2 defer 3
if2 defer 2
if2 defer 1
if defer 3
if defer 2
if defer 1
defer 3
defer 2
defer 1

可以看到虽然 defer所在的作用域不同,但是执行顺序的规则与前一例是一致的,都是倒序执行,也就是最后定义的最先执行,先进后出(Stack)策略。

go-banner.png

Go 的不足或取舍

interface 隐式匹配

隐式匹配带来过于灵活的问题,下文同样也和 Java 对照,两者在接口表达上有哪些不同。

Golang 实现接口

type duck interface {
  fly()
}
type brid interface {
  fly()
}
type pig struct {}
func (p pig) fly() {
  // TODO 
  // Pig can fly too!
}

Go 语言中由于隐式匹配的特性,Pig对象不需要任何声明就可以讲实现了duck接口也实现了brid

Java 实现接口

public interface Duck {
  void fly()
}
public interface Brid {
  void fly() 
}
public class Pig implements Duck, Brid {
  public void fly() {}
}

Java 需要显式的声明具体实现了哪些接口,才可以具有实际的多态能力,否则这只猪只能是鸭子或者小鸟。

这正是面向对象语言的魅力,一只猪可以是鸭子🦆也同样可以是小鸟🐦

笔者故意使用 pig 和 duck 以及 brid 这三个动物表达编程语言的抽象之处,另一个用意也希望表达 Golang 接口隐式匹配过于灵活,其实会带来意想不到的问题。

Go 在实际工程化中,针对某些特殊的接口往往要做一些补充或者限制,以避免被某个结构体错误的实现,但是这些派生出的接口方法往往是无用的,举几个例子:

runtime.Error

type runtime.Error interface {
    error
// RuntimeError is a no-op function but
// serves to distinguish types that are run time
// errors from ordinary errors: a type is a
// run time error if it has a RuntimeError method.
RuntimeError()
}

Protobuf 中的 Message 接口

type proto.Message interface {
    Reset()
    String() string
    ProtoMessage()
}

疑问疑答

Go 语言不需要实例化 struct 吗?

Go 中的切片赋值是拷贝吗?

举例下方打印 a b的地址是不一样的,其实某些结构的赋值本质上就是拷贝一条新的引用,即 a 和 b 是两条引用,同时指向一块内存地址。

a := make([]int, 10)
b := a

fmt.Printf("%p %p", a, b)

由于切片扩容的特性,引用的地址导致人琢磨不透,更是要多多注意,例如下方代码,b在扩容后引用指向另一块内存地址。

package main

import (
    "fmt"
)

func main() {
    a := []int{0, 1, 2, 3, 4}
    b := a
    b[0] = 111 // 此处修改,a[0]同时修改
    fmt.Println(a)
    fmt.Println(b)
    fmt.Println("---------------------")

    b = append(b, 0) / /b扩容后,不再与a共享内存
    b[0] = 222       // 此处修改,a[0]不受影响
    fmt.Println(a)
    fmt.Println(b)
}

条件编译

Go 中条件编译非常简单。

最近专注在 TypeType 的开发协同上,使用到一些关于条件编译的特性,非常简单。

示例:

在 linux 环境进行编译

// +build linux

package main

非 linux 环境下编译

// +build !linux

package main