1. Are tied to processes. They are not first-class values. (The processes are, and the mailboxes go with them.)
2. Can receive messages out of order using pattern matching. This is critically used all over the place for RPC simulations; you send a message out to some other process, and then receive the reply by matching on the mailbox. You may receive other messages in the meantime in that mailbox, but the RPC process is safe because the block waiting for the answer will keep waiting for that message, and the rest will stay there for later reception.
3. Are zero-or-one delivery. Messages going to the same OS process are reliable enough to be treated as reliable exactly-once delivery but you are not really supposed to count on that. (This is one of the ways you can write an Erlang system and you accidentally make it so it can't run on a cluster.) Messages going across nodes may not arrive because the network may eat them. If you're really excited you can examine a process ID to see if it's local but you're generally not supposed to do that.
As part of this, mailboxes are fundamentally asynchronous. You can not wait on "the message has arrived at the other end" because in a network environment this isn't even a well-defined concept. The only way to know that a message arrived is to be explicitly sent an acknowledgement, an acknowledgement that may itself get lost (Byzantine general problem).
4. Send Erlang terms, only Erlang terms, and exactly Erlang terms. Erlang terms are an internal dynamically-typed language with no support for user-defined types. This is important because this is how Erlang is built around upgradeability and having multiple versions of things in the same cluster. Since there are no user-defined types, you never have problems with having mismatched type definitions. (You can still have mismatched data, of course; if a format changes, it changes. But the problem is at least alleviated by not supported complicated user defined types. It is, arguably and in my opinion, a throwing the baby out with the bathwater situation, but I do admit against interest (as the lawyers say) that practically it works out reasonably well. Especially since the architect of the system really ought to know this is how it works up front.)
Because of the dynamically-typed nature, they are effectively untyped. Any message can be sent to a mailbox.
5. Are many-to-one communication, across an entire Erlang cluster. A given mailbox is only visible to one Erlang process; it is part of that process. Again, the process ID is a first-class value that can be passed around but the mailbox is not.
6. There are debug mechanisms that allow you to look into a mailbox live, on the cluster's shell. You can see what is currently in there. You really shouldn't be using these debug facilities as part of your system, but you can use them as a devops sort of thing. (As hard as I've been on Erlang, the devops story is pretty powerful for fixing broken systems. That said, the dynamic types means I had to fix more broken systems live than I ever have for Go; I haven't missed this because my Go systems generally don't break. Still, if they are going to break, Erlang has a lot of tools for dealing with it live.)
Go channels:
1. Are first-class values not tied to any goroutine. One goroutine may create a channel and pass it off to two others who will communicate on it, and they may further pass it around.
2. Are intrinsically ordered, at least in the sense that receivers can't go poking along the channel to see what they want to pull out of it.
However, an aspect of Go channels is that with a "select" statement, a single goroutine can wait on an arbitrary combination of "things I'm trying to send" and "things I'm trying to receive". This is entirely unlike pattern matching on a mailbox and is probably a really good example of the way you need solutions to certain communication problems, but they don't have to be the exact Erlang solution in order to work. Complicated communications scenarios that Erlang might achieve with selective matching can be done in Go with multiple channels multiplexed with a select. There are tradeoffs in either direction and it isn't clear to me one dominates the other at all.
3. Are synchronous exactly-once delivery. This further implies they only work on a single machine, and in Go they only work within a single process. This further implies that there is no such thing as a "network channel" in Go. You can, of course, have all sorts of things that sort of look like channels that work over a network, but it is fundamentally impossible to have a channel (of the "chan" type that can participate in the "select" statement) that goes over the network because no network can maintain the properties required for a Go channel.
It is also in general guaranteed that if you proceed past a send on a channel, that some other goroutine has received the value. This makes it useful for synchronization.
(There are buffered channels that can hold a certain fixed number of values without having actually been sent, but in general I think they should be treated as exceptions precisely because losing this property is a bigger deal that people often realize. A lot of things in Go are built on it. Contrary to popular belief, unbuffered channels are not asynchronous, because they are fixed size. They're just asynchronous, up to a certain point. Erlang mailboxes are asynchronous, until you run out of memory, which is not unheard of or impossible but isn't terribly common, especially if you follow the normal OTP patterns.)
4. Are typed. Each channel sends exactly one type of Go value, though this value can be an interface value meaning it can theoretically send multiple concrete types. (Generally I define the channel with the type I want though there is the occasional use case where I have a channel with a closed interface that basically uses the interface value like a sum type. I am still not sure whether this is better or worse that having a channel per possible type, and I've been doing this for a long time. I'm still going back and forth.)
5. Are many-to-many communication, isolated to a single OS process. It is perfectly legal and valid to have a single channel value that has dozens of producers and dozens of consumers. Performance implications are related to the rate these goroutines are trying to communicate over; if, for instance, at low rates it's no problem at all.
6. Are completely opaque, even within Go. There is no "peek", which would after all break the guarantee that if an unbuffered channel has had a "send" that there has been a corresponding "receive".
Contra another comment I see, no, you can not implement one in terms of the other. You can get close-ish, but there are certain aspects of them that simply do not cross, notably their sync/async nature, their differing network transparencies, and the inability of an Erlang mailbox to be "many-to-many", particularly in the way the "many-to-many" still guarantees "exactly once" delivery. (You can set up certain structures in Erlang that get close to this, but no matter what you do, you can not build any abstraction on a zero-or-one delivery system to create an exactly-one delivery system.)
You can solve almost any problem you have with either of these. You can use either to sort of get close to the other, but in both directions you'll sacrifice significant native capabilities in the process. It's really an intriguing study in how the solution to very similar problems can almost intertwine like an Asclepius staff, twisting around the same central pole while having basically no overlap. And again, it's not clear to me that either is "better"; both have small parts of the problem space where they are better than the other, both solve the vast bulk of problems you'll encounter just fine.
1. Are tied to processes. They are not first-class values. (The processes are, and the mailboxes go with them.)
2. Can receive messages out of order using pattern matching. This is critically used all over the place for RPC simulations; you send a message out to some other process, and then receive the reply by matching on the mailbox. You may receive other messages in the meantime in that mailbox, but the RPC process is safe because the block waiting for the answer will keep waiting for that message, and the rest will stay there for later reception.
3. Are zero-or-one delivery. Messages going to the same OS process are reliable enough to be treated as reliable exactly-once delivery but you are not really supposed to count on that. (This is one of the ways you can write an Erlang system and you accidentally make it so it can't run on a cluster.) Messages going across nodes may not arrive because the network may eat them. If you're really excited you can examine a process ID to see if it's local but you're generally not supposed to do that.
As part of this, mailboxes are fundamentally asynchronous. You can not wait on "the message has arrived at the other end" because in a network environment this isn't even a well-defined concept. The only way to know that a message arrived is to be explicitly sent an acknowledgement, an acknowledgement that may itself get lost (Byzantine general problem).
4. Send Erlang terms, only Erlang terms, and exactly Erlang terms. Erlang terms are an internal dynamically-typed language with no support for user-defined types. This is important because this is how Erlang is built around upgradeability and having multiple versions of things in the same cluster. Since there are no user-defined types, you never have problems with having mismatched type definitions. (You can still have mismatched data, of course; if a format changes, it changes. But the problem is at least alleviated by not supported complicated user defined types. It is, arguably and in my opinion, a throwing the baby out with the bathwater situation, but I do admit against interest (as the lawyers say) that practically it works out reasonably well. Especially since the architect of the system really ought to know this is how it works up front.)
Because of the dynamically-typed nature, they are effectively untyped. Any message can be sent to a mailbox.
5. Are many-to-one communication, across an entire Erlang cluster. A given mailbox is only visible to one Erlang process; it is part of that process. Again, the process ID is a first-class value that can be passed around but the mailbox is not.
6. There are debug mechanisms that allow you to look into a mailbox live, on the cluster's shell. You can see what is currently in there. You really shouldn't be using these debug facilities as part of your system, but you can use them as a devops sort of thing. (As hard as I've been on Erlang, the devops story is pretty powerful for fixing broken systems. That said, the dynamic types means I had to fix more broken systems live than I ever have for Go; I haven't missed this because my Go systems generally don't break. Still, if they are going to break, Erlang has a lot of tools for dealing with it live.)
Go channels:
1. Are first-class values not tied to any goroutine. One goroutine may create a channel and pass it off to two others who will communicate on it, and they may further pass it around.
2. Are intrinsically ordered, at least in the sense that receivers can't go poking along the channel to see what they want to pull out of it.
However, an aspect of Go channels is that with a "select" statement, a single goroutine can wait on an arbitrary combination of "things I'm trying to send" and "things I'm trying to receive". This is entirely unlike pattern matching on a mailbox and is probably a really good example of the way you need solutions to certain communication problems, but they don't have to be the exact Erlang solution in order to work. Complicated communications scenarios that Erlang might achieve with selective matching can be done in Go with multiple channels multiplexed with a select. There are tradeoffs in either direction and it isn't clear to me one dominates the other at all.
3. Are synchronous exactly-once delivery. This further implies they only work on a single machine, and in Go they only work within a single process. This further implies that there is no such thing as a "network channel" in Go. You can, of course, have all sorts of things that sort of look like channels that work over a network, but it is fundamentally impossible to have a channel (of the "chan" type that can participate in the "select" statement) that goes over the network because no network can maintain the properties required for a Go channel.
It is also in general guaranteed that if you proceed past a send on a channel, that some other goroutine has received the value. This makes it useful for synchronization.
(There are buffered channels that can hold a certain fixed number of values without having actually been sent, but in general I think they should be treated as exceptions precisely because losing this property is a bigger deal that people often realize. A lot of things in Go are built on it. Contrary to popular belief, unbuffered channels are not asynchronous, because they are fixed size. They're just asynchronous, up to a certain point. Erlang mailboxes are asynchronous, until you run out of memory, which is not unheard of or impossible but isn't terribly common, especially if you follow the normal OTP patterns.)
4. Are typed. Each channel sends exactly one type of Go value, though this value can be an interface value meaning it can theoretically send multiple concrete types. (Generally I define the channel with the type I want though there is the occasional use case where I have a channel with a closed interface that basically uses the interface value like a sum type. I am still not sure whether this is better or worse that having a channel per possible type, and I've been doing this for a long time. I'm still going back and forth.)
5. Are many-to-many communication, isolated to a single OS process. It is perfectly legal and valid to have a single channel value that has dozens of producers and dozens of consumers. Performance implications are related to the rate these goroutines are trying to communicate over; if, for instance, at low rates it's no problem at all.
6. Are completely opaque, even within Go. There is no "peek", which would after all break the guarantee that if an unbuffered channel has had a "send" that there has been a corresponding "receive".
Contra another comment I see, no, you can not implement one in terms of the other. You can get close-ish, but there are certain aspects of them that simply do not cross, notably their sync/async nature, their differing network transparencies, and the inability of an Erlang mailbox to be "many-to-many", particularly in the way the "many-to-many" still guarantees "exactly once" delivery. (You can set up certain structures in Erlang that get close to this, but no matter what you do, you can not build any abstraction on a zero-or-one delivery system to create an exactly-one delivery system.)
You can solve almost any problem you have with either of these. You can use either to sort of get close to the other, but in both directions you'll sacrifice significant native capabilities in the process. It's really an intriguing study in how the solution to very similar problems can almost intertwine like an Asclepius staff, twisting around the same central pole while having basically no overlap. And again, it's not clear to me that either is "better"; both have small parts of the problem space where they are better than the other, both solve the vast bulk of problems you'll encounter just fine.