初学rust,如何实现在运行时对全局变量设置和读取
Tag 运行时, 全局变量, 读写, on by view 241

在rust中,如何实现在运行时对全局变量设置和读取,这个问题困扰了我一段时间。因为在rust中,全局变量是不能在运行时修改的。rust的全局变量是属于全局静态变量,使用关键词static来定义,如下:

static mut count: usize = 0;

pub fn init_config(file_path: &str) {
    count = count + 1;
}

编译器会告诉你修改count不安全(unsafe)。

➜  las git:(feature/self-update-with-watcher-fork) ✗ cargo build
   Compiling las v0.1.1 (/root/code/las)
error[E0133]: use of mutable static is unsafe and requires unsafe function or block
  --> src/config/cfg.rs:35:13
   |
35 |     count = count + 1;
   |             ^^^^^ use of mutable static
   |
   = note: mutable statics can be mutated by multiple threads: aliasing violations or data races will cause undefined behavior

error[E0133]: use of mutable static is unsafe and requires unsafe function or block
  --> src/config/cfg.rs:35:5
   |
35 |     count = count + 1;
   |     ^^^^^^^^^^^^^^^^^ use of mutable static
   |
   = note: mutable statics can be mutated by multiple threads: aliasing violations or data races will cause undefined behavior

For more information about this error, try `rustc --explain E0133`.
error: could not compile `las` (bin "las") due to 2 previous errors

一定要修改的话,需要加unsafe块包裹住:

static mut count: usize = 0;

pub fn init_config(file_path: &str) {
    unsafe { count = count + 1 };
}

但是,在rust中使用unsafe是一件不好的事情,我们应该尽量避免使用unsafe,这里有另一个方案可以实现在运行时设置全局变量,就是使用once_cell这个包,虽然它的底层也是基于unsafe来实现的,但是它对“不安全”代码进行了一定的包装。具体用法如下:

static CFG: OnceCell<Conf> = OnceCell::new();

// 初始化和设置
pub fn init_config(file_path: &str) {
    CFG.get_or_init(|| load_config(file_path));
}

// 读取
pub fn get_config() -> &'static Conf {
    CFG.get().unwrap()
}

可以看到,我们可以在运行时使用once_cellget_or_init方法对全局变量进行初始化设置,它还有set方法也可以在运行时对全局变量进行设置,get方法在运行时对全局变量进行读取。从而实现不直接使用unsafe块,来操作全局变量。而且once_cell使用的全局变量完全不必申明为可变(mut)变量。

我们可以通过追踪once_cell::get_or_init方法,可以看到在initialize方法中unsafe块中,将value设置给了*slot

qby9tqm9

可以看到,once_cell对全局变量的操作进行了安全封装,因此,建议使用once_cell进行全局变量的操作,而不是到处使用unsafe块进行不安全的操作。

^注意:实践证明,OnceCell只能调用set()对全局变量设置一次,后续再调用set()就会报错。要反复读写甚至多线程中读写全局变量,应该使用RwLock,详情参考新的文章《初学rust,多线程中读写全局变量》


服务程序实现自己更新自己
Tag 服务, 自更新, on by view 96

作为一个广泛存在于众多设备上的常驻程序如何更新?他可以是一个agent,也可以是一个service,可以运行在众多容器中作为基础支持的程序,也可以运行在路由器、空调、洗衣机等家用设备或者物联设备上的支持程序。或许有些程序可以通过第三方平台进行更新,比如某些服务程序,配有专门的发版平台。但是更多的是无法通过发版平台发布的程序,比如是linux上的一个自启动的service,这个linux是你家里的路由器。这时候就需要程序自己更新自己了。

程序更新通常有下列步骤,下载新的程序,替换旧的可执行文件和配置文件,重启程序(退出旧进程,拉起新进程)。

程序自己如何去做这些操作?通常下载和替换文件并不难,只需要有一个接口能查询到新的程序版本和下载地址,很容易实现下载新版程序和替换。唯一需要注意一点的是重启程序这一步骤,这一步骤在业务程序进程上是无法实现的,因为它要拉起一个业务进程(worker),并且退出自己,在它拉起业务进程的时候,如果自己本身就是业务进程(worker),这样就会有两个业务进程(worker),这对于大多数服务是不允许的,会出错。那么我们可以专门设计一个守护进程(watcher)仅用于程序的更新与重启。

因此watcher进程里,更新程序和重启程序有如下流程 hhy70tzc

下载替换与杀旧进程(worker)放在一个线程中,另一个线程只负责监视worker进程是否存在,如果worker进程不存在,则拉起worker进程。

同一个程序中,如何区分worker进程与watcher进程?我这里使用的是特定环境变量来区分的。终端中拉起程序,默认启动的是watcher进程,watcher进程启动后检测到worker进程不存在,那么它将会通过exec.Command拉起进程,并且注入特定环境变量,如下

cmd := exec.Command(procName, os.Args[1:]...)
cmd.Env = append(os.Environ(), "PROC_FLAG=worker")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if e := cmd.Start(); nil != e {
	log.Printf("ERROR: %v\n", e)
	return
}

而且程序启动的时候,首先也会判断该环境变量是否存在,如果存在该环境变量,那么就进入业务进程的业务逻辑。如果不存在该环境变量,则进入守护进程的守护逻辑。

这样一来就能够实现程序自己更新自己了。


初学rust,没有途径修改argv[0]
Tag argv0, rust, 进程标题, on by view 48

我们知道在C语言程序中,可以通过修改argv[0]的值,来实现改变一个进程在ps命令中显示的标题,先给一个C语言的demo如下:

#include <stdio.h>
#include <string.h>
  
extern char **environ;
  
int main(int argc , char *argv[]) {
    int i;
  
    printf("argc:%d\n" , argc);
    for (i = 0; i < argc; i++) {
        printf("argv[%d]:%s\t0x%x\n" , i , argv[i], argv[i]);
    }
  
    for (i = 0; i < argc && environ[i]; i++) {
        printf("evriron[%d]:%s\t0x%x\n" , i , environ[i], environ[i]);
    }

    strcpy(argv[0], "nginx: process is shuting down");

    sleep(1000);
    return 0;
}

进程原本的名称是demo,但是我们通过strcpy修改了argv[0]之后,ps命令显示进程的名称变为我们指定的nginx: process is shuting down,如下

➜  build git:(master) ✗ ps -ef | grep nginx
root     1263942 1252322  0 15:29 pts/2    00:00:00 nginx: process is shuting down
root     1263973 1253542  0 15:29 pts/3    00:00:00 grep --color=auto --exclude-dir=.bzr --exclude-dir=CVS --exclude-dir=.git --exclude-dir=.hg --exclude-dir=.svn --exclude-dir=.idea --exclude-dir=.tox nginx

这也是nginx程序实现在修改进程标题为nginx: master, nginx: worker 以及 nginx: process is shuting down 的原理,从而实现在ps的标题中标识不同类别的进程。

我们在rust语言中如何达到相同的效果呢,查阅资料了解到,通过下面方法可以修改进程名

use nix::libc::{PR_SET_NAME, prctl};
use std::ffi::CString;

fn main() {
    let new_name = CString::new("NewProcessName").expect("CString::new failed");
    unsafe {
        prctl(PR_SET_NAME, new_name.as_ptr() as usize, 0, 0, 0);
    }
}

但是实际使用后,发现,这种方法修改的进程名并不是ps命令显示的进程标题,ps命令显示的进程标题还是不变,而是修改了pstree命令显示的进程树种的进程名称,所以,这种方法并不能达到我们想要的效果。

我们尝试修改argv[0],在rust中是通过env::args()来获取程序的传参,也即argv,追踪到env::args()调用的是env::args_os(),于是我们有这么一段代码尝试修改argv[0]:

use std::env;
use std::ffi::{CStr, CString, OsString};
use std::os::unix::ffi::OsStringExt;

fn set_process_title(title: &str) {
    let args: Vec<OsString> = env::args_os().collect();
    let mut argv: Vec<*mut i8> = args.iter()
        .map(|arg| {
            let arg_cstring = CString::new(arg.as_bytes()).expect("Failed to create CString");
            arg_cstring.into_raw()
        })
        .collect();
    argv.push(std::ptr::null_mut());

    let title_cstring = CString::new(title).expect("Failed to create CString");

    unsafe {
        strcpy(argv[0] as *mut c_char, title_cstring.as_ptr());
    }
}

fn main() {
    set_process_title("MyWorker");

    // 继续执行其他操作...
}

但是很遗憾,并没有修改argv[0]的效果,继续追踪args_os()发现,其实在多个地方存在clone()操作,我们获取到的argv早就不是原始的argvargs_os()的实现如下

#[stable(feature = "env", since = "1.0.0")]
pub fn args_os() -> ArgsOs {
    ArgsOs { inner: sys::args::args() }
}

sys::args::args()的实现如下

/// Returns the command line arguments
pub fn args() -> Args {
    imp::args()
}

imp::args()的实现如下

pub fn args() -> Args {
    Args { iter: clone().into_iter() }
}

这里clone()实现如下

fn clone() -> Vec<OsString> {
    unsafe {
        // Load ARGC and ARGV, which hold the unmodified system-provided
        // argc/argv, so we can read the pointed-to memory without atomics
        // or synchronization.
        //
        // If either ARGC or ARGV is still zero or null, then either there
        // really are no arguments, or someone is asking for `args()`
        // before initialization has completed, and we return an empty
        // list.
        let argv = ARGV.load(Ordering::Relaxed);
        let argc = if argv.is_null() { 0 } else { ARGC.load(Ordering::Relaxed) };
        let mut args = Vec::with_capacity(argc as usize);
        for i in 0..argc {
            let ptr = *argv.offset(i) as *const libc::c_char;

            // Some C commandline parsers (e.g. GLib and Qt) are replacing already
            // handled arguments in `argv` with `NULL` and move them to the end. That
            // means that `argc` might be bigger than the actual number of non-`NULL`
            // pointers in `argv` at this point.
            //
            // To handle this we simply stop iterating at the first `NULL` argument.
            //
            // `argv` is also guaranteed to be `NULL`-terminated so any non-`NULL` arguments
            // after the first `NULL` can safely be ignored.
            if ptr.is_null() {
                break;
            }

            let cstr = CStr::from_ptr(ptr);
            args.push(OsStringExt::from_vec(cstr.to_bytes().to_vec()));
        }

        args
    }
}

可以看到argv是从ARGV.load(Ordering::Relaxed);中加载出来的。可以看到argv经历了多次拷贝,最终才到args,然后通过args_os()再呈现在我们面前,实际早就不再是最初的那个argv。


端午游锦绣中华
Tag 端午, 锦绣中华, on by view 49

含苞待放的荷花

84ixt04a

夜景灯展

ap8qyfq8

oagum0wj


2023年上半年简单回顾
Tag 生活, 回顾, on by view 62

转眼间23年已经过了一半,简单回顾一下这半年的一些事情。

22年底12月份阳了,到了23年大家再也不关注新冠了

自从去年年底放开新冠防控之后,新冠在去年十二月份席卷全国,深圳万人空巷,就如同到了春节大家都回老家了一样,街头几乎见不到车辆与行人,阳了的人在家里躺着,没阳的人害怕阳了,不敢出门,但最终还是逃不过阳了。工作日公司办公室,整层楼到岗人数不足十人。

阳了很难受,发烧两天才停止发烧,之后感觉自己气管就像被刮掉一层皮一样,气管里面咳出来的全是浓痰,咳嗽异物感严重。恢复之后,还连续干咳了一个多月,才逐渐消停。发烧的这两天,还去社康“求助”过,平日人很少的社康,塞满了人,社康门口全是病人排队付钱做抗原检测,因为全国全网都买不到抗原检测试剂盒,公司发的五份抗原检测试剂盒也很快被家人用完了,我发烧第一天在社康测抗原没测出来,医生开了点治疗感冒的中成药,问到退烧药,医生说早就已经没有了;第二天再去社康测抗原,强阳性,我就直接回家了,反正医生那边连退烧药都开不出来,去看医生也没用,好在家里还有一盒退烧药。那几天也有邻居到处借退烧药,分了2颗给别人。

去年十二月份初次阳了之后,很长时间没有阳了,直到5月。大家都在传第二波新冠来了,然后5月底,我老婆二阳了,6月初,我也二阳了。第二轮感染新冠,只发烧了一晚上,吃了一颗退烧药,没多久就退烧了,第二天测出来强阳性,然后就是有点咽喉发炎,扁桃体的老毛病又犯了。乘着周末跑了一趟医院,医生开了点感冒药,然后咽喉含片,吃了两天,完全恢复了。总体来看,第二轮阳了病情比较轻,恢复也很快,遗留症状几乎没有。

困扰我多日的“牙齿”崩坏问题通过洗牙解决了

从去年开始,我的牙齿经常会因为咬到硬骨头就会崩下来一块,崩下来的东西外层黄色,内层黑色,还有臭味。我一度以为那是我牙齿的一部分骨质。再加上牙龈会出血,我以为我牙齿要坏掉了。于是挂了牙齿专科医院看了一下牙齿。医生看了之后,告诉我,我牙齿上全是结石,让我预约洗牙。最终,进洗牙室人生第一次洗了牙,那感觉真是“酸爽”,洗完之后,满嘴的血,牙龈出血严重。但是我照镜子之后才知道,原来我的牙齿可以这么白亮。遵医嘱,当天注意饮食,吃清淡的白粥,不受冷热刺激。到了第二天下午,出血的牙龈基本上已经完全恢复,留下的只有一口的白牙。为此我还嘚瑟了几天。洗掉了二三十年积累下来的牙结石之后,困扰我多日的牙龈出血问题终于解决了。

拖了四年多的驾校学习,终于拿到驾驶证了

另一件值得一提的事情就是,我在6月份终于拿到了驾驶证。困扰我多年的科一练题在二月份一个月的时间里终于解决了,其实找到科一练题的方法之后,科一并没有那么难。如果没有找到方法,去刷题库1000题或者精简题库500题,终究是反复折腾记了忘、忘了记。科二练习了5天,然后去考试,一把过。科三练习了3天,去考试,两把都是挂在终点。科三第二次考试,第一把也是挂在终点,因为终点掉灯之后忘记补灯了,第二把设备故障导致中断,重新补考第二把,终于完美通过了。科三考完回家第二天就阳了。最终,等了一个星期,完全恢复之后,练了差不多一个星期的科四题目,在周六去考了科四,终于拿到了驾驶证,成为了一个合法的驾驶员。

时间过得真快,很多事情已经是物是人非了。估计下半年还会有更多值得回顾的事情。


QQ账号在网页上一键登录的实现原理
Tag 登录, 鉴权, QQ, on by view 268

我们经常可以在网页上看到QQ快捷登录,只需要点击一下QQ图像,不需要账号,不需要密码,不需要扫码,就可以直接登录了。

apnf6a4m

下面简单介绍一下这其中的原理,我们可以在浏览器请求中找到这么一个请求,如下图

4fnj1xbe

可以发现它请求的是127.0.0.1:4301,很明显这个服务是一个本地服务,它就运行在你电脑上。其实它就是你QQ客户端上运行的一个服务,可以看到,请求了这个服务的接口之后,服务响应中设置了相关的cookies,后续,QQ系的网站就可以根据这个cookies进行鉴权了。

所以说,要实现类似于QQ的这种一键登录能力,你需要有个本地客户端,客户端要提供一个http接口,你的网站会请求这个本地客户端,已鉴权的本地客户端收到这个网页上的请求之后,就会将当前登录账号的鉴权cookies植入到浏览器了,这样就成功的实现了一键快捷登录鉴权。这也就是所谓的桌面客户端为浏览器植入cookies的技术。


初学rust,初始化一个C语言结构体
Tag rust, clang, 结构体, on by view 75

最近用到了libc这个包,调用其中的statfs函数,用于查询指定路径挂载的磁盘占用。可以看到

pub fn statfs(path: *const ::c_char, buf: *mut statfs) -> ::c_int;

statfs第二个参数是一个C语言结构体statfs,其定义如下

pub struct statfs {
    pub f_type: ::__fsword_t,
    pub f_bsize: ::__fsword_t,
    pub f_blocks: ::fsblkcnt_t,
    pub f_bfree: ::fsblkcnt_t,
    pub f_bavail: ::fsblkcnt_t,

    pub f_files: ::fsfilcnt_t,
    pub f_ffree: ::fsfilcnt_t,
    pub f_fsid: ::fsid_t,

    pub f_namelen: ::__fsword_t,
    pub f_frsize: ::__fsword_t,
    f_spare: [::__fsword_t; 5],
}

如果用常规的rust初始化方法,必须填充结构体的各字段

let x = statfs{
    ...,
    f_spare: ...,
}

可以看到其中f_spare字段是一个数组,属于复杂类型,用rust的初始化方式,需要一个个字段的填充,而且需要填充为初始值0,将会非常复杂。其实有一种简单方法,使用std::mem::zeroed函数即可。举例如下

fn statfs(&mut self, mount_path: String) -> String {
    let x = CString::new(mount_path.as_bytes()).expect("covert cstring failed");

    let mut info = String::from("");
    let mut statfs_buf = unsafe { std::mem::zeroed() };
    let ret = unsafe { libc::statfs(x.as_ptr(), &mut statfs_buf) };
    if ret == 0 {
        info = format!(
            "{} {} {} {}",
            statfs_buf.f_bsize, statfs_buf.f_blocks, statfs_buf.f_bfree, statfs_buf.f_bavail
        );
    }
    return info;
}

可以看到std::mem::zeroed()方法,直接能够初始化一段0值的内存空间,这段空间具体大小,直接由下一行libc::statfs(...)调用的第二个参数类型决定,由这一行直接能推导出该申请多大的0空间。


初学rust,tokio无法在spawn中使用MutexGuard
Tag rust, tokio, MutexGuard, spawn, on by view 120

最近再次在rust中尝试用tokio::spawn实现类似go语言中goroutine的用法,但是报错了。

#[tokio::main]
async fn main() {
    let cfg = cfg::get_config();
    let client_id = cfg::get_client_id();
    let ws_addr = cfg.agent.as_ref().unwrap().remote_ws_addr.as_ref().unwrap();

    let mut wsc = ws::WebSocketClient::new(ws_addr.clone(), client_id);
    tokio::spawn(wsc.start());

    let monitor_addr = cfg.agent.as_ref().unwrap().remote_ws_addr.as_ref().unwrap();
    let mut monitor_client = monitor::Monitor::new(monitor_addr.to_string());
    monitor_client.start().await;
}

报错位置在这一行 tokio::spawn(wsc.start());,报错内容如下

error: future cannot be sent between threads safely
   --> src/main.rs:19:18
    |
19  |     tokio::spawn(wsc.start());
    |                  ^^^^^^^^^^^ future returned by `start` is not `Send`
    |
    = help: the trait `Sync` is not implemented for `std::sync::mpsc::Receiver<Message>`
note: future is not `Send` as this value is used across an await
   --> src/ws.rs:117:47
    |
115 |                 if let Ok(msg) = rx.recv() {
    |                                  -- has type `&std::sync::mpsc::Receiver<Message>` which is not `Send`
116 |                     println!("Socket Send    : {:?}", msg);
117 |                     let rst = writer.send(msg).await;
    |                                               ^^^^^^ await occurs here, with `rx` maybe used later
...
126 |             }
    |             - `rx` is later dropped here
help: consider moving this into a `let` binding to create a shorter lived borrow

可以看到原因是,我在WebSocketClient中用到了MutexGuard,而MutexGuard不支持被tokio::spawn调用。解释参考这篇文档。实际上我是在另一个结构体Pty中使用了Arc<Mutex<Receiver<Message>>>类型,然后WebSocketClient中又实用了Pty结构体,所以在调用tokio::spawn时报错。

如何解决?别用标准库的Mutex了,用tokio的。而且,我同样发现了Sender,Receiver都在报类似的错误the trait std::marker::Send is not implemented for ...

use std::sync::mpsc::{channel, Receiver, Sender}

// 改为
use tokio::sync::mpsc::channel;
use tokio::sync::mpsc::{Receiver, Sender};
use tokio::sync::Mutex;

channel,Receiver,Sender全部改为tokio版本的,就可以兼容tokio了。


初学rust,多态JSON对象的序列化与反序列化
Tag rust, 序列化, 反序列化, on by view 167

在rust中序列化反序列化JSON我们常用的是serde这个包,但是json中有一种情况就是存在不确定类型的字段,或者说这个字段可能是多种类型;在golang中,我们用interface{}类型来表达,在java中,我们用Object表达,那么在rust应该如何表达呢。直接说结果,用enum来组合类型

比如我定义了一种JSON数据结构,如下

// 第一种
{
    "msg_type": "resize",
    "content": {
        "width": 12,
        "height": 3
    }
}

// 第二种
{
    "msg_type": "ping",
    "content": {
        "ts": 123
    }
}

很明显,两个数据中,content字段表现的类型不一样,在msg_typeresizecontent字段是一种结构体,msg_typepingcontent字段是另一种结构体。我们定义枚举和结构体如下

#[derive(Clone, Debug, Deserialize)]
struct ControlMessage {
    msg_type: String,
    #[serde(default)]
    content: ControlContent,
}

#[derive(Clone, Debug, Deserialize, PartialEq)]
#[serde(untagged)]
enum ControlContent {
    Empty,
    Resize(ResizeControl),
    Time(TimeControl),
}

impl Default for ControlContent {
    fn default() -> Self {
        Self::Empty
    }
}

#[derive(Clone, Debug, Deserialize, PartialEq)]
struct ResizeControl {
    width: u32,
    height: u32,
}

#[derive(Clone, Debug, Deserialize, PartialEq)]
struct TimeControl {
    ts: u32,
}

可以看到我们定义了两种类型ResizeControlTimeControl,需要注意以下几点:

  1. 要支持反序列化,所有用到的结构体都需要添加Deserialize注解
  2. 枚举中,Empty,Resize,Time这3个都属于枚举的字段名,圆括号()里定义类型,没有定义的表示空
  3. 枚举上,需要添加untagged注解
  4. 要为枚举实现Default::default方法,否则无法被Deserialize注解
  5. 被枚举引用的结构体ResizeControl,TimeControl,以及枚举ControlContent都需要加上PartialEq注解,否则无法在枚举圆括号()中引用

调用反序列化

#[test]
pub fn test_field_deserialize() -> serde_json::Result<()> {
    let a = r#"{"msg_type": "resize", "content": {"width": 12, "height": 3}}"#;
    let v: ControlMessage = serde_json::from_str(a)?;
    println!("v: {:?}", v);

    let b = r#"{"msg_type": "resize", "content": {"ts": 123}}"#;
    let v: ControlMessage = serde_json::from_str(b)?;
    println!("v: {:?}", v);

    Ok(())
}

结果调用成功

running 1 test
v: ControlMessage { msg_type: "resize", content: Resize(ResizeControl { width: 12, height: 3 }) }
v: ControlMessage { msg_type: "resize", content: Time(TimeControl { ts: 123 }) }
test control::test_field_deserialize ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

初学rust,return与无分号结尾区别
Tag rust, return, on by view 197

在rust中,我们在一开始学习rust时,就被告诉,在函数结尾可以不用写return,只需要结尾的那个语句别写分号(;),那么结尾的这个语句计算的值将会作为这个函数的返回值返回。

#[test]
fn test_return_2() {
    println!("none semi: {}", none_semicolon());
}

fn none_semicolon() -> String {
    String::from("hello world")
}

正常输出

running 1 test
none semi: hello world
test test_return_2 ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

这时,我形成一个印象,“结尾语句不加分号,可以当return用”。还有如下的例子

fn test_return_1() -> bool {
    let x = Option::Some(true);

    let y = match x {
        Some(val) => {
            println!("{:?}", val);
            val
        }
        None => false,
    };

    println!("will finish function, y: {:?}", y);

    false
}

我可以看到y变量是bool类型,这时候我以为val这句没有加分号,所以它将值返回给语句块,从而赋值给y,但是,这时候,我改为下面这样的

#[test]
fn test_return_2() {
    println!("none semi: {}", test_return_1());
}

fn test_return_1() -> bool {
    let x = Option::Some(true);

    let y = match x {
        Some(val) => {
            println!("{:?}", val);
            return val;
        }
        None => false,
    };

    println!("will finish function, y: {:?}", y);

    false
}

我发现,return 处就结束了函数并返回。后续的println并没有执行。

running 1 test
true
none semi: true
test test_return_2 ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

总结,位于语句块或者函数体结尾无分号结尾的语句,并不等同于return,它处于语句块结尾时,代表将它的值赋值给语句块所代表的变量,处于函数体结尾时,代表将它的值赋值给函数体所代表的变量(函数返回变量);而return这个函数终止返回的指令,从来都跟无分号结尾的语句无关,而是函数体的反花括号 (}) 的出现所带来的行为。