不要用动态语言的方式去看待Golang
背景
当今时代对一门语言的市场需求:快速部署、并发、可扩展、社区活跃、低成本维护和经济高效。云原生时代,不少PHP开发转型Golang开发(一般说到这都会掰扯下两者区别,我这里就不做阐述了)。虽然转型更换了语言,但是在开中还是能发现很多PHP的影子,一些语法方面的就不说了,编译的时候会报错提示。最大的问题是以前习惯用PHP的数组,现在到了Golang强类型语言这里不习惯先定义好类型再使用。还有的小伙伴以前没有使用抛异常的习惯,经常在使用约定好的值做为错误依据来做返回数据。本文不是说针对PHP开发,而是对于动态语言转Golang可能出现的一些编码习惯问题提出一些建议。
一、“尽量”使用结构体代替Map
有的小伙伴经常用map或者切片map来存储一些固定的信息,如配置信息等,有多个的话就存放在切片map中。
以下例子只是为了突出map[string]interface{}的问题,而不是说使用的时候一定是配置信息才会出现这种问题。例如:
var configMap = []map[string]string{
{
"svc": "user",
"type": "rpc",
"limit": "100",
},
}
后面使用时再通过键去取值,虽然这样做能实现,但会发现Go里面因为是强类型,你在用上述例子map里面的数字值时还得做类型转换。很多人会说把map的类型换成map[string]interface{}不就可以了,那你可以动手实践下看你用的时候能不能类型断言。
// 类型断言
var configMap = map[string]interface{}{
"svc": "user",
"type": "rpc",
"limit": "100",
}
fmt.Println(configMap)
if bar, ok := configMap["limit"].(int64); ok {
fmt.Println(bar)
} else {
fmt.Println(ok)
}
这其实是一个思维的转变,在Go这种强类型语言里需要养成先定义结构体类型后使用的习惯,比如上面的例子可以先定义一个结构体。
type Config struct {
Svc string
Type string
limit int64
}
var configs = []*Config {
{
Svc: "user",
Type: "rpc",
Limit: 100,
},
// ...
}
这样就能避免还得把Limit转成整型的问题了,而且编辑器还会有类型提示,不需要你去记map里的键名,可以避免把键名写错导致BUG。
除了上面说的还有的小伙伴喜欢在返回值里返回Map,这种写法除了可能会导致上面说的问题,也会让其他人使用起来不方便。
比如:我现在要调用你的方法,我还得进去看你的代码,看看你的返回值map到底有哪些字段,字段是什么类型,是否要进行类型断言,要是字段多的话岂不是全部要整一遍。
我们写Go的时候,其实map的使用要比在PHP里使用数组少,很多时候都是用结构体或者切片结构体。
对于那种key为ID,值为数据map的这种映射,也是改成Key为ID,值为自己定义好的结构体类型才对。
比如下面这个map类型变量,它的Key是ID,值的类型是我们上面定义的Config结构体:
var ConfigMap = map[int64]*Config {
1: {
Svc: "user",
Type: "rpc",
Limit: 100,
},
// ...
}
总之记住一句话:“先定义类型再使用”
二、“零值”问题
在Go中,没有初始化的变量默认初始值为其类型的零值,需要注意的是slice,map,chan和*T指针类型对应的零值是nil。
这些类型的变量在没初始化前是无法直接使用的,会导致运行时错误。
常见的两种panic如下:
// 第一种
var configMap map[string]string
configMap["limit"] = "100" // panic: assignment to entry in nil map
// 第二种
type User struct {
Name string
}
var user *User
fmt.Printf("%v\n", user)
fmt.Println(user.Name) // panic: runtime error: invalid memory address or nil pointer dereference
第一个错误是因为对一个未初始化的map进行赋值导致的,所以使用map类型的变量前要记得用make对变量进行初始化。
如果是切片slice的话使用append向空切片追加新元素就可以了,因为append会生成新的切片,在底层为切片分配底层数组。
第二个错误是因为对nil指针进行引用导致的,指针的零值nil与*T{}是不相等的。所以指针类型的变量在使用前要先new进行初始化。
说到这里就想到一点也是会影响对接端的不便:
举个例子(列表接口),前端小伙伴们不喜欢接口返回值的字段有数据的时候是个数组,没数据的时候是null。
原因是切片没有初始化导致的,如果数据库查不到数据,那么代码里就执行不到给切片追加数据的那步操作,所以就会出现这个问题。这是一个保持接口字段类型一致性的一个重要的细节,不要给定的接口文档是数组类型,返回的又是其他类型。
三、使用error返回方法错误
在使用PHP时,接口错误一般是通过抛出异常,有的是通过返回0,false等来表示错误(不推荐这种做法)
// 第一种,抛异常
public function dataUpdateProvider(UpdateDto $updateDto): ?UpdateDto
{
try {
// 数据库操作
// ...
} catch (DbException $e) {
throw new SystemException('Error Msg', '500' , $e);
}
return $updateDto;
}
// 第二种,返回约定值去表示错误
public function dataUpdateProvider(UpdateDto $updateDto): bool
{
// 数据库操作
$res = '......';
if (!$res) {
return false;
}
return true;
}
在Go中虽然没有异常机制,但是可以让方法返回error表明遇到的错误。大多数情况下我们的方法都需要返回error的,除非确定方法不需要返回error。
所以在定义方法时要明确返回数据和error的区别,两种返回值的职责范围不一样。要通过方法返回的error是否为空,而不是返回数据为0或者false等去判断方法是否执行成功。
function DataUpdateProvider(UpdateDto requests.UpdateDto) error {
// 数据库操作
res, err := "....."
if err != nil {
return err
}
return nil
}
可以看下这篇文章,对日常开发中Golang错误处理的个人建议
四、不要使用map[string]interface{}做参数
写过PHP的小伙伴都知道,PHP里的数组几乎是万能的,还能保证数组里面key的遍历顺序,这点是很多语言的map类型办不到的事情。
很多刚从PHP开发转到用Go开发的小伙伴还是带着在PHP里使用数组参数的习惯,在Go语言里最像PHP数组的可能就是map[string]interface{}了,这种还是属于典型的动态语言编程的思维
在使用Go的时候,针对比较复杂的一类事物的参数,我们也是应该先定义好结构体,然后使用结构体指针或者切片结构体指针作为参数。
尽量不使用map[string]interface{}这种类型的参数,即使是强大的IDE也没法提示这些参数的内部结构,这让其他人使用这个代码时就会十分痛苦,还得先看看方法的具体实现代码里用到了哪些字段。(map[string]interface{}作为返回值的问题在上面第一点有说明)
下面这两个方法的对比,一目了然:
type UserRequest struct{
Name string
Age int
}
func AddUser(params *UserRequest) error {
// 简单模拟业务逻辑
// ...
// 数据库操作
CreateUser(params.Name, params.Age)
// ...
}
func FindUser(params map[string]interface{}) error {
// 简单模拟业务逻辑
// ...
// 字段类型转换或者类型断言
// 数据库操作
GetUser(input["name"], input["age"])
// ...
}
一般业务开发中,要保存一些额外信息到数据表中才用到map[string]interface{}类型。写表之前把额外信息这部分数据转成JSON格式再写入。当然还是得看使用场景,这里只是一些在编码习惯上的建议。
我是六涛sheliutao,文章编写总结不易,转载注明出处,喜欢本篇文章的小伙伴欢迎点赞、关注,有问题可以评论区留言或者私信我,相互交流!!!