VSCode配置golang用空格缩进替代tab

2,229 阅读4分钟

前言

  gofmt 格式化不支持空格替代tab缩进。最早gofmt是有选项支持这个功能的,但在2014年6月开始,这个选项就被取消了。详见 Gofmt No Longer Allows Spaces. Tabs Only (code.google.com)

现在我们去看官方的gofmt文档,可以很明显看到只使用tab来缩进。

Gofmt formats Go programs. It uses tabs for indentation and blanks for alignment. Alignment assumes that an editor is using a fixed-width font.

  正因为如此,在vscode设置里面针对golang的"editor.insertSpaces": true配置是无效,不起作用的。

先前解决方案

使用goformat

Alternative to gofmt with configurable formatting style (indentation etc.) 使用goformat是可以做到使用space来替代tab做为缩进。 可惜这个项目不再维护,已经好几年没有更新了。对于目前go1.19的泛型,goformat不支持泛型的格式化,这个不是我们所希望的。所以,目前来说,只能放弃goformat

重新修改gofmt代码

  gofmt源码

  const (
  tabWidth    = 8
  printerMode = printer.UseSpaces | printer.TabIndent | printerNormalizeNumbers
  )
  ///将上面的改成如下即可
  const (
  tabWidth    = 2 ///你可以改成你需要的空格数
  printerMode = printer.UseSpaces
  )

然后自己编译,进行替换本地的 /usr/local/bin/gofmt即可. 这样的问题是,后续维护起来很麻烦,每次都得自己去编译。 同时,在vscode里面如果使用golps就会忽略gofmt. 不过,我们可以使用Go:Format Tool中选择custom,然后在Go: Alternate Tools手工进行配置新的format工具即可.见下面的做法。

目前比较好的解决方案

目前gofmt的格式化,基本我们都想采用,除了强制使用tab来进行缩进。 看到vscode的extension: golang.go 中,可以自定义格式化工具,此时我们就可以使用脚本来达到我们的目的. extension setting中选择@ext:golang.go format过滤, Go:Format Tool中选择custom,然后在Go: Alternate Tools手工进行配置新的format工具即可.

新建go-fmt.sh脚本

我们将文件放在/works/UseTools/vscode-settings/go-fmt.sh

chmod a+x /works/UseTools/vscode-settings/go-fmt.sh

go-fmt.sh内容如下.

#!/bin/bash
## 此处只是为了看效果
echo /usr/local/go/bin/gofmt $*
/usr/local/go/bin/gofmt $*

配置Go: Alternate Tools.

vim ~/.config/Code/User/settings.json 确保此json包含下面的key

{
  ...
  "go.formatTool": "custom",
  "go.alternateTools": {
    "customFormatter": "/works/UseTools/vscode-settings/go-fmt.sh"
  }
}

为了看效果,我们上面将go-fmt.sh内容为 echo /usr/local/go/bin/gofmt $*,

然后一旦保存,就发现编辑的最上面多了一行."/usr/local/go/bin/gofmt"

到此,恭喜你,配置自定义的golang的format脚本运行正确。

我们也发现,这个脚本无论输出什么,最终都会到vscode的编辑内容中。 那最后就是我们使用shell脚本来替换开头的tab为空格。

完善go-fmt.sh脚本

上面我们说到,只需要在/works/UseTools/vscode-settings/go-fmt.sh脚本里面,将每行开头的tab字符换成我们需要的空格即可. 此时,我们用上的就是sed这个工具。 sed替换模式.

#!/bin/bash
  ## 此处只是为了看效果
  ## echo /usr/local/go/bin/gofmt $*
  ## 这里可以改成你需要的空格数
  tab_space="  " 
  /usr/local/go/bin/gofmt $* | sed -E "s/^\t+/${tab_space}/"

你保存一下go文件,就会发现,和你想象的不一样。缩进没有达到我们的目的,没办法把相同数量的tab字符替换成相同数量的tabspace。除非你正则表达式改成"s/\t/{tab_space}。 除非你正则表达式改成 "s/\t/{tab_space}/",但这个是将一行里面所有的tab都换成空格,不是我们希望的开头的tab字符。

那我们有没办法,将每行最开始的tab字符中最后一个tab字符替换${tab_space}? 这个是可以达到的.

我们在terminal中开始尝试.

echo -e "\t\t\t\t\t\tzdddddfsdfs" > ~/test.txt
sed  -E -e "s/^(\s*)\t([^\t])/\1SPACE\2/"    ~/test.txt 

此时输出" SPACEzdddddfsdfs". 如果要将多个tab进行替换,可以使用多个 -e来执行. 比如:

sed -E -e "s/^(\s*)\t([^\t])/\1SPACE\2/" -e "s/^(\s*)\t([^\t])/\1SPACE\2/" -e "s/^(\s*)\t([^\t])/\1SPACE\2/" ~/test.txt

此时输出:" SPACESPACESPACEzdddddfsdfs" 确实是替换了三个spaces.

如果是任何多个tab,难道我们要加更多个-e的条件吗? 并且每个条件都是无条件执行的,也就是就算没有那边多tab字符,也要执行这么多的条件,效率太差。

其实,sed里面有个t指令进行分支跳转。

echo -e "\t\t\t\t\t\tzdddddfsdfs" > ~/test.txt
tab_replace="[SPACE]"

sed -E -e "
 :loop; s/^(\s*)\t([^\t])/\1${tab_replace}\2/; 
 t loop; "    ~/test.txt 

上面输出: "[SPACE][SPACE][SPACE][SPACE][SPACE][SPACE]zdddddfsdfs"

这样终于达到我们的目的了,效率也不错,每行有多少个tab开头的,就会执行几次替换。

go-fmt.sh最终版本

#!/bin/bash

###这里可以替换成你需要的空格数.
tab_replace="  "

command_out=`/usr/local/go/bin/gofmt $*`
if [ "$?" -ne "0" ]; then
  exit 1
fi
echo "$command_out" | sed -E "
:loop; s/^(\s*)\t([^\t])/\1${tab_replace}\2/;
 t loop; "


  上面我们增加了判断gofmt是否失败,如果失败,直接exit 1,此时vscode的内容不变。

  如果不增加此判断,一旦go文件的语法不对,格式化失败,就会将文件内容替换成空字符串。这也不是我们希望看到的。

小结

  gofmt为了统一编码风格,将先前的可以使用space的选项关闭了。但我们为了所有语言风格的缩进都保持一致,所以,选择了将Tab替换成2个space的缩进风格。先前有goformat项目支持这个选项,无奈此项目没更新,无法支持最新的泛型格式化。还好,可以使用自定义的脚本来进行格式化。我们只是将gofmt的输出再次进行处理。既然是每行处理文本,首先想到的就是sed。在不断的尝试中,我们可以再次感受到sed的强大,最终终于实现了我们需要的替换格式。最后一个问题,就是要处理格式化错误的情况,只需要增加一个判断条件即可。

  我们终于实现我们需要的功能,同时也没有增加后续的维护负担。后面只需要更新官方版本的gofmt文件即可,我们脚本是不用变化的。