写给前端er的go快速上手指南(上)

前言

最近在刷leetcode的时候,发现有些算法用ts写会超时,但是其他语言不会。然后看来看去觉得go上手应该挺简单的,就顺手学了下基本语法,希望对ts转go的小伙伴有帮助。

hello world

// go中通过包来组织代码,其中main包是入口,是特殊的包
package main   

// import 就不多说了,引入其他的包
import "fmt"  

// 函数声明通过func
func main() {  
//  fmt包控制io的输出,Println类似console.log
    fmt.Println("hello world")   
}

main()

声明变量和赋值

package main

//  声明全局变量
//  go自带零值安全,如果放在ts为undefined,在go中int的零值为0
var num int  

//  声明全局常量
// 通过值自动推导出类型为float64
const pi = 3.1415  

//  var name type = expression
//  基础的定义是这样,其中type和expression可以省略一个,但是不可以都省,因为go可以自动推导

func main() {
    //  短声明变量,只可用在函数内使用
    a := 1
    
    //  可以一次性声明多个变量
    b, c := 2, 3
    
    //  可以多重赋值
    //  ts中的话就是 
    //  let a = 1, b = 2, c = 3; [a,b,c] = [b,c,a]
    a, b, c = b, c, a
    
    fmt.Println(a, b, c)  //  2, 3, 1
}

if/else

package main

func main() {
    a := "a"
    
    //  需要注意的是go中不需要使用括号包裹
    if a == "a" {

    } else if a == "b" {

    } else {

    }

}

循环

go中的循环很简单,只需靠 for 关键字,没有while。

package main

func main() {
    //  1、普通的for循环
    //  同样的,不需要括号包裹
    for i := 0; i < 10; i++ {
       fmt.Println(i)
    }
    
    //  定义一个slice
    //  类似于ts中的数据 let arr:number[] = [10, 20];
    arr := []int{10, 20}
    
    //  2、类似于ts的for-of循环
    for index, item := range arr {
       fmt.Println(index, item)
    }

    //  3、仿while循环
    j := 0
    for true {
       j++
       if j%2 == 0 {
          continue
       }
       fmt.Println(j)

       if j > 5 {
          break
       }

    }


}

函数

package main

//  0、如果你没有返回值,就不需要写返回类型,类似于ts的void
func main() {

}

//  1、需要声明入参的类型和返回值类型
func add(a int, b int) int {
   return a + b
}

func multiple(a, b int) int {
   return a * b
}


//  2、如果入参的类型相同,可以前面的变量可以省略类型声明
func add2(a, b, c int) int {
   return a + b + c
}

//  3、可以一次性返回多个返回值
//  ts中做不到,只能返回数组或者对象再解构
func returnMultiple(a, b int) (int, int) {
   return a, b
}

//  4、函数可以作为入参和返回值
func curryAdd(x int) func(int) int {
   return func(y int) int {
      return add(x, y)
   }
}

//  5、函数可以赋值给变量
//  函数类型的零值是nil,不能直接调用
//  nil类似于ts中的null
func testFunc() {
    fn := add
    fn = multiple
    
    fn(1, 2);
    
    //  6、匿名函数,懂的都懂就不多说了
    foo := func() int {
       return 0
    }

    foo()

}

//  7、变长参数
//  ts中类似的 function fn(..args: any[]) {}

func addMore(args ...int) int {
   total := 0
   for _, arg := range args {
      total += arg
   }
   
   return total
}

字符串

为什么要单独的拿字符串出来讲,是什么go中的字符串类型有点复杂,对unicode,utf8之类的不熟悉的小伙伴可能会懵逼,我就直接贴代码告诉你一些常用的场景怎么用就好了。

package main

import (
    "strings"
    "strconv"
)

func main() {

    s := "hello"
    a := s[0]  // 此时 a 是 byte类型的,也可以称为是uint8类型,这两个类型是等价的
    
    // 0、通过上面那个例子,就知道[]byte和string是等价的,不过他跟ts不一样,不是鸭子类型的语言,所以要转类型
    by := []byte{74}
    fmt.Println(by)   //  74
    fmt.Println(string(by))   //  "J"  
    fmt.Println([]byte("J"))  //  74

    
    // 1、获取字符串的长度使用len函数
    sLength = len(s)  // 5
    
    fmt.Println(a, s[3])  // 返回ASCII码 104 108
    
    //  2、如果我们想获取对应位置的字符,需要转类型
    fmt.Println(string(a), string(s[3]))  // "h" "l"
    
    //  3、如何获取子串,通过指定位置即可
    //  如果你用过python,用法几乎一模一样,不赘述了
    substr := s[1:3]
    
    //  4、如何拼接字符串
    s2 := s + "!"
    
    //  5、如何遍历字符串,我这边只推荐for range
    for index, r := range s {
       //  特别注意,这里的r是rune类型,即int 32类型
       //  `for range` 循环遍历字符串时,会将字符串按 Unicode 字符进行解码
       //  每次迭代会返回当前字符的 Unicode 码点(rune)和对应的索引,避免了处理多字节 Unicode 字符时可能出现的问题
       str = string(r)
    }
    
    //  6、怎么将string和number互换
    //  我们需要用到一个包叫 "strconv",方法的前缀叫ParseXXX
    //  下面那个10是10进制,32是指int32的意思
    fmt.Println(strconv.ParseInt("123", 10, 32))

    
    //  7、处理字符串的方法都在strings包里面,我给大伙列出来一些常用的,方法名跟用法和ts一模一样好吧

    fmt.Println(strings.Index("abcd", "b"))   // 1
    fmt.Println(strings.Split("a b c d ", " ")) // ["a", "b", "c", "d"]
    fmt.Println(strings.Contains("a abcd ", "ab"))  // true
    fmt.Println(strings.Join([]string{"a", "b", "c"}, ","))  //  "a,b,c"
    fmt.Println(strings.ReplaceAll("abcada", "a", "a2"))   //  "a2bca2da2"


}

slice

在go中其实是有数组这个概念的,但是那个数组和ts中的元组(tuple)更接近,对长度和类型都有要求,所以就不讨论了。

而slice才更接近我们使用的数组,不需要理会长度,会自动扩容

slice由三部分组成,指针长度容量

长度是指slice里实际元素的个数

容量是指在底层数组中可以容纳的元素的最大数量

初始化slice

package main

func main() {
    //  0.1,普通声明
    var nums []int{}

    fmt.Println(len(nums), cap(nums))  // 0  0
    
    //  0.2,直接使用字面量
    arr := []int{1,2,3}  // => [1,2,3]
    fmt.Println(len(arr), cap(arr))  // 3  3
    
    //  通过这里可以观察到长度和容量的不同
    arr = arr[0:1]  //  => [1]
    fmt.Println(len(arr), cap(arr))  // 1  3
    
}

添加元素

package main

func main() {
    //  如何添加元素,通过append方法
    arr2 := []int{1,2,3}
    //  一定要注意返回的slice要赋值,他这个不是直接影响引用的
    arr2 = append(arr2, 4, 5) // [1,2,3,4,5]
}

删除元素

package main

func main() {
    //  如何删除元素
    //  比较遗憾的是,我了解到go并没有提供相关的api,只能直接去手动拼接
    //  比如我想除去arr2中的元素4
    arr2 := []int{1,2,3,4,5}
    
    // make你可以理解成new函数,然后[]int说明是int slice类型, 0 说明是长度为0
    arr3 := make([]int, 0)  // []
    arr3 = append(arr3, arr2[0:3]...) //  [1,2,3]
    arr3 = append(arr3, arr2[4:]...)  //  [1,2,3,5]
    fmt.Println(arr3)
    
    //  其实上面的转成ts就是
    //  let arr3 = [];  
    //  arr3 = [...arr3, ...arr2.slice(0, 3)];
    //  arr3 = [...arr3, ...arr2.slice(4)];
    //  如果你问为什么不是直接let arr3 = [...arr2.slice(0,3), ...arr2.slice(4)];
    //  我只能说go不支持append(arr3, arr2[0:3]..., arr3[4:]...),他只能像上面我说的那样一个个展开数组,有点鸡肋
}

附上一个我直接封装的仿splice方法,嘎嘎好用

package main

func Splice[T any](slice []T, start int, deleteCount int, elements ...T) []T {
   // 计算插入的位置和删除的元素
   end := start + deleteCount

   // 创建一个新的切片保存结果
   result := make([]T, 0, len(slice)-deleteCount+len(elements))

   // 把start之前的数据存到result
   result = append(result, slice[:start]...)

   // 把新插入的元素添加到result中
   result = append(result, elements...)

   // 把end之后的元素插入到result中
   result = append(result, slice[end:]...)

   return result
}



//  ======= 下面是测试用例 ========
func TestSplice(t *testing.T) {

   slice := []int{1, 2, 3, 4, 5}

   // 测试删除元素
   result := Splice(slice, 1, 2)
   expected := []int{1, 4, 5}
   if !reflect.DeepEqual(result, expected) {
      t.Errorf("删除元素错误,期望 %v,实际 %v", expected, result)
   }

   // 测试插入元素
   result = Splice(slice, 1, 0, 6, 7)
   expected = []int{1, 6, 7, 2, 3, 4, 5}
   if !reflect.DeepEqual(result, expected) {
      t.Errorf("插入元素错误,期望 %v,实际 %v", expected, result)
   }

   // 测试替换元素
   result = Splice(slice, 1, 2, 6, 7)
   expected = []int{1, 6, 7, 4, 5}
   if !reflect.DeepEqual(result, expected) {
      t.Errorf("替换元素错误,期望 %v,实际 %v", expected, result)
   }

   // 测试删除所有元素
   result = Splice(slice, 0, len(slice))
   expected = []int{}
   if !reflect.DeepEqual(result, expected) {
      t.Errorf("删除所有元素错误,期望 %v,实际 %v", expected, result)
   }
}

拷贝

下面会分享两种拷贝方式,跟 ts 自带的浅拷贝有些许区别

共享引用

你可以理解成他们的共享了了同一个数组,两个切片的修改都会对底层造成影响。简单点理解拷贝的是引用

package main

func copyExample() {

    arr := []int{1, 2, 3}
    arr2 := arr[:2]

    fmt.Println(arr)  //  [1,2,3]
    fmt.Println(arr2) //  [1,2]

    arr[0] = 0
    fmt.Println(arr)  //  [0,2,3]
    fmt.Println(arr2) //  [0,2]

    arr2[0] = 100
    fmt.Println(arr)  //  [100, 2, 3]
    fmt.Println(arr2) //  [100, 2]

}

浅拷贝

这种情况才是更符合我们认知的拷贝 let arr2 = [...arr];,即 对于简单类型来说拷贝的是值

如果拷贝的是引用类型的slice话,对引用类型的修改也是会生效的,这点也符合 ts 那边的规则。

package main

func copyExample() {

    arr := []int{1, 2, 3}

    arr2 := make([]int, len(arr) - 1)
    copy(arr2, arr)   

    fmt.Println(arr)  //  [1,2,3]
    fmt.Println(arr2) //  [1,2]

    arr[0] = 0
    fmt.Println(arr)  //  [0,2,3]
    fmt.Println(arr2) //  [1,2]

    arr2[0] = 100
    fmt.Println(arr)  //  [0, 2, 3]
    fmt.Println(arr2) //  [100, 2]

}

指针

指针这玩意老实说我自己学的也不咋样,被c++的指针劝退ts 虽然明面上没有这玩意,但是大伙思考下面这个例子:


function pushArr(arr: number[], val: number) {
    arr.push(val);

    arr = [0,2];
}

function main() {
    const nums = [0];
    pushArr(nums, 1);
    console.log(nums);  //  [0, 1]
}

main();

我们知道 ts 中参数传递的方式决定了函数内部对参数的操作是否会影响到原始值。

ts 中的对象(包括数组和函数)是按引用传递的。这意味着当你将对象传递给函数时,实际上传递的是对象的引用(内存地址)。因此,在函数内部对传递的对象进行的修改会影响到原始对象,因为它们指向同一个内存地址。

也就是说,我们用 ts的时候,其实也在不经意间在使用类似指针的玩意。

切换到 go 的话,是否取引用,其实更加明显。

package main

import "fmt"

func main() {
    arr := []int{1}
    pushArr(arr)
    fmt.Println(arr)  // [1]
    
    //  &是表示取地址的意思
    pushArr2(&arr)
    fmt.Println(arr)  // [1,2]

}

// 这是一个错误的示范:
func pushArr(arr []int) {
   // 因为在函数内部创建了一个新的切片,因此对新切片的修改不会影响到原始切片。这是因为在函数内部创建的切片是局部变量,它们具有不同的内存地址。
   arr = append(arr, 2)
}

// 这是一个正确的示范:
func pushArr2(arr *[]int) {
   //  *运算符表示解引用的意思,可以直接影响原始切片
   *arr = append(*arr, 2)
}

如果让我自己总结指针的话,大概一句话就能概括完:

变量 存的是值,指针 存的是地址,使用 指针 可以在无须知道变量名字的情况下 间接读取或更新变量的值

Map

散列表,这个我们都很熟了,ts 自带 Map,我们只要对比下是否符合我们所认知的特性即可。

package main

func main() {
    // 0、直接定义 key为string类型 value为int类型的散列表
    nameToRecordMap := map[string]int{
       "xiaoming": 100,
    }
    
    // 1、手中添加,通过下标的方式
    nameToRecordMap["xiaohong"] = 90
    fmt.Println(nameToRecordMap)  //  map[xiaohong:90 xiaoming:100]
    
    //  2、手动删除,使用delete函数
    delete(nameToRecordMap, "xiaoming")
    fmt.Println(nameToRecordMap)  //  map[xiaohong:90]
    
    //  3、想知道某个键是否存在,可以借用第二个参数
    xiaoqiangRecord, ok := nameToRecordMap["xiaoqiang"]
    fmt.Println(xiaoqiangRecord, ok)  //  0  false
    
    //  4、删除不存在的键,不会报错
    delete(nameToRecordMap, "xiaoqiang")
    fmt.Println(nameToRecordMap)  //  map[xiaohong:90]
    
    //  5、操作不存在的键,不会报错,因为零值安全的特性,像这里int会被初始化为0再操作
    nameToRecordMap["xiaodong"] += 1
    fmt.Println(nameToRecordMap)  //  map[xiaodong:1 xiaohong:90]
    
    //  6、循环map,注意,循环的顺序是不固定的,跟ts的map不一样,它会根据插入顺序来作为顺序
    for key, value := range nameToRecordMap {
       fmt.Println(key, value)   // xiaodong 1 ;  xiaohong 90
    }
    
    //  7、获取key的个数
    len(nameToRecordMap)  // 2
}

结构体

结构体就有点像ts中的类,但是我先声明,go 没有类 classgo 不是跟java那种正儿八经的面向对象,这点你先搞清楚。我这里将类是方便大伙理解,毕竟多少都有 oop 的基础,理解起来应该很快。

结构体虽然像类,但是它只声明类的成员,也就是把一些变量组合在一块罢了,没什么东西的。

定义成员

package main

//  定义一个二叉树节点
type Node[T comparable] struct {
   value       T
   
   //  注意到这里我们用了指针
   //  因为go不允许结构体类型S,它自身的成员也有类型S,但是允许定义类型S的指针
   left, right *Node[T]
}

//  我们之前提到过说,go不是oop的,所以new的用法和ts相去甚远
//  所以我们遵从习惯会写个newXX函数来创建实例
func newNode[T comparable](value T) Node[T] {
   return Node[T]{
      value: value,
   }
}

func printNode[T comparable](n Node[T]) {

   var zeroValue T
   
   //  可能你会好奇为什么要定义个zeroValue,而不是nil
   //  因为 `T` 是一个泛型类型,它不能直接和 `nil` 进行比较
   //  所以我们借助go的零值安全的特性自己对比
   if n.value != zeroValue {
      fmt.Println(n.value)
   }
   if n.left != nil {
      printNode(*n.left)
   }
   if n.right != nil {
      printNode(*n.right)
   }
}

func main() {

    root := newNode(-1)

    leftNode := newNode(2)
    
    // 结构体通过.来访问成员
    root.left = &leftNode

    printNode(root)
}

定义方法

可能你留意到了,上面的 printNode 是专门用于打印 Node 类型的,每次都要把 root 传进去好麻烦。

是的,go 专门有个东西叫接收器,用来处理结构体专用的方法。 也就是我们类对应的方法。

package main

type Node[T comparable] struct {
   value       T
   left, right *Node[T]
}

func newNode[T comparable](value T) Node[T] {
   return Node[T]{
      value: value,
   }
}

//  注意到我们的方法前面多了一个 (n Node[T]) 的参数
//  这个参数会把函数绑定到该参数对应的类型上
//  这个参数n你可以理解成this即可,但是不要起名为this,go官方不推荐,他推荐是类型的缩写比如Node 缩写成 n

//  习惯上,一般会定义为指针,避免对象太大的时候进行复制,通过地址传递会比较合适一点
func (n *Node[T]) print() {
   var zeroValue T

   if n.value != zeroValue {
      fmt.Println(n.value)
   }
   if n.left != nil {
      printNode(*n.left)
   }
   if n.right != nil {
      printNode(*n.right)
   }
}

func main() {
   root := newNode(-1)

   leftNode := newNode(2)

   root.left = &leftNode

   //printNode(root)
   root.print()
}

结语

当你学会了上面的语法,其实你已经可以去leetcode用go开刷了,还有一些什么类型,并发,reflect之类的,其实并不影响你去写leetcode。主要问题是我自己也用的还不够深入

然后立个flag,今年把 go 学完,把下篇肝出来。

最后祝各位龙年快乐,事业有成~

原文链接:https://juejin.cn/post/7332735436325797942 作者:Yuin316

(0)
上一篇 2024年2月10日 上午10:16
下一篇 2024年2月10日 上午10:27

相关推荐

发表回复

登录后才能评论