Go 语法速学:JS 开发者的 Go 入门指南

399 阅读4分钟

如果使用 VSCode,需要安装两个插件:GoGolang

Hello World

  1. 初始化项目

    mkdir hello
    cd hello
    go mod init github.com/heycn/hello
    touch main.go
    
  2. 编写代码

    进入 main.go

     package main
    
     import "fmt"
    
     func main() {
       fmt.Println("Hello World!")
     }
    
  3. 编译并执行

    go build .
    ./hello # "Hello World!"
    # 也可以只运行
    go run .  # "Hello World!"
    

变量、常量、iota

变量

// 声明类型
var b int = 2
var c, d int = 3, 4
var (
	e        = 5
	f, g int = 6, 7
        // 不需要 var 关键字,并且自动指定类型
        h        := 8
        i, j     := 9, 10
)

常量与 iota

const (
	a1 = iota // iota 是从 0 开始
	a2
	a3
)

iota

  • iota 即希腊字母 ι
  • iota 是其英文发音
  • GoLang 中 iota 默认为 0,每行加 1
  • 使用 iota 可以减少 hard code(写死的代码)
  • 只能用在 const () 中

for 循环

// 接 1 个表达式
i := 1
for i <= 3 {
  fmt.Println(i)
  i++
}

// 3 个表达式
for j := 1; j <= 3; j++ {
  fmt.Println(j)
}

// 接 0 个表达式
k := 1
for {
  if k > 3 {
      break
  }
  fmt.Println("可能无限循环")
  k++
}

与 JS 不一样的地方:

  • 不加 ( ),加了会自动删
  • for 接 0 个表达式等价于 JS 的 while(true)
  • for 接 1 个表达式等价于 JS 的 while(condition)
  • for 接 3 个表达式等价于 JS 的 for(初始化, 判断, 后续)

if else

if num := 9; num < 0 {
  fmt.Println(num, "负数")
} else if num < 10 {
  fmt.Println(num, "一位数")
} else {
  fmt.Println(num, "多位数")
}

与 JS 不一样的地方:

  • 推荐不加 ( )
  • if 接一个表达式等价于 JS 的 if(condition)
  • if 接两个表达式没有等价的 JS 常见写法

switch

switch time.Now().Weekday() {
case time.Saturday, time.Sunday:
  fmt.Println("休息日")
default:
  fmt.Println("工作日")
}

// switch case 判断类型
var i any = true
switch t := i.(type) {
case bool:
  fmt.Println("布尔")
case int:
  fmt.Println("整数")
default:
  fmt.Printf("未知类型 %T\n", t)
}

与 JS 不一样的地方:

  • 不推荐加 ( )
  • 不要加 break
  • 一个 case 可以有多个值,用逗号隔开

函数

普通函数

func f1(x, y int) (int, int) {
  return x + y, x * y
}
func f2(x, y int) (sum, product int) {
  sum = x + y
  product = x * y
  return
}
func main() {
  a, b := f1(3, 4)
  c, d := f2(5, 6)
  fmt.Println(a, b, c, d)
}

可变参数函数

func sum(numbers ...int) int {
  total := 0
  for _, n := range numbers {
    total += n
  }
  return total
}
 func main() {
  fmt.Println(sum(1, 2))
  fmt.Println(sum(1, 2, 3))
  nums := []int{1, 2, 3, 4}
  fmt.Println(sum(nums...))
}

匿名函数

func main() {
  sum := func(a, b int) int {
    return a + b
  }
  fmt.Println(sum(1, 2))
}

func(c int) {
  fmt.Println(c)
}(3)

与 JS/TS 不一样的地方:

  • 返回值可以提前定义名字,return 可缩写
  • numbers ...int 表示多个 int 参数组成的数组
  • 立即执行函数不需要 Hack

数据类型

简单值类型

  • 数字(14 种):int32、float64
  • 字符串
  • 布尔

复杂值类型

  • 结构体(struct)
  • 数组(定长):[3]int

引用类型

该类型的变量不直接存放值,而是存放值的地址和其他信息

  • 指针:*int
  • 切片:[]int
  • 哈希表:map[string]int
  • 函数
  • 通道:chan int
  • 接口:interface {}

结构体

type Point struct{ X, Y int }

p1 := Point{1, 2}
fmt.Println(p1.X, p1.Y)

p2 := Point{X: 3, Y: 4}
fmt.Println(p2.X, p2.Y)

把结构体当做参数 1

type Point struct{ X, Y int }
  func modify(p Point) {
    p.X = 42
}
 func main() {
   p1 := Point{1, 2}
   modify(p1)
   fmt.Println(p1)
}

* 与 &

用于类型:

  • var a *int 表示 a 存 int 的地址
  • 此时称 a 为指针

用于值:

  • &b 表示 b 的地址
  • *c 表示指针 c 对应的值

我个人认为,用 var a &int 表示 a 存 int 的地址,更符合语义

把结构体当做参数 2

type Point struct{ X, Y int }
 func modify(p *Point) {
   (*p).X = 42 // 与 p.X = 42 等价,语法糖
}
func main() {
  p1 := Point{1, 2}
  modify(&p1)
  fmt.Println(p1)
}

结构体变字符串

type User struct {
  ID       string `json:"id"`
  UserName string `json:"username"`
  Email    string `json:"email"`
}
func main() {
  u := User{
    ID:       "1",
    UserName: "test",
    Email:    "test@test.com",
  }

  bytes, err := json.Marshal(u)
  if err != nil {
    panic(err)
  }

  fmt.Println(string(bytes))
}

结构体的特点

与 JS 的不同之处:

  • 结构体是值类型,不是引用类型,不能与 JS 的对象进行类比
  • Go 只支持传值,不过可以把地址当做值 modify(&p1)
  • 结构体支持 label,用于各种功能;JS 没有 label

数组(定长)

a := [5]int{1, 2, 3}
a[1] = 20
fmt.Println(a) // [1 20 3 0 0]

与 JS 的不同之处:

  • 数组是值类型,不是引用类型,不能与 JS 的数组进行类比
  • 数组长度是固定的
  • 可以用 len(a) 获取数组的长度,而不是 a.length

指针 Pointer

func zeroValue(ival int)    { ival = 0 }
func zeroPointer(iptr *int) { *iptr = 0 }
func main() {
  i := 1
  iPtr := &i
  fmt.Println("i: ", i)
  fmt.Println("iPtr :", &i)

  zeroValue(i)
  fmt.Println("zeroValue(i), i =", i)

  zeroPointer(iPtr)
  fmt.Println("zeroPointer(&i), i =", i)
}

与 JS 的不同之处:

  • JS 没有指针(无法获取或者打印变量的地址),只有引用(别名)
  • &i 表示 i 的地址,*p 表示指针 p 对应的值
  • 地址一般用十六进制数表示(0xFFFFFFFFF)
  • 理论上可以对指针进行加减操作,但是 Go 默认关闭了此功能(见下页)

unsafe 不安全

vals := []int{10, 20, 30, 40}
start := unsafe.Pointer(&vals[0])
size := unsafe.Sizeof(int(0))

for i := 0; i < len(vals); i++ {
  item := *(*int)(unsafe.Pointer(
    uintptr(start) + size*uintptr(i)
  ))
  fmt.Println(item)
}

切片(可扩容)

a := [3]int{1, 2, 3}
a2 := [...]int{1, 2, 3}

s := []int{1, 2, 3}
s2 := make([]int, 3)
  ta := reflect.TypeOf(a)
  fmt.Println(ta.Kind())
  // array
  ta2 := reflect.TypeOf(a2)
  fmt.Println(ta2.Kind())
  // array
  ts := reflect.TypeOf(s)
  fmt.Println(ts.Kind())
  // slice
  ts2 := reflect.TypeOf(s2)
  fmt.Println(ts2.Kind(), s2)
  // slice [0 0 0]

追加元素

s := make([]string, 3)
s[0] = "a"
s[1] = "b"
s[2] = "c"
fmt.Printf("s: %v, &s: %p\n", s, &s)
s = append(s, "d")
fmt.Printf("s: %v, &s: %p\n", s, &s)
s = append(s, "e", "f")
fmt.Printf("s: %v, &s: %p\n", s, &s)

扩容

var s []bool
for i := 0; i < 100; i++ {
  s = append(s, true)
  fmt.Printf("cap: %v, address: %p\n", cap(s), s)
}

切取

a := [...]int{1, 2, 3, 4, 5}
s1 := a[0:3]
s2 := s1[0:1]
fmt.Println(s1, s2)

切片的特点

  • 每个 slice 都含有一个底层数组、一个长度和一个容量
  • slice 本身只是一个结构体而已
  • 使用 append 向切片追加元素时可能触发扩容,得到新的切片

数据所占字节数

获取

方法一:

var x int
t := reflect.TypeOf(x)
fmt.Println(t, ":", t.Size(), "字节")
// int : 8 字节

方法二:

var x int
fmt.Printf("%T: %d 字节\n", x, unsafe.Sizeof(x))
// int: 8 字节

数据所占字节数表格:

数据类型占用字节数占用位数说明
int864跟操作系统位数有关
bool18
string1664 * 2内容指针 + length
数组size * lensize * len * 8
函数864
指针864
通道864
map864
interface1664 * 2内容指针 + itab 指针
切片2464 * 2数组指针 + length + capacity
结构体要算要算需要考虑内存对齐

更多学习资料

  • Go 官方指南 tour.go-zh.org

  • Go by Example gobyexample-cn.github.io

  • Go 官方文档 go.dev/doc

  • 幼麟实验室 BV1hv411x7we

  • 深入理解 GPM 模型 BV19r4y1w7Nx