UEFI是什么,有什么用?如何使用UEFI方式安装系统?

UEFI直白的讲是一种系统启动方式,但是这样讲实际上是不严格的。UEFI的全称是统一可扩展固件接口(Unified Extensible Firmware Interface),这就表示实际上它是一系列统一的接口标准,但是我们通常更关注它对系统引导的影响,我们通常拿它和BIOS方式做对比,本片文章也主要讨论UEFI对于启动的影响。

我们在配置启动选项的时候通常看到的是UEFI和Legacy两种方式,Legacy也就是传统BIOS对应的启动方式。说是启动方式,实际上说成“发现启动的方式”更加好理解。因为在启动过程中UEFI和Legacy主要功能是发现磁盘上可以启动的选项。

现在我们就来看看Legacy方式和UEFI方式分别是怎么发现启动的。在早期,系统的启动项是写在主引导记录也就是MBR中的,这个记录只能写在磁盘开头的446字节中,很明显主引导记录的容量和位置都有很大的局限性,并且磁盘的分区信息也是写在主引导记录中的,主引导记录的容量限制就更加明显了。也正是因为主引导记录的体积限制,我们只能用它创建4个分区的描述,这也是广为流传的一个硬盘只能创建4个主分区的原因。因为主引导记录的特点,电脑在开机的时候只需要完成一定工作后读取主引导记录并按照主引导记录的描述继续读取需要运行的程序就可以了,但是如果主引导记录损坏,那整个磁盘就都会遇到故障,这也是使用Legacy经常遇到系统无法启动的原因。

而对应UEFI的引导方式不再继续使用MBR分区方式,而是采用GPT分区表。GPT分区表可以带来更多的分区和更大的分区容量。虽然很多人用不上更多的分区和更大的磁盘,但我依然推荐使用GPT分区方式。实际上GPT分区表里是有MBR的,但是包含MBR记录的目的仅仅是告诉那些只支持MBR的电脑不要乱动我的分区表。在使用GPT分区表的情况下,机器不能仅仅通过读取第一个扇区就确定可以从哪里启动了,但是不用担心因为UEFI比BIOS更加强大,它不仅能轻松发现哪些分区可以启动,甚至可以直接挂载分区进行文件管理。实际上UEFI发现启动选项正式依靠文件管理,UEFI可以直接读取各个分区的文件搜索可以启动的文件,如果你的机器是使用UEFI方式引导的,你通常可以在启动分区中发现一个叫做EFI的文件夹,这里面就是引导要用到的程序。如果你确定你的电脑是UEFI方式引导的,但是却看不到EFI文件,那是因为有一个专门的EFI分区,并且这个分区通常是没有设置卷标或者挂载出来。使用这个方式发现引导的好处很明显,首先避免的主引导记录的位置和容量的限制,所有分区的任意位置都可以存储引导信息和引导程序,其次,如果你的硬盘头部的扇区损坏了,你完全可以不用他们,只需要在分区的时候跳过它们,不管你的分区在哪个位置,都可以通过写入EFI文件配置启动。其次得益于UEFI的文件管理功能,有些高级的主板甚至支持直接挂载分区对分区中的文件进行编辑,这样在发生引导错误的时候我们就可以直接通过覆写文件的方式进行修复,实际上Windows10上的自动修复修复功能就是这样实现的(但是确实更加复杂),相比传统的编辑磁盘头部的主引导记录,这种方式真避免了很多麻烦。

那么如何设置UEFI启动呢?配置UEFI启动需要在安装系统的时候配置。首先你要确定你的机器支持UEFI模式启动,只要你的机器不是古董机通常都可以使用UEFI方式启动,你可以在BIOS(你注意到我这里使用BIOS指代UEFI和传统BIOS,实际上UEFI就是一种BIOS)选项中查看引导选项,通常有Legacy、UEFI和兼容3种模式,如果你确定你不在需要使用Legacy启动方式,你也可以直接设置为仅UEFI模式。

然后需要确定你要安装的操作系统是否支持UEFI模式启动。方法很简单,使用虚拟磁盘挂载安装镜像或者使用解压缩软件打开镜像,查看镜像中有没有EFI文件夹。如果存在EFI文件夹,那就说明这个镜像支持以UEFI的方式启动。

下面我们就来创建一个可以使用UEFI模式启动的U盘,使用Legacy方式启动的启动U盘通常需要使用烧录软件模拟成CD-ROM进行创建,相比之下,创建UEFI模式启动的U盘就简单多了。首先将U盘格式化为FAT32格式,如果你的U盘已经是FAT32格式就不需要再进行格式化了,然后使用虚拟光驱挂载镜像,将所有文件拷贝的U盘中,或者直接使用解压缩工具解压到U盘中。得益于UEFI模式使用文件管理发现启动项的方式,只要使用主板上的UEFI程序支持的文件系统并将所有安装文件拷贝到分区中就可以创建一个引导分区。同理,使用任何主板支持的存储介质都可以创建一个安装盘。

值得之前没有使用UEFI安装系统的同学注意,在使用UEFI安装系统的时候要选择有UEFI前缀的引导项,如果你找不到这个引导项,需要到BIOS中确认是否支持或者打开UEFI引导方式。

希望通过上文,你可以明白UEFI并没有多么复杂,相比之下使用UEFI来配置计算机更加简单,并且可以带来更多的好处。甚至有些厂商会在UEFI中嵌入反病毒程序,这也都得益于UEFI文件管理等新特点。至于BIOS和UEFI在运行中的区别,我们可以理解成传统BIOS只知道读取磁盘的第一条记录,然后按照他的描述去启动,而UEFI不一样,UEFI清楚的知道自己启动的是什么以及如何启动它,甚至还可以对启动项目安排一下,总之更高级就是了。

Golang 错误声明的几种方式

golang的错误处理机制非常特别,golang将错误声明为一种接口,并且可以作为参数传递。这种机制是为了让程序员重视错误处理,同时避免胡乱引发异常的情况。

在使用golang返回错误的时候,我通常使用以下几种方式:

1.直接返回声明的错误:

示例:

return errors.New("这是一个错误!")

return fmt.Errorf("这是一个错误!")

这种方式使用起来是最快捷的,同时也是我最不推荐的。因为这种方式上级函数拿到错误之后很难分析到底发生了什么错误,即使可以通过比对字符串进行分析,也是既低效又不稳定的方法。

2.返回声明的错误变量

示例:

var ErrMyErr  error = errors.New("这是一个错误!")

fun returnErr() err {
    return ErrMyErr
}

这种方式是我最推荐的方式,因为这是声明最方便,同时也是判断最简单的方案。上级函数拿到错误之后只需要简单的使用 == 运算就可以判断错误类型。

if err == ErrMyErr {
    // 进行错误处理
}

3.声明一种错误结构体:

示例:

type ErrMyErr struct {
    ErrString string
}

func (e ErrMyErr) Error() string {
    return fmt.Sprintf("字符串%s发生了错误", e.ErrString)
}

// 返回错误的函数
func returnErr()  error {
    return ErrMyErr{"发生错误的字符串"}
}

如果你的错误比较简单,只需要返回一个错误变量就可以让上级函数轻松的判断具体发生了什么错误,但是如果你的错误比较复杂,有一些参数需要跟随错误上报,那么简单的变量就不能满足这种情况了,我们需要先声明一种错误结构体,表示一类错误,然后声明独特的错误实例。这种情况适用于这一类错误每次发生的情况都会有一些小的区别的时候,比如查询数据库中的行的时候,某一列不存在,需要通过返回错误告知。

这种情况下判断错误类型也很简单:

if _, ok := err.(ErrMyErr); ok {
    // 错误处理
}

4.声明一种错误结构体,同时通过属性来区别错误。

示例:

type ErrMyErr struct {
    ErrString string
    ErrType uint
}

func (e ErrMyErr) Error() string {
    return fmt.Sprintf("字符串%s发生了错误", e.ErrString)
}

const (
    ErrTypezero    uint =  iota
    ErrTypeone
    //....
)

// 返回错误的函数
func returnErr() error {
    return ErrMyErr{"发生错误的字符串", ErrTypezero}
}

这种返回错误的方式就比较复杂了,幸亏它的适用场景也很少,大多数情况下,最好不要使用这种返回错误的方式。这种错误的定义方式适用于那些存在很多种类的错误,同时又是一大类错误的时候,有的时候我们只需要知道发生的错误是不是这一个大类,有的时候我们又希望知道具体发生的错误是哪个小类。当然我们也可以继续使用上一种错误声明方式,但是可能又要面临第一中错误声明方式遇到的比对字符串的情况。所以不如直接在结构体中定义一个属性用于说明错误的小类。

判断方法:

if _, ok := err.(ErrMyErr); ok {
    // 错误处理
}
if e, ok := err.(ErrMyErr); ok && e.ErrType == ErrTypezero {
    // 错误处理
}

5.使用普通的变量代表错误

以上几种方式都是围绕golang的错误处理机制实现的,但是其实仔细一想,这种错误处理机制并不受限于golang的语法,只是一种大家共同认可的方式,如果你自己简单的声明一个变量代表一种错误,同样可以实现返回错误。但是为了代码的兼容性,我也完全不赞成在错误处理的时候使用这种方式,除非你的错误非常小,或者算不上什么错误,别人根本不会在乎,也完全不需要实现Error()函数。

示例:

func returnErr() int {
    return 0
}

这几种方式是我能想到的几种声明错误的方式,我推荐先在包中定义错误变量,然后直接返回错误变量,这种方式几乎适用于绝大多数的情况,而且处理起来非常方便稳定。为了代码的兼容性和可维护性,希望看到这篇文章的人也都能养成良好的错误声明的习惯。

6.最近我在阅读别人的代码时发现了另外一种定义错误的方式,就是把字符串重定义为错误类型,然后直接将错误描述的字符串强制类型转换为错误类型返回。

type Err String
func (e Err) Error() string{return string(e)}
func returnErr() error {
    return Err{"这里发生了一个错误"}
}

你也许会觉得,这和第一种方式没有什么区别,但是实际上相比第一种方式,这种方式多了一个好处,在使用时可以判断这个错误是否你调用的模块发生的错误。通常,我们调用的模块都会有依赖模块,比如我们用的orm会调用数据库的模块,当数据库的模块发生错误的时候orm通常会直接把错误传递给我们,如果ORMorm块在定义错误的时候采用的是这种方式,那么我们就可以判断得到的错误是不是orm.Err类型,以了解到这个错误到底是在orm模块发生的还是在数据库模块发生的,而且返回错误的过程更加简单。