工作中用Go: Go基础

2023-01-13  本文已影响0人  daydaygo

背景介绍

工作中用Go: 工具篇 - 简书 (jianshu.com) 介绍了相关工具的使用, 这篇聚集 Go基础.

Go is simple but not easy.

Go 很简单,但不容易掌握

type: 类型系统

先说结论:

Go内置关键字, 大部分可以直接从源码中查看 <Go>/src/builtin/builtin.go 中查看, 其中大部分都是Go内置类型

怎么快速查看Go源码, 可以访问上一篇blog: 工作中用Go: 工具篇 - 简书 (jianshu.com)

var 变量

变量的本质: 特定名字 <-> 特定内存块

变量声明的3种方式:

// 申明且显式初始化
a := int(10) 

// 默认为0值
var a int

func A() (a int) // 命名返回值相当于 var a int

变量的作用域(scope)

变量常见问题 - 变量遮蔽(variable shadowing): 定义了同名变量, 容易导致变量混淆, 产生隐藏bug且难以定位

a, err := A()
// do something
b, err := B() // 再次定义同名 err 变量

type alias 类型别名

类型别名(type alias)的存在,是 渐进式代码修复(Gradual code repair) 的关键

// <Go>/src/builtin/builtin.go

// rune is an alias for int32 and is equivalent to int32 in all ways. It is
// used, by convention, to distinguish character values from integer values.
type rune = int32

类型别名其实是对现实世界的一种映射, 同一个事物拥有不同的名字的场景太多, 比如 apple苹果, 再比如 土豆马铃薯 , 更有意思的一个例子:

你们抓周树人,关我鲁迅什么事? -- 《楼外楼》

0值

Go中基础类型和0值对照表:

type 0值
int byte rune 0
float 0.0
bool false
string ""
struct 字段都为0值
slice map pointer interface func nil

关于 nil, 可以从源码中获取到详细信息:

// <Go>/src/builtin/builtin.go

// nil is a predeclared identifier representing the zero value for a
// pointer, channel, func, interface, map, or slice type.
var nil Type // Type must be a pointer, channel, func, interface, map, or slice type

func 也只是类型的一种:

t := T{}
f := func(){} // 函数字面值.FunctionLiteral

type HandlerFunc func(ResponseWriter, *Request)
http.HandlerFunc(hello) // hello 和 HandlerFunc 出入参相同, 所以才能进行类型转换
func hello(writer http.ResponseWriter, request *http.Request) {
 // fmt.Fprintln(writer, "<h1>hello world</h1>")
 fmt.Fprintf(writer, "<h1>hello world %v</h1>", request.FormValue("name"))
}

值类型 vs 指针类型

结合上面变量的本质来理解:

变量的本质: 特定名字 <-> 特定内存块

那么值类型和指针类型就很容易理解: 值类型在函数调用过程中会发生复制, 指向新的内存块, 而指针则指向同一块内存

再结合上面的0值, 有一个简单的规则:

0值的为 nil 的类型, 函数调用时不会发生复制

当然, 这条规则还需要打上不少补丁, 我们在后面继续聊

还有一个经典问题: 值类型 vs 指针类型, 怎么选 / 用哪个?

其实回答这个问题, 只需要列举几个 Must 的 case 即可:

// 源码中 sync.Mutex 上的说明
// A Mutex must not be copied after first use.

// Go中还有特殊 noCopy 类型
// noCopy may be added to structs which must not be copied
// after the first use.
//
// See https://golang.org/issues/8005#issuecomment-190753527
// for details.
//
// Note that it must not be embedded, due to the Lock and Unlock methods.
type noCopy struct{}

// Lock is a no-op used by -copylocks checker from `go vet`.
func (*noCopy) Lock()   {}
func (*noCopy) Unlock() {}

// Good:
type Record struct {
  buf bytes.Buffer
  // other fields omitted
}

func New() *Record {...}

func (r *Record) Process(...) {...}

func Consumer(r *Record) {...}

// Bad:
type Record struct {
  buf bytes.Buffer
  // other fields omitted
}

func (r Record) Process(...) {...} // Makes a copy of r.buf

func Consumer(r Record) {...} // Makes a copy of r.buf

0值可用

大部分情况下, Go中的类型都是满足 0值可用 的, 需要注意几个点:

m := make(map[int]int, 10) // 推荐

var m = map[int]int{1:1} // 初始化对应值

// 以 sync.Mutex 的使用举例
var mu sync.Mutex // 零值不需要额外初始化

type Counter struct {
 Type int
 Name string

 mu  sync.Mutex // 1.放在要控制的字段上面并空行 2.内嵌字段
 cnt uint64
}

// 1.封装成方法 
// 2.读写都需要
func (c *Counter) Incr() {
 c.mu.Lock()
 c.cnt++
 c.mu.Unlock()
}
func (c *Counter) Cnt() uint64 {
 c.mu.Lock()
 defer c.mu.Unlock()
 return c.cnt
}

type ReqListReq struct {
 Month     string     `form:"month"`      // 期间, 格式: 202212
 Status    pay.Status `form:"status"`     // 审批状态
}

type Status int // 审批状态

const (
 StatusNone    Status = iota // 0值
 StatusInit                  // 未开始
 StatusIng                   // 审批中
 StatusDone                  // 已通过
 StatusReject                // 已拒绝
 StatusCancel                // 已撤回
)

如果请求带了 status 查询条件, 则一定非0值

语法和易错点

byte / rune / string

type rune = int32: Go中用 rune 表示一个 utf8 编码, 在 utf8 中, 一个字符由 1-4字节 来编码

len("汉") // 3
utf8.RuneCountInString("汉") // 1

[]byte("汉") // []byte{0xE6, 0xB1, 0x89}
[]rune("汉")

遍历string:

字符串拼接:

性能上的注意点:

slice

s := make([]int, 0, 10)
s = append(s, 10)

map

sort.Slice(resp, func(i, j int) bool {
 return resp[i].MonthNumber < resp[j].MonthNumber
})

type Set[K comparable] map[K]struct{}

struct

var t T // T的所有字段设置为0值进行初始化

var t *T // nil, 不推荐, t必须初始化才能使用
(t *T) // 函数的命名返回值也会踩这个坑

t := &T{} // 等价的, 都是使用 0 值来初始化T并返回指针, 推荐使用 &T{}
t := new(T)

  1. struct{} 是 0 内存占用, 可以在一些优化一些场景, 不需要分配内存, 比如
  1. struct 内存对齐(aligned)
// 查看内存占用: unsafe.Sizeof
i := int32(10)
s := struct {}{}
fmt.Println(unsafe.Sizeof(i)) // 4
fmt.Println(unsafe.Sizeof(s)) // 0

// 查看内存对齐后的内存占用
unsafe.Alignof()

for

// bad
var a []T
var b []*T
for _, v := range a {
 b = append(b, &v) // &V 都是指向同一个地址, 最后导致 b 中都是相同的元素
}

// bad
for i := 0; i < 10; i++ {
 go func() {
  println(i) // 协程被调度时, i 的值并不确定
 }()
}

// good
for i := 0; i < 10; i++ {
 go func(i int) {
  println(i)
 }(i)
}

break

break + for/select/switch 只能跳出一层循环, 如果要跳出多层循环, 使用 break label

switch

Go中的switch和以前的语言有很大的不同, break只能退出当前switch, 而 Go 中 switch 执行完当前 case 就会退出, 所以大部分情况下, break 都可以省略

func

err

// 初始化
err := errors.New("xxx")
err := fmt.Errorf("%v", xxx)

// wrap
err := fmt.Errorf("wrap err: %w", err)

// 预期内err
var ErrFoo = errors.New("foo")

// 非预期err, 比如 net.Error
// An Error represents a network error.
type Error interface {
 error
 Timeout() bool // Is the error a timeout?

 // Deprecated: Temporary errors are not well-defined.
 // Most "temporary" errors are timeouts, and the few exceptions are surprising.
 // Do not use this method.
 Temporary() bool
}

// Format formats the frame according to the fmt.Formatter interface.
//
//    %s    source file
//    %d    source line
//    %n    function name
//    %v    equivalent to %s:%d
//
// Format accepts flags that alter the printing of some verbs, as follows:
//
//    %+s   function name and path of source file relative to the compile time
//          GOPATH separated by \n\t (<funcname>\n\t<path>)
//    %+v   equivalent to %+s:%d
func (f Frame) Format(s fmt.State, verb rune) {
 switch verb {
 case 's':
  switch {
  case s.Flag('+'):
   io.WriteString(s, f.name())
   io.WriteString(s, "\n\t")
   io.WriteString(s, f.file())
  default:
   io.WriteString(s, path.Base(f.file()))
  }
 case 'd':
  io.WriteString(s, strconv.Itoa(f.line()))
 case 'n':
  io.WriteString(s, funcname(f.name()))
 case 'v':
  f.Format(s, 's')
  io.WriteString(s, ":")
  f.Format(s, 'd')
 }
}

defer

panic

运行时 / panic() 产生, panic 会一直出栈, 直到程序退出或者 recover, 而 defer 一定会在函数运行后执行, 所以:

Go源码中还有一种用法: 提示潜在bug

// json/encode.go resolve()
func (w *reflectWithString) resolve() error {
 if w.k.Kind() == reflect.String {
  w.ks = w.k.String()
  return nil
 }
 if tm, ok := w.k.Interface().(encoding.TextMarshaler); ok {
  if w.k.Kind() == reflect.Pointer && w.k.IsNil() {
   return nil
  }
  buf, err := tm.MarshalText()
  w.ks = string(buf)
  return err
 }
 switch w.k.Kind() {
 case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
  w.ks = strconv.FormatInt(w.k.Int(), 10)
  return nil
 case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
  w.ks = strconv.FormatUint(w.k.Uint(), 10)
  return nil
 }
 panic("unexpected map key type") // 正常情况不会走到这里, 如果走到了, 就有潜在bug
}

方法(method)

interface

func TestInterface(t *testing.T) {
 var a any
 var b error
 var c *error
 d := &b
 t.Log(a == b)   // true
 t.Log(a == c)   // false
 t.Log(c == nil) // true
 t.Log(d == nil) // false
}

使用 debug 查看:

debug: interface & nil

Go 社区流传一个经验法则:“接受接口,返回结构体(Accept interfaces, return structs)

Go 语言之父 Rob Pike 曾说过:空接口不提供任何信息(The empty interface says nothing)

写在最后

得益于Go的核心理念和设计哲学:

核心理念:简单、诗意、简洁(Simple, Poetic, Pithy)

设计哲学:简单、显式、组合、并发和面向工程

Go 拥有编程语言中相对最少的关键字和类型系统, 让Go入门变得极易学习和上手, 希望这blog能帮助入门的gopher, 更快掌握Go, 同时避免一些易错点

上一篇 下一篇

猜你喜欢

热点阅读