1. Output location is different

Looking at the source code of fmt.Println, it is obvious in the comments and the code: writes to standard output, the content is output to os.Stdout, which is the standard output.

1
2
3
4
5
6
// Println formats using the default formats for its operands and writes to standard output.
// Spaces are always added between operands and a newline is appended.
// It returns the number of bytes written and any write error encountered.
func Println(a ...interface{}) (n int, err error) {
    return Fprintln(os.Stdout, a...)
}

Let’s look at println, because it is a built-in function, only the function description is written here, but in the comment, you can find this sentence: writes the result to standard error. This means that println/print is indeed output to stderr, which is the standard error output.

1
2
3
4
5
6
// The println built-in function formats its arguments in an
// implementation-specific way and writes the result to standard error.
// Spaces are always added between arguments and a newline is appended.
// Println is useful for bootstrapping and debugging; it is not guaranteed
// to stay in the language.
func println(args ...Type)

We can find the implementation of println in the go source code at runtime/print.go. The code is as follows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
func printnl() {
    printstring("\n")
}

func printstring(s string) {
    gwrite(bytes(s))
}

// write to goroutine-local buffer if diverting output,
// or else standard error.
func gwrite(b []byte) {
    if len(b) == 0 {
        return
    }
    recordForPanic(b)
    gp := getg()
    // Don't use the writebuf if gp.m is dying. We want anything
    // written through gwrite to appear in the terminal rather
    // than be written to in some buffer, if we're in a panicking state.
    // Note that we can't just clear writebuf in the gp.m.dying case
    // because a panic isn't allowed to have any write barriers.
    if gp == nil || gp.writebuf == nil || gp.m.dying > 0 {
        writeErr(b)
        return
    }
    n := copy(gp.writebuf[len(gp.writebuf):cap(gp.writebuf)], b)
    gp.writebuf = gp.writebuf[:len(gp.writebuf)+n]
}

In the gwrite function, the main focus here is on writeErr.

1
2
3
4
5
6
7
8
9
func writeErr(b []byte) {
    write(2, unsafe.Pointer(&b[0]), int32(len(b)))
}

func write(fd uintptr, p unsafe.Pointer, n int32) int32 {
    return write1(fd, p, n)
}

func write1(fd uintptr, p unsafe.Pointer, n int32) int32

You can see that writeErr passes the fd parameter with a unitptr of 2 and ends up making a system call at write1. And the standard error output has a file descriptor of exactly 2.

In unix, stdin, stdout, and stderr correspond to three file descriptors, 0, 1, and 2, respectively.

2. Function definition is different

Normally, they are used directly as println("?") or fmt.Println("?") , but the two function definitions are also somewhat different.

1
2
3
4
5
// println
func println(args ...Type)

// fmt.Println
func Println(a ...interface{}) (n int, err error)

While both accept multiple arguments, only fmt.Println is the one with a return value, where the first one returns the number of bytes of output content, and for Println a byte of the final newline character \n is added.

Besides, println/print does not accept array and structure parameters.

3. Different output formats

If the parameter implements the String() or Error() method, these two methods will be called when calling fmt.Println to print the parameter, while the built-in println/print will not.