你这只土拔鼠呀——前端眼中的golang

1,466 阅读7分钟

最近学习了golang,并在业务上开始使用。我们来用一名只会js/ts的前端视角,来快速熟悉一下go语言,10几分钟光速入门。简单的语法层面的不会多说,只从一些共同点突出点来出发。更深入的语言特性,自行根据文档去探索吧

多返回值

go一个函数可以返回多个值:

func A() (int bool) {
	return 1, false
}

一般用于返回一个操作的结果与错误:

func A(a int) (error int) {
	var (
    	err error
        val int
    )
    val, err = getUserId(a)
    if err != nil {
    	return err, 0
    }
    return nil, err
}

// 使用
err, val := A(100)

js里面的promise场景也有类似的用法,个人也有这种喜好:

export function awaitToJs<T, U = Error>(promise: Promise<T>): Promise<[U | null, T | undefined]> {
  return promise
    .then<[null, T]>((data: T) => [null, data])
    .catch<[U, undefined]>((err: U) => [err, undefined]);
}

// 使用
(async () => {
	const [err, data] = await getSomeInfo()
})()

方法与函数【重点】

go里面没有this,如何实现类似的效果?那就是方法了。go里面的方法,和函数的区别是,函数名字前面多了receiver。go的面向对象,其实也是如此。go里面对标js的plain object的,就是struct,而struct里面不能写函数,使用receiver来实现

// 比如我们定义一个类似js的map的功能
func (this Array0) ArrayMap(cb func(interface{}, int) interface{}) []interface{} {
	var ret []interface{}
	for i, v := range this {
		ret = append(ret, cb(v, i))
	}
	return ret
}

// 使用的时候test.ArrayMap来使用
func main() {
	test := Array0{{a: 10}, {a: 3}}
	fmt.Println(test.ArrayMap(func(item interface{}, index int) interface{} {
		return item.(struct{ a int }).a + index
	}))
}

// 定义一个Math类型,是一个结构体
type Math struct {
	E float64
}

// Math类型下有一个max方法
func (this *Math) max(values ...int) int {
	var ret int
	for _, v := range values {
		if v > ret {
			ret = v
		}
	}
	return ret
}
// 使用
func main() {
	var test Math
	test.E = 2.718281828459045
	fmt.Println(test.max(1, 2, 3, 4, 6)) // 6
}

interface

go的interface里面是一些方法的类型集合,它们是抽象的没有被实现的。接口里也不能包含变量。你如果给一个变量声明了interface类型,那么你要去实现它:

type GetInfo interface {
	GetName() string
	GetAge() int
}

type People struct {
	name string
	age  int
}

// 实现GetInfo
func (people *People) GetName() string {
	return people.name
}

func (people *People) GetAge() int {
	return people.age
}

func main() {
	var people GetInfo
	instance := new(People) // 实例化
	instance.name = "lhyt"
	instance.age = 100
	people = instance // 实现了GetInfo的instance
	fmt.Println(people.GetAge(), people.GetName())
}

但是一般业务代码中,用空interface较多。空interface类似ts的any的效果。

any => 空interface【重点】

基本介绍

空interface不包含任何方法,任何其他类型都实现了空接口,因此具有ts的any的效果。前面代码也看见了,有空interface。使用的时候如果想取值(你知道那是一个结构体/一个int),那么需要断言。如果断言失败,将会导致程序错误

回头看看这段代码:

fmt.Println(test.ArrayMap(func(item interface{}, index int) interface{} {
    return item.(struct{ a int }).a + index
}))

item是空interface类型(any),我们事先知道是一个struct,因此断言它就是struct{ a int },语法为anyValue.(someType)表示空interface类型的anyValue此处运行时类型为someType。当我们断言错误的时候:

fmt.Println(test.ArrayMap(func(item interface{}, index int) interface{} {
    return item.(int) + index
}))
// panic: interface conversion: interface {} is struct { a int }, not int

动态类型

那么问题来了,如果有真的多个类型存在的可能性呢?比如某个比较坑的第三方库,有时候返回个int有时候返回个float的。面对这种情况,go还是有办法的:

// 第三方库给的id有时候是string有时候是float64
func getIntIdFromStringOrFloat64(id interface{}) int {
	if _, ok := id.(string); ok {
		val, err := strconv.Atoi(id.(string)) // strconv很常用,做string和其他互转
		if err == nil {
			return val
		}
		panic(err)
	}
	return int(id.(float64))
}

这段表示,如果id断言为string成功,那么string转为int;如果断言到的是float64,那么float64转为int。经过我们的兼容,就不怕他们乱改类型了,我们的服务还是不会出事

补充一点,逗号ok模式是go里面很常用的,一般会接if一起用。表示做一些事情,返回成功或者错误,并根据有没有成功来做一些事情:

if _, ok := id.(string); ok {}

type-switch

如果有很多种类型,当然不会是像上面的写法那样子一层层if,go有专门的特殊的switch支持这种需求。type也可以是其他复杂的类型,如struct、map、slice、channel等

func getIntIdFromStringOrFloat64(id interface{}) int {
	switch id.(type) {
	case string:
		val, err := strconv.Atoi(id.(string))
		if err == nil {
			return val
		}
		panic(err)
	case float64:
		return int(id.(float64))
	}
	return id.(int)
}

业务代码中实现动态调用

比如有一个rpc客户端映射表,通过key去获取然后进行调用,那么大概会这样做:

type Rpc interface {
	Request(c *gin.Context, v ...interface{}) interface{}
}

func SomeService(c *gin.Context, key string) {
// ClientMap是map[string]Rpc类型
	RpcClients := ClientMap[key]
    if RpcClients == nil {
    	return
    }
    Params := map[string]interface{}{
		"id": 666,
	}
    RpcClients.Request(c, Params)
}

对象 => 结构体/映射

go中的结构体/映射对标js的plain object了。但结构体和映射有一些不一样,结构体是需要提前知道且确定好每一个字段,做不到动态;而map就可以做到动态增减key-value对。取值的时候,结构体可以通过.,而map需要["someKey"]


type Hello struct {
	a int
	b string
}

type World map[string]string
type World1 map[bool]string

// 结构体
test1 := Hello{1, "hey"}
// map,key为string
test2 := World{"a": "1", "b": "2"}
// key为bool
testc := World1{false: "1", true: "2"}
// 注意取值方式区别
fmt.Println(test1.a, test2["a"], testc[false])

结构体做不到后续新增key了,map却可以,map取不到的话返回nil 。类似的,js的数组对标go的切片/数组,go数组也是需要提前知道有什么元素,而slice类似map一样,可以动态维护元素

try-catch => panic/recover

js中使用try-catch捕获错误,go的话,类型上的错误在编译阶段即可抛出,剩下的就是那些动态的、运行时报错了。运行时报错在go里面叫panic——程序恐慌

func exec(g func()) {
    defer func() {
        if err := recover(); err != nil {
        	fmt.Println("错误: %v", err)
        }
    }()
    g()
}

当运行时出错,将会panic。recover是指从panicError中恢复,让程序可以重新获得控制权,停止终止过程进而恢复正常执行。类似的js代码:

function exec(f) {
	try {
    	f()
   	} catch (e) {
    	console.log('错误:', e)  
    }
}

toString类型转换 => stringer

go也有类似js的类型转换toString。js中默认的把对象转字符串是[object Object],数组转字符串是隐式调用join,或者可以手动修改Symbol.toPrimitive方法。go里面类似手动重写toString的方式就是stringer了。fmt包自带

type Stringer interface {
    String() string
}

在fmt打印的时候打印字符串,如果打印的是struct,则会走系统默认打印出{value1, value2}集合。我们想自定义这个过程,需要自己去实现String方法:

type Person struct {
	Name string `json:"name"`
	Age  int    `json:"年龄"`
}

func (p Person) String() string {
	data, err := json.Marshal(p)
	if err == nil {
		return string(data)
	}
	panic(err)
}

func main(){
	test := Person{"lhyt", 100}
	fmt.Println(test)
    // {"name":"lhyt","年龄":100}
    // 不修改的情况是{lhyt, 100}
}

此外,一个结构体里面后面接json:"name"表示在json序列化的时候,key是这个。这是go的结构体标签,你可以理解为一些字段描述信息,运行时可以通过反射读取到这些信息,做一些对应的逻辑

reflect

go的运行时动态相关的逻辑很多就靠反射来实现了。比如js的object.keys的go的实现:


type World map[string]string

test2 := World{"a": "1", "b": "2"}
v := reflect.ValueOf(test2)
fmt.Println(v.MapKeys(), "<<<")
// [a b]

知道了keys,那么object.values/entries都可以实现了

还可以做很多很有趣的动态的事情,看看v它的提示有啥:

最后

go的特色和深入这里不多说了,比如协程,有兴趣的移步这里。当然还有一些前端开发的共同点,可以从前端的视角去快速熟悉,比如go的hmr——air,go的包管理——go mod等。单机玩go的话,可以装个air热更新跑起来即可,包括我现在单机测试也是这样

一大批文档:learnku.com/go/docs