最近学习了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是指从panic
或Error
中恢复,让程序可以重新获得控制权,停止终止过程进而恢复正常执行。类似的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